├── .dockerignore ├── host ├── html │ └── error.html ├── docker-compose.yml ├── README.md └── nginx.conf ├── doc └── html │ └── example.png ├── html ├── favicon.ico ├── favicon │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-196x196.png │ └── favicon-32x32.png ├── control.js ├── js │ ├── common.js │ ├── plot_spectrum.js │ ├── plot_detection.js │ ├── plot_timing.js │ └── plot_map.js ├── lib │ └── blah2.css ├── display │ ├── spectrum │ │ └── index.html │ ├── map │ │ └── index.html │ ├── maxhold │ │ └── index.html │ ├── timing │ │ └── index.html │ └── detection │ │ ├── delay │ │ └── index.html │ │ ├── doppler │ │ └── index.html │ │ └── delay-doppler │ │ └── index.html ├── index.html └── controller │ └── index.html ├── example.png ├── script ├── crontab.txt └── blah2_rspduo_restart.bash ├── src ├── capture │ ├── hackrf │ │ ├── hackrf-blah2.png │ │ ├── README.md │ │ ├── HackRf.h │ │ └── HackRf.cpp │ ├── rspduo │ │ └── README.md │ ├── Source.cpp │ ├── usrp │ │ ├── Usrp.h │ │ └── Usrp.cpp │ ├── Capture.h │ ├── Source.h │ ├── kraken │ │ ├── Kraken.h │ │ └── Kraken.cpp │ └── Capture.cpp ├── data │ ├── meta │ │ ├── Constants.h │ │ ├── Timing.h │ │ └── Timing.cpp │ ├── Detection.h │ ├── IqData.h │ ├── IqData.cpp │ ├── Map.h │ ├── Detection.cpp │ ├── Track.h │ └── Map.cpp └── process │ ├── meta │ ├── HammingNumber.h │ └── HammingNumber.cpp │ ├── utility │ ├── Socket.cpp │ └── Socket.h │ ├── detection │ ├── Centroid.h │ ├── Interpolate.h │ ├── CfarDetector1D.h │ ├── Centroid.cpp │ ├── CfarDetector1D.cpp │ └── Interpolate.cpp │ ├── spectrum │ ├── SpectrumAnalyser.h │ └── SpectrumAnalyser.cpp │ ├── clutter │ ├── WienerHopf.h │ └── WienerHopf.cpp │ ├── tracker │ ├── Tracker.h │ └── Tracker.cpp │ └── ambiguity │ ├── Ambiguity.h │ └── Ambiguity.cpp ├── lib ├── sdrplay-3.15.2 │ └── SDRplay_RSP_API-Linux-3.15.2.run └── vcpkg.json ├── .gitignore ├── docker ├── README.md ├── Dockerfile-kraken └── Dockerfile-uhd ├── test ├── data │ └── README.md ├── unit │ └── process │ │ ├── meta │ │ └── TestHammingNumber.cpp │ │ ├── tracker │ │ └── TestTracker.cpp │ │ └── ambiguity │ │ └── TestAmbiguity.cpp └── README.md ├── api ├── package.json ├── Dockerfile ├── stash │ ├── timing.js │ ├── maxhold.js │ ├── iqdata.js │ └── detection.js └── server.js ├── .devcontainer ├── docker-compose.yml ├── README.md ├── Dockerfile └── devcontainer.json ├── Jenkinsfile ├── docker-compose.yml ├── LICENSE ├── config ├── config-usrp.yml ├── config-kraken.yml ├── radar4.yml ├── config-hackrf.yml └── config.yml ├── Dockerfile ├── README.md ├── CMakeLists.txt └── CMakePresets.json /.dockerignore: -------------------------------------------------------------------------------- 1 | save/ 2 | -------------------------------------------------------------------------------- /host/html/error.html: -------------------------------------------------------------------------------- 1 | radar/down -------------------------------------------------------------------------------- /doc/html/example.png: -------------------------------------------------------------------------------- 1 | ../../example.png -------------------------------------------------------------------------------- /html/favicon.ico: -------------------------------------------------------------------------------- 1 | ./favicon/favicon-32x32.png -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/example.png -------------------------------------------------------------------------------- /html/favicon/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/html/favicon/favicon-128x128.png -------------------------------------------------------------------------------- /html/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/html/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /html/favicon/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/html/favicon/favicon-196x196.png -------------------------------------------------------------------------------- /html/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/html/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /script/crontab.txt: -------------------------------------------------------------------------------- 1 | # add to /etc/crontab 2 | */5 * * * * root /opt/blah2/script/blah2_rspduo_restart.bash 3 | -------------------------------------------------------------------------------- /src/capture/hackrf/hackrf-blah2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/src/capture/hackrf/hackrf-blah2.png -------------------------------------------------------------------------------- /lib/sdrplay-3.15.2/SDRplay_RSP_API-Linux-3.15.2.run: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/30hours/blah2/HEAD/lib/sdrplay-3.15.2/SDRplay_RSP_API-Linux-3.15.2.run -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build*/* 2 | bin/* 3 | !build/README.md 4 | !bin/README.md 5 | *.a 6 | *.so 7 | .vscode/ 8 | doc/html/* 9 | doc/latex/* 10 | !doc/html/example.png 11 | save/* 12 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | The top level Dockerfile supports every SDR supported by the software. This folder contains a minimal/specific Dockerfile for each SDR platform. These specific Dockerfiles build from source where applicable. -------------------------------------------------------------------------------- /test/data/README.md: -------------------------------------------------------------------------------- 1 | # blah2 Test Data 2 | 3 | A set of golden data used for testing. 4 | 5 | ## Log 6 | 7 | | File | Description | 8 | | ------------- | ------------- | 9 | | `todo.rspduo.iq` | Stores 1 CPI of IQ data for the SDRPlay RspDuo. | 10 | -------------------------------------------------------------------------------- /src/data/meta/Constants.h: -------------------------------------------------------------------------------- 1 | /// @file Constants.h 2 | /// @brief Constants header namespace. 3 | /// @author 30hours 4 | 5 | #ifndef CONSTANTS_H 6 | #define CONSTANTS_H 7 | 8 | #include 9 | 10 | namespace Constants 11 | { 12 | /// @brief Speed of light (m/s). 13 | const uint32_t c = 299792458; 14 | } 15 | 16 | #endif -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blah2-api", 3 | "version": "1.0.0", 4 | "description": "blah2-api", 5 | "author": "github.com/30hours", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "dependencies": { 11 | "express": "^4.16.1", 12 | "js-yaml": "^4.1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | 5 | blah2-dev: 6 | user: vscode 7 | build: 8 | context: .. 9 | dockerfile: .devcontainer/Dockerfile 10 | volumes: 11 | - type: bind 12 | source: .. 13 | target: /workspace 14 | working_dir: /workspace 15 | command: sleep infinity -------------------------------------------------------------------------------- /host/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | nginx-web: 5 | external: true 6 | 7 | services: 8 | httpd: 9 | restart: always 10 | image: nginx:1.25.2-alpine 11 | ports: 12 | - 8080:80 13 | volumes: 14 | - ./nginx.conf:/etc/nginx/nginx.conf 15 | - ./html:/usr/local/apache2/htdocs 16 | environment: 17 | - VIRTUAL_HOST=domain.tld 18 | networks: 19 | - nginx-web 20 | container_name: blah2 -------------------------------------------------------------------------------- /html/control.js: -------------------------------------------------------------------------------- 1 | var host = window.location.hostname; 2 | 3 | $(document).on('keypress', function (e) { 4 | if (e.which == 32) { 5 | url = "/capture/toggle"; 6 | 7 | $.getJSON('http://' + host + ':3000/capture/toggle', function () { }) 8 | 9 | .done(function (data) { 10 | console.log('API worked'); 11 | }) 12 | 13 | .fail(function () { 14 | console.log('API Fail'); 15 | }) 16 | 17 | .always(function () { 18 | 19 | }); 20 | 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # todo: currently not working 2 | 3 | ## Usage 4 | 5 | Install a recent `nodejs` using [nvm](https://github.com/nvm-sh/nvm). 6 | 7 | ``` 8 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash 9 | nvm install node 21.6.2 10 | ``` 11 | 12 | Install the latest [devcontainer CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli). 13 | 14 | ``` 15 | npm install -g @devcontainers/cli 16 | devcontainer --version 17 | ``` 18 | 19 | Run the devcontainer. 20 | 21 | ``` 22 | devcontainer up --workspace-folder . 23 | ``` -------------------------------------------------------------------------------- /html/js/common.js: -------------------------------------------------------------------------------- 1 | function is_localhost(ip) { 2 | 3 | if (ip === 'localhost') { 4 | return true; 5 | } 6 | 7 | const localRanges = ['127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12']; 8 | 9 | const ipToInt = ip => ip.split('.').reduce((acc, octet) => (acc << 8) + +octet, 0) >>> 0; 10 | 11 | return localRanges.some(range => { 12 | const [rangeStart, rangeSize = 32] = range.split('/'); 13 | const start = ipToInt(rangeStart); 14 | const end = (start | (1 << (32 - +rangeSize))) >>> 0; 15 | return ipToInt(ip) >= start && ipToInt(ip) <= end; 16 | }); 17 | 18 | } -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | LABEL maintainer="30hours " 4 | LABEL org.opencontainers.image.source https://github.com/30hours/blah2 5 | 6 | # Create app directory 7 | WORKDIR /usr/src/app 8 | 9 | # Install app dependencies 10 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 11 | # where available (npm@5+) 12 | COPY package*.json ./ 13 | 14 | RUN npm install 15 | # If you are building your code for production 16 | # RUN npm ci --only=production 17 | 18 | # Bundle app source 19 | COPY . . 20 | 21 | EXPOSE 8080 22 | CMD [ "node", "server.js" ] 23 | -------------------------------------------------------------------------------- /test/unit/process/meta/TestHammingNumber.cpp: -------------------------------------------------------------------------------- 1 | /// @file TestHammingNumber.cpp 2 | /// @brief Unit test for HammingNumber.cpp 3 | /// @author 30hours 4 | /// @author Dan G 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "process/meta/HammingNumber.h" 11 | 12 | /// @brief Test Hamming number calculation. 13 | TEST_CASE("Next_Hamming", "[hamming]") 14 | { 15 | CHECK(next_hamming(104) == 108); 16 | CHECK(next_hamming(3322) == 3375); 17 | CHECK(next_hamming(19043) == 19200); 18 | } -------------------------------------------------------------------------------- /script/blah2_rspduo_restart.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run script with a crontab to automatically restart on error. 4 | # Checks the API to see if data is still being pushed through. 5 | 6 | FIRST_CHAR=$(curl -s 127.0.0.1:3000/api/map | head -c1) 7 | TIMESTAMP=$(curl -s 127.0.0.1:3000/api/map | head -c23 | tail -c10) 8 | CURR_TIMESTAMP=$(date +%s) 9 | DIFF_TIMESTAMP=$(($CURR_TIMESTAMP-$TIMESTAMP)) 10 | 11 | if [[ "$FIRST_CHAR" != "{" ]] || [[ $DIFF_TIMESTAMP -gt 60 ]]; then 12 | docker compose -f /opt/blah2/docker-compose.yml down 13 | kill -9 $(pgrep -f "sdrplay_apiService") 14 | systemctl restart sdrplay.service 15 | docker compose -f /opt/blah2/docker-compose.yml up -d 16 | echo "Successfully restarted blah2" 17 | fi 18 | -------------------------------------------------------------------------------- /src/capture/hackrf/README.md: -------------------------------------------------------------------------------- 1 | # HackRF setup for blah2 2 | 3 | This requires 2 HackRF units with a shared clock signal and a shared hardware trigger. 4 | 5 | ## Instructions 6 | 7 | - The 2 HackRF boards should be wired as per the diagram below. 8 | - Note the [official guide](https://hackrf.readthedocs.io/en/latest/hardware_triggering.html) on setting up the shared clock and hardware trigger. 9 | - Install the HackRF package on the host using `sudo apt install hackrf` to access HackRF tools. 10 | - Run `hackrf_info` to get HackRF serial numbers. 11 | - Edit the `config/config-hackrf.yml` file to add serial numbers and parameters. 12 | - Update the `docker-compose.yml` file to use the above config file in both locations. 13 | 14 | ![HackRF blah2 wiring diagram](./hackrf-blah2.png "HackRF") 15 | -------------------------------------------------------------------------------- /lib/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blah2", 3 | "version": "1.0.0", 4 | "dependencies": [ 5 | { "name": "catch2", "version>=": "3.4.0" }, 6 | { "name": "rapidjson", "version>=": "1.1.0" }, 7 | { "name": "asio", "version>=": "1.28.0" }, 8 | { "name": "cpp-httplib", "version>=": "0.12.2" }, 9 | { "name": "armadillo", "version>=": "12.0.1" }, 10 | { "name": "ryml", "version>=": "0.5.0" } 11 | ], 12 | "builtin-baseline": "c8696863d371ab7f46e213d8f5ca923c4aef2a00", 13 | "overrides": [ 14 | { "name": "catch2", "version": "3.4.0" }, 15 | { "name": "rapidjson", "version": "1.1.0" }, 16 | { "name": "asio", "version": "1.28.0" }, 17 | { "name": "cpp-httplib", "version": "0.12.2" }, 18 | { "name": "armadillo", "version": "12.0.1" }, 19 | { "name": "ryml", "version": "0.5.0" } 20 | ] 21 | } -------------------------------------------------------------------------------- /html/lib/blah2.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f78c5881; 3 | } 4 | .menu { 5 | font-family: 'Helvetica', sans-serif !important; 6 | font-size: 1.5rem; 7 | font-weight: bold; 8 | } 9 | .title { 10 | font-family: 'Helvetica', sans-serif !important; 11 | font-size: 1.5rem; 12 | font-weight: bold; 13 | text-decoration: underline; 14 | } 15 | .label { 16 | font-family: 'Helvetica', sans-serif !important; 17 | font-size: 1rem; 18 | } 19 | @media (min-width: 768px) { 20 | .menu { 21 | font-family: 'Helvetica', sans-serif !important; 22 | font-size: 2rem; 23 | } 24 | .title { 25 | font-family: 'Helvetica', sans-serif !important; 26 | font-size: 2.5rem; 27 | font-weight: bold; 28 | text-decoration: underline; 29 | } 30 | .label { 31 | font-family: 'Helvetica', sans-serif !important; 32 | font-size: 1.5rem; 33 | } 34 | } 35 | .navbar-nav { 36 | flex-wrap: wrap; 37 | } 38 | div.plotly-notifier { 39 | visibility: hidden; 40 | } 41 | -------------------------------------------------------------------------------- /src/process/meta/HammingNumber.h: -------------------------------------------------------------------------------- 1 | /// @file HammingNumber.h 2 | /// @class HammingNumber 3 | /// @brief Hamming number generator 4 | /// @author Nigel Galloway 5 | /// @cite https://rosettacode.org/wiki/Hamming_numbers 6 | 7 | #ifndef HAMMING_GENERATOR_H 8 | #define HAMMING_GENERATOR_H 9 | 10 | #include 11 | #include 12 | 13 | class HammingNumber 14 | { 15 | 16 | private: 17 | std::vector H, hp, hv, x; 18 | 19 | public: 20 | bool operator!=(const HammingNumber &other) const; 21 | HammingNumber begin() const; 22 | HammingNumber end() const; 23 | unsigned int operator*() const; 24 | HammingNumber(const std::vector &pfs); 25 | const HammingNumber &operator++(); 26 | 27 | }; 28 | 29 | /// @brief Calculate the next 5-smooth Hamming Number larger than value 30 | /// @param value Value to round 31 | /// @return value rounded to Hamming number 32 | uint32_t next_hamming(uint32_t value); 33 | 34 | #endif -------------------------------------------------------------------------------- /host/README.md: -------------------------------------------------------------------------------- 1 | # blah2 Host 2 | 3 | A reverse proxy to host blah2 on the internet. 4 | 5 | ## Description 6 | 7 | This can be used to forward the radar to the internet. The radar front-end is at `localhost:49152`, and the API is located at `localhost:3000/api/`. This reverse proxy forwards `localhost:49152` to a port of your choosing, and forwards `localhost:3000` to the same port at `/api/`. 8 | 9 | ## Usage 10 | 11 | **docker-compose.yml** 12 | 13 | - Change the output port from `8080` as desired in `docker-compose.yml`. 14 | - The environment variable `VIRTUAL_HOST=domain.tld` is only applicable if also using [jwilder/nginx](https://github.com/nginx-proxy/nginx-proxy). 15 | - If using [jwilder/nginx](https://github.com/nginx-proxy/nginx-proxy), ensure both containers are on the same network with `sudo docker network create `. Otherwise the network configuration can be deleted. 16 | 17 | **nginx.conf** 18 | 19 | - Edit the `backend_ip` and `domain_name` variables in `nginx.conf`. -------------------------------------------------------------------------------- /html/display/spectrum/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /html/display/map/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/process/utility/Socket.cpp: -------------------------------------------------------------------------------- 1 | #include "Socket.h" 2 | #include 3 | 4 | asio::io_context Socket::io_context; 5 | const uint32_t Socket::MTU = 1024; 6 | 7 | Socket::Socket(const std::string& ip, uint16_t port) 8 | : endpoint(asio::ip::address::from_string(ip), port), socket(io_context) { 9 | try { 10 | socket.connect(endpoint); 11 | } catch (const std::exception& e) { 12 | std::cerr << "Error connecting to endpoint: " << e.what() << std::endl; 13 | throw; 14 | } 15 | } 16 | 17 | Socket::~Socket() 18 | { 19 | } 20 | 21 | void Socket::sendData(const std::string& data) { 22 | asio::error_code err; 23 | 24 | for (std::size_t i = 0; i < (data.size() + MTU - 1) / MTU; ++i) { 25 | std::string subdata = data.substr(i * MTU, MTU); 26 | socket.write_some(asio::buffer(subdata, subdata.size()), err); 27 | 28 | if (err) { 29 | std::cerr << "Error sending data: " << err.message() << std::endl; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /html/display/maxhold/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | 4 | environment { 5 | GHCR_REGISTRY = "ghcr.io" 6 | GHCR_TOKEN = credentials('ghcr-login') 7 | BLAH2_NAME = "30hours/blah2" 8 | } 9 | 10 | stages { 11 | stage('Checkout') { 12 | steps { 13 | checkout scm 14 | } 15 | } 16 | stage('Build') { 17 | steps { 18 | echo 'Building the project' 19 | sh 'docker build -t $BLAH2_NAME .' 20 | } 21 | } 22 | stage('Test') { 23 | steps { 24 | echo 'Running tests' 25 | } 26 | } 27 | stage('Push') { 28 | steps { 29 | sh 'echo $GHCR_TOKEN_PSW | docker login ghcr.io -u $GHCR_TOKEN_USR --password-stdin' 30 | sh 'docker tag $BLAH2_NAME ghcr.io/$BLAH2_NAME' 31 | sh 'docker push ghcr.io/$BLAH2_NAME' 32 | sh 'docker logout' 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /html/display/timing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | blah2: 5 | external: true 6 | 7 | services: 8 | 9 | blah2: 10 | restart: always 11 | build: . 12 | image: blah2 13 | tty: true 14 | depends_on: 15 | - blah2_api 16 | volumes: 17 | - ./config:/blah2/config 18 | - /opt/blah2/save:/blah2/save 19 | - /dev/shm:/dev/shm:rw 20 | - /dev/usb:/dev/usb:rw 21 | network_mode: host 22 | privileged: true 23 | command: "/blah2/bin/blah2 -c config/config.yml" 24 | container_name: blah2 25 | 26 | blah2_web: 27 | restart: always 28 | image: httpd:2.4 29 | ports: 30 | - 49152:80 31 | volumes: 32 | - ./html:/usr/local/apache2/htdocs 33 | networks: 34 | - blah2 35 | container_name: blah2-web 36 | 37 | blah2_api: 38 | restart: always 39 | build: ./api 40 | image: blah2_api 41 | volumes: 42 | - ./config:/usr/src/app/config 43 | network_mode: host 44 | command: "node server.js /usr/src/app/config/config.yml" 45 | container_name: blah2-api 46 | -------------------------------------------------------------------------------- /html/display/detection/delay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /html/display/detection/doppler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /html/display/detection/delay-doppler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 30hours 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/process/meta/HammingNumber.cpp: -------------------------------------------------------------------------------- 1 | #include "HammingNumber.h" 2 | 3 | bool HammingNumber::operator!=(const HammingNumber &other) const 4 | { 5 | return true; 6 | } 7 | 8 | HammingNumber HammingNumber::begin() const 9 | { 10 | return *this; 11 | } 12 | 13 | HammingNumber HammingNumber::end() const 14 | { 15 | return *this; 16 | } 17 | 18 | unsigned int HammingNumber::operator*() const 19 | { 20 | return x.back(); 21 | } 22 | 23 | HammingNumber::HammingNumber(const std::vector &pfs) 24 | : H(pfs), hp(pfs.size(), 0), hv({pfs}), x({1}) {} 25 | 26 | const HammingNumber &HammingNumber::operator++() 27 | { 28 | for (std::vector::size_type i = 0; i < H.size(); i++) 29 | for (; hv[i] <= x.back(); hv[i] = x[++hp[i]] * H[i]) 30 | ; 31 | x.push_back(hv[0]); 32 | for (std::vector::size_type i = 1; i < H.size(); i++) 33 | if (hv[i] < x.back()) 34 | x.back() = hv[i]; 35 | return *this; 36 | } 37 | 38 | uint32_t next_hamming(uint32_t value) 39 | { 40 | for (auto i : HammingNumber({2, 3, 5})) 41 | { 42 | if (i > value) 43 | { 44 | return i; 45 | } 46 | } 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/capture/rspduo/README.md: -------------------------------------------------------------------------------- 1 | ## Config File 2 | 3 | The source of truth for the config file parameters is the [SDRplay API Specification](https://www.sdrplay.com/docs/SDRplay_API_Specification_v3.pdf). 4 | 5 | Here is a list of available config parameters: 6 | 7 | - **agcSetPoint** in dBfs has a default value of -60 dBfs, and can be set between -72 dBfs and 0 dBfs. 8 | 9 | - **bandwidthNumber** in Hz has a default value of 50 Hz, and can be 0, 5, 50 or 100 Hz. This is the number of times per second the AGC makes gain adjustments by changing the gain reduction value. If setting to 0 then the AGC is effectively disabled. 10 | 11 | - **gainReduction** in dB has a default value of 40 dB, and can be set between 20 and 59. This is the initial value the gain reduction is set to, before the AGC changes this parameter to approach the AGC set point. 12 | 13 | - **lnaState** has a default value of 4, must be between 1 and 9. A larger number means a larger gain reduction (attenuation). Maximum gain at LNA state 1 and minimum gain at LNA state 9. 14 | 15 | - **dabNotch** is a bool, true turns on the DAB band notch filter (default false). 16 | 17 | - **rfNotch** is a bool, true turns on the AM/FM band notch filter (default false). 18 | 19 | -------------------------------------------------------------------------------- /docker/Dockerfile-kraken: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 as blah2_env 2 | LABEL maintainer="30hours " 3 | LABEL org.opencontainers.image.source https://github.com/30hours/blah2 4 | 5 | WORKDIR /blah2 6 | ADD lib lib 7 | RUN apt-get update && apt-get install -y software-properties-common \ 8 | && apt-get update \ 9 | && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \ 10 | g++ make cmake git curl zip unzip doxygen graphviz \ 11 | libfftw3-dev pkg-config gfortran \ 12 | libusb-dev libusb-1.0.0-dev \ 13 | && apt-get autoremove -y \ 14 | && apt-get clean -y \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | # install RTL-SDR API 18 | RUN git clone https://github.com/krakenrf/librtlsdr /opt/librtlsdr \ 19 | && cd /opt/librtlsdr && mkdir build && cd build \ 20 | && cmake ../ -DINSTALL_UDEV_RULES=ON -DDETACH_KERNEL_DRIVER=ON && make && make install && ldconfig 21 | 22 | FROM blah2_env as blah2 23 | LABEL maintainer="30hours " 24 | 25 | ADD src src 26 | ADD test test 27 | ADD CMakeLists.txt CMakePresets.json Doxyfile /blah2/ 28 | RUN mkdir -p build && cd build && cmake -S . --preset prod-release \ 29 | -DCMAKE_PREFIX_PATH=$(echo /blah2/lib/vcpkg_installed/*/share) .. \ 30 | && cd prod-release && make 31 | RUN chmod +x bin/blah2 32 | -------------------------------------------------------------------------------- /src/process/utility/Socket.h: -------------------------------------------------------------------------------- 1 | /// @file Socket.h 2 | /// @class Socket 3 | /// @brief A class to implement network socket functionality. 4 | /// @details Used to pass radar data from app to the API. 5 | /// @author 30hours 6 | 7 | #ifndef SOCKET_H 8 | #define SOCKET_H 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | class Socket 15 | { 16 | private: 17 | /// @brief Common io_context for all socket objects. 18 | static asio::io_context io_context; 19 | 20 | /// @brief Common MTU size for all socket objects. 21 | static const uint32_t MTU; 22 | 23 | /// @brief The ASIO endpoint. 24 | asio::ip::tcp::endpoint endpoint; 25 | 26 | /// @brief The ASIO socket. 27 | asio::ip::tcp::socket socket; 28 | 29 | public: 30 | /// @brief Constructor for Socket. 31 | /// @param ip IP address of data destination. 32 | /// @param port Port of data destination. 33 | /// @return The object. 34 | Socket(const std::string& ip, uint16_t port); 35 | 36 | /// @brief Destructor. 37 | /// @return Void. 38 | ~Socket(); 39 | 40 | /// @brief Helper function to send data in chunks. 41 | /// @param data String of complete data to send. 42 | /// @return Void. 43 | void sendData(const std::string& data); 44 | 45 | }; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /src/process/detection/Centroid.h: -------------------------------------------------------------------------------- 1 | /// @file Centroid.h 2 | /// @class Centroid 3 | /// @brief A class to remove duplicate target detections. 4 | /// @details If detection SNR is larger than neighbours, then remove. 5 | /// @author 30hours 6 | 7 | #ifndef CENTROID_H 8 | #define CENTROID_H 9 | 10 | #include "data/Detection.h" 11 | #include 12 | #include 13 | 14 | class Centroid 15 | { 16 | private: 17 | /// @brief Number of delay bins to check. 18 | uint16_t nDelay; 19 | 20 | /// @brief Number of Doppler bins to check. 21 | uint16_t nDoppler; 22 | 23 | /// @brief Doppler resolution to convert Hz to bins (Hz). 24 | double resolutionDoppler; 25 | 26 | /// @brief Pointer to detection data to store result. 27 | Detection *detection; 28 | 29 | public: 30 | /// @brief Constructor. 31 | /// @param nDelay Number of delay bins to check. 32 | /// @param nDoppler Number of Doppler bins to check. 33 | /// @param resolutionDoppler Doppler resolution to convert Hz to bins (Hz). 34 | /// @return The object. 35 | Centroid(uint16_t nDelay, uint16_t nDoppler, double resolutionDoppler); 36 | 37 | /// @brief Destructor. 38 | /// @return Void. 39 | ~Centroid(); 40 | 41 | /// @brief Implement the 1D CFAR detector. 42 | /// @param x Detections from the 1D CFAR detector. 43 | /// @return Centroided detections. 44 | std::unique_ptr process(Detection *x); 45 | }; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /src/process/detection/Interpolate.h: -------------------------------------------------------------------------------- 1 | /// @file Interpolate.h 2 | /// @class Interpolate 3 | /// @brief A class to interpolate detection data using a quadratic curve. 4 | /// @details Interpolate in delay and Doppler. If 2 points either side have a higher SNR, then remove detection. 5 | /// References: 6 | /// - https://ccrma.stanford.edu/~jos/sasp/Quadratic_Interpolation_Spectral_Peaks.html 7 | /// - Fundamentals of Signal Processing (2nd), Richards, Section 5.3.6 8 | /// @author 30hours 9 | /// @todo Should I remove the detection pointer? Also on Centroid. 10 | 11 | #ifndef INTERPOLATE_H 12 | #define INTERPOLATE_H 13 | 14 | #include "data/Map.h" 15 | #include "data/Detection.h" 16 | 17 | #include 18 | 19 | class Interpolate 20 | { 21 | private: 22 | /// @brief True if interpolating over delay. 23 | bool doDelay; 24 | 25 | /// @brief True if interpolating over Doppler. 26 | bool doDoppler; 27 | 28 | /// @brief Pointer to detection data to store result. 29 | Detection *detection; 30 | 31 | public: 32 | /// @brief Constructor. 33 | /// @param doDelay True if interpolating over delay. 34 | /// @param doDoppler True if interpolating over Doppler. 35 | /// @return The object. 36 | Interpolate(bool doDelay, bool doDoppler); 37 | 38 | /// @brief Destructor. 39 | /// @return Void. 40 | ~Interpolate(); 41 | 42 | /// @brief Implement the 1D CFAR detector. 43 | /// @param x Detections from the 1D CFAR detector. 44 | /// @return Interpolated detections. 45 | std::unique_ptr process(Detection *x, Map> *y); 46 | }; 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/data/meta/Timing.h: -------------------------------------------------------------------------------- 1 | /// @file Timing.h 2 | /// @class Timing 3 | /// @brief A class to store timing statistics. 4 | /// @author 30hours 5 | 6 | #ifndef TIMING_H 7 | #define TIMING_H 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | class Timing 14 | { 15 | private: 16 | /// @brief Start time (POSIX ms). 17 | uint64_t tStart; 18 | 19 | /// @brief Current time (POSIX ms). 20 | uint64_t tNow; 21 | 22 | /// @brief Number of CPI's. 23 | uint64_t n; 24 | 25 | /// @brief Time since first CPI (ms). 26 | uint64_t uptime; 27 | 28 | /// @brief Time differences (ms). 29 | std::vector time; 30 | 31 | /// @brief Names of time differences. 32 | std::vector name; 33 | 34 | public: 35 | /// @brief Constructor. 36 | /// @param tStart Start time (POSIX ms). 37 | /// @return The object. 38 | Timing(uint64_t tStart); 39 | 40 | /// @brief Update the time differences and names. 41 | /// @param tNow Current time (POSIX ms). 42 | /// @param time Vector of time differences (ms). 43 | /// @param name Vector of time difference names. 44 | /// @return Void. 45 | void update(uint64_t tNow, std::vector time, std::vector name); 46 | 47 | /// @brief Generate JSON of the map and metadata. 48 | /// @return JSON string. 49 | std::string to_json(); 50 | 51 | /// @brief Append the map to a save file. 52 | /// @param json JSON string of map and metadata. 53 | /// @param path Path of file to save. 54 | /// @return True is save is successful. 55 | bool save(std::string json, std::string path); 56 | }; 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /src/process/spectrum/SpectrumAnalyser.h: -------------------------------------------------------------------------------- 1 | /// @file SpectrumAnalyser.h 2 | /// @class SpectrumAnalyser 3 | /// @brief A class to generate frequency spectrum plots. 4 | /// @details Simple decimate and FFT on CPI IQ data for frequency spectrum. 5 | /// @author 30hours 6 | /// @todo Potentially create k spectrum plots from sub-CPIs. 7 | /// @todo FFT with HammingNumber class. 8 | 9 | #ifndef SPECTRUMANALYSER_H 10 | #define SPECTRUMANALYSER_H 11 | 12 | #include "data/IqData.h" 13 | #include 14 | #include 15 | 16 | class SpectrumAnalyser 17 | { 18 | private: 19 | /// @brief Number of samples on input. 20 | uint32_t n; 21 | 22 | /// @brief Minimum bandwidth of frequency bin (Hz). 23 | double bandwidth; 24 | 25 | /// @brief Decimation factor. 26 | uint32_t decimation; 27 | 28 | /// @brief FFTW plans for ambiguity processing. 29 | fftw_plan fftX; 30 | 31 | /// @brief FFTW storage for ambiguity processing. 32 | std::complex *dataX; 33 | 34 | /// @brief Number of samples to perform FFT. 35 | uint32_t nfft; 36 | 37 | /// @brief Number of samples in decimated spectrum. 38 | uint32_t nSpectrum; 39 | 40 | /// @brief Resolution of spectrum (Hz). 41 | double resolution; 42 | 43 | public: 44 | /// @brief Constructor. 45 | /// @param n Number of samples on input. 46 | /// @param bandwidth Minimum bandwidth of frequency bin (Hz). 47 | /// @return The object. 48 | SpectrumAnalyser(uint32_t n, double bandwidth); 49 | 50 | /// @brief Destructor. 51 | /// @return Void. 52 | ~SpectrumAnalyser(); 53 | 54 | /// @brief Process spectrum data. 55 | /// @param x Reference samples. 56 | /// @return Void. 57 | void process(IqData *x); 58 | }; 59 | 60 | #endif -------------------------------------------------------------------------------- /config/config-usrp.yml: -------------------------------------------------------------------------------- 1 | capture: 2 | fs: 2000000 3 | fc: 204640000 4 | device: 5 | type: "Usrp" 6 | address: "localhost" 7 | subdev: "A:A A:B" 8 | antenna: ["RX2", "RX2"] 9 | gain: [20.0, 20.0] 10 | replay: 11 | state: false 12 | loop: true 13 | file: '/opt/blah2/replay/file.rspduo' 14 | 15 | process: 16 | data: 17 | cpi: 0.5 18 | buffer: 1.5 19 | overlap: 0 20 | ambiguity: 21 | delayMin: -10 22 | delayMax: 400 23 | dopplerMin: -200 24 | dopplerMax: 200 25 | clutter: 26 | enable: true 27 | delayMin: -10 28 | delayMax: 400 29 | detection: 30 | enable: true 31 | pfa: 0.00001 32 | nGuard: 2 33 | nTrain: 6 34 | minDelay: 5 35 | minDoppler: 15 36 | nCentroid: 6 37 | tracker: 38 | enable: true 39 | initiate: 40 | M: 3 41 | N: 5 42 | maxAcc: 10 43 | delete: 10 44 | smooth: "none" 45 | 46 | network: 47 | ip: 0.0.0.0 48 | ports: 49 | api: 3000 50 | map: 3001 51 | detection: 3002 52 | track: 3003 53 | timestamp: 4000 54 | timing: 4001 55 | iqdata: 4002 56 | config: 4003 57 | 58 | truth: 59 | adsb: 60 | enabled: false 61 | tar1090: 'adsb.30hours.dev' 62 | adsb2dd: 'adsb2dd.30hours.dev' 63 | ais: 64 | enabled: false 65 | ip: 0.0.0.0 66 | port: 30001 67 | 68 | location: 69 | rx: 70 | latitude: -34.9286 71 | longitude: 138.5999 72 | altitude: 50 73 | name: "Adelaide" 74 | tx: 75 | latitude: -34.9810 76 | longitude: 138.7081 77 | altitude: 750 78 | name: "Mount Lofty" 79 | 80 | save: 81 | iq: true 82 | map: false 83 | detection: false 84 | timing: false 85 | path: "/blah2/save/" 86 | -------------------------------------------------------------------------------- /config/config-kraken.yml: -------------------------------------------------------------------------------- 1 | capture: 2 | fs: 2000000 3 | fc: 204640000 4 | device: 5 | type: "Kraken" 6 | gain: [15.0, 15.0] 7 | array: 8 | x: [0, 0] 9 | y: [0, 0] 10 | z: [0, 0] 11 | boresight: 0.0 12 | replay: 13 | state: false 14 | loop: true 15 | file: '/opt/blah2/replay/file.kraken' 16 | 17 | process: 18 | data: 19 | cpi: 0.5 20 | buffer: 1.5 21 | overlap: 0 22 | ambiguity: 23 | delayMin: -10 24 | delayMax: 400 25 | dopplerMin: -200 26 | dopplerMax: 200 27 | clutter: 28 | enable: true 29 | delayMin: -10 30 | delayMax: 400 31 | detection: 32 | enable: true 33 | pfa: 0.00001 34 | nGuard: 2 35 | nTrain: 6 36 | minDelay: 5 37 | minDoppler: 15 38 | nCentroid: 6 39 | tracker: 40 | enable: true 41 | initiate: 42 | M: 3 43 | N: 5 44 | maxAcc: 10 45 | delete: 10 46 | smooth: "none" 47 | 48 | network: 49 | ip: 0.0.0.0 50 | ports: 51 | api: 3000 52 | map: 3001 53 | detection: 3002 54 | track: 3003 55 | timestamp: 4000 56 | timing: 4001 57 | iqdata: 4002 58 | config: 4003 59 | 60 | truth: 61 | adsb: 62 | enabled: false 63 | tar1090: 'adsb.30hours.dev' 64 | adsb2dd: 'adsb2dd.30hours.dev' 65 | ais: 66 | enabled: false 67 | ip: 0.0.0.0 68 | port: 30001 69 | 70 | location: 71 | rx: 72 | latitude: -34.9286 73 | longitude: 138.5999 74 | altitude: 50 75 | name: "Adelaide" 76 | tx: 77 | latitude: -34.9810 78 | longitude: 138.7081 79 | altitude: 750 80 | name: "Mount Lofty" 81 | 82 | save: 83 | iq: true 84 | map: false 85 | detection: false 86 | timing: false 87 | path: "/blah2/save/" 88 | -------------------------------------------------------------------------------- /config/radar4.yml: -------------------------------------------------------------------------------- 1 | capture: 2 | fs: 2000000 3 | fc: 204640000 4 | device: 5 | type: "RspDuo" 6 | agcSetPoint: -20 7 | bandwidthNumber: 5 8 | gainReduction: 59 9 | lnaState: 1 10 | dabNotch: false 11 | rfNotch: false 12 | replay: 13 | state: false 14 | loop: true 15 | file: '/opt/blah2/replay/file.rspduo' 16 | 17 | process: 18 | data: 19 | cpi: 0.5 20 | buffer: 1.5 21 | overlap: 0 22 | ambiguity: 23 | delayMin: -10 24 | delayMax: 400 25 | dopplerMin: -200 26 | dopplerMax: 200 27 | clutter: 28 | enable: true 29 | delayMin: -10 30 | delayMax: 400 31 | detection: 32 | enable: true 33 | pfa: 0.00001 34 | nGuard: 2 35 | nTrain: 6 36 | minDelay: 5 37 | minDoppler: 15 38 | nCentroid: 6 39 | tracker: 40 | enable: true 41 | initiate: 42 | M: 3 43 | N: 5 44 | maxAcc: 10 45 | delete: 10 46 | smooth: "none" 47 | 48 | network: 49 | ip: 0.0.0.0 50 | ports: 51 | api: 3000 52 | map: 3001 53 | detection: 3002 54 | track: 3003 55 | timestamp: 4000 56 | timing: 4001 57 | iqdata: 4002 58 | config: 4003 59 | 60 | truth: 61 | adsb: 62 | enabled: false 63 | tar1090: 'adsb.30hours.dev' 64 | adsb2dd: 'adsb2dd.30hours.dev' 65 | ais: 66 | enabled: false 67 | ip: 0.0.0.0 68 | port: 30001 69 | 70 | location: 71 | rx: 72 | latitude: -34.9286 73 | longitude: 138.5999 74 | altitude: 50 75 | name: "Adelaide" 76 | tx: 77 | latitude: -34.9810 78 | longitude: 138.7081 79 | altitude: 750 80 | name: "Mount Lofty" 81 | 82 | save: 83 | iq: true 84 | map: false 85 | detection: false 86 | timing: false 87 | path: "/blah2/save/" 88 | -------------------------------------------------------------------------------- /api/stash/timing.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | var nCpi = 20; 4 | var ts = ''; 5 | var cpi = []; 6 | var output = {}; 7 | const options_timestamp = { 8 | host: '127.0.0.1', 9 | path: '/api/timestamp', 10 | port: 3000 11 | }; 12 | const options_iqdata = { 13 | host: '127.0.0.1', 14 | path: '/api/timing', 15 | port: 3000 16 | }; 17 | 18 | function update_data(callback) { 19 | http.get(options_timestamp, function (res) { 20 | res.setEncoding('utf8'); 21 | res.on('data', function (body) { 22 | if (ts != body) { 23 | ts = body; 24 | http.get(options_iqdata, function (res) { 25 | let body_map = ''; 26 | res.setEncoding('utf8'); 27 | res.on('data', (chunk) => { 28 | body_map += chunk; 29 | }); 30 | res.on('end', () => { 31 | try { 32 | cpi = JSON.parse(body_map); 33 | keys = Object.keys(cpi); 34 | keys = keys.filter(item => item !== "uptime"); 35 | keys = keys.filter(item => item !== "nCpi"); 36 | for (i = 0; i < keys.length; i++) { 37 | if (!(keys[i] in output)) { 38 | output[keys[i]] = []; 39 | } 40 | output[keys[i]].push(cpi[keys[i]]); 41 | if (output[keys[i]].length > nCpi) { 42 | output[keys[i]].shift(); 43 | } 44 | } 45 | } catch (e) { 46 | console.error(e.message); 47 | } 48 | }); 49 | }); 50 | } 51 | }); 52 | }); 53 | } 54 | 55 | setInterval(update_data, 100); 56 | 57 | function get_data() { 58 | return output; 59 | } 60 | 61 | module.exports.get_data_timing = get_data; 62 | -------------------------------------------------------------------------------- /host/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | default_type application/octet-stream; 13 | include /etc/nginx/mime.types; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | include /etc/nginx/conf.d/*.conf; 25 | 26 | server { 27 | listen 80 default_server; 28 | listen [::]:80 default_server; 29 | include /etc/nginx/mime.types; 30 | 31 | set $backend_ip localhost; 32 | set $domain_name localhost; 33 | 34 | proxy_pass_header Content-Type; 35 | proxy_set_header X-Real-IP $domain_name; 36 | proxy_set_header X-Forwarded-For $domain_name; 37 | proxy_set_header Host $domain_name; 38 | proxy_http_version 1.1; 39 | proxy_set_header Connection ""; 40 | proxy_connect_timeout 1; 41 | proxy_next_upstream error timeout http_500 http_502 http_503 http_504 http_404; 42 | proxy_intercept_errors on; 43 | 44 | location / { 45 | proxy_pass http://$backend_ip:49152; 46 | } 47 | 48 | location ~ ^/(maxhold|api|stash)/(.*) { 49 | proxy_pass http://$backend_ip:3000/$1/$2; 50 | } 51 | 52 | error_page 501 502 503 504 =200 /error.html; 53 | location = /error.html { 54 | root /usr/local/apache2/htdocs; 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/config-hackrf.yml: -------------------------------------------------------------------------------- 1 | capture: 2 | fs: 2000000 3 | fc: 204640000 4 | device: 5 | type: "HackRF" 6 | serial: 7 | - "REFERENCE_DEVICE_SERIAL_NUMBER" 8 | - "SURVILLANCE_DEVICE_SERIAL_NUMBER" 9 | gain_lna: [32, 32] 10 | gain_vga: [30, 30] 11 | amp_enable: [false, false] 12 | replay: 13 | state: false 14 | loop: true 15 | file: '/opt/blah2/replay/file.hackrf' 16 | 17 | process: 18 | data: 19 | cpi: 0.5 20 | buffer: 1.5 21 | overlap: 0 22 | ambiguity: 23 | delayMin: -10 24 | delayMax: 400 25 | dopplerMin: -200 26 | dopplerMax: 200 27 | clutter: 28 | enable: true 29 | delayMin: -10 30 | delayMax: 400 31 | detection: 32 | enable: true 33 | pfa: 0.00001 34 | nGuard: 2 35 | nTrain: 6 36 | minDelay: 5 37 | minDoppler: 15 38 | nCentroid: 6 39 | tracker: 40 | enable: true 41 | initiate: 42 | M: 3 43 | N: 5 44 | maxAcc: 10 45 | delete: 10 46 | smooth: "none" 47 | 48 | network: 49 | ip: 0.0.0.0 50 | ports: 51 | api: 3000 52 | map: 3001 53 | detection: 3002 54 | track: 3003 55 | timestamp: 4000 56 | timing: 4001 57 | iqdata: 4002 58 | config: 4003 59 | 60 | truth: 61 | adsb: 62 | enabled: false 63 | tar1090: 'adsb.30hours.dev' 64 | adsb2dd: 'adsb2dd.30hours.dev' 65 | ais: 66 | enabled: false 67 | ip: 0.0.0.0 68 | port: 30001 69 | 70 | location: 71 | rx: 72 | latitude: -34.9286 73 | longitude: 138.5999 74 | altitude: 50 75 | name: "Adelaide" 76 | tx: 77 | latitude: -34.9810 78 | longitude: 138.7081 79 | altitude: 750 80 | name: "Mount Lofty" 81 | 82 | save: 83 | iq: true 84 | map: false 85 | detection: false 86 | timing: false 87 | path: "/blah2/save/" 88 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO: update devcontainer with vcpkg manifest 2 | 3 | # ubuntu-22.04 by default 4 | ARG VARIANT="jammy" 5 | FROM mcr.microsoft.com/vscode/devcontainers/cpp:0-${VARIANT} 6 | LABEL maintainer="30hours " 7 | 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | WORKDIR /blah2 11 | ADD lib lib 12 | RUN apt-get update && apt-get install -y software-properties-common \ 13 | && apt-add-repository ppa:ettusresearch/uhd \ 14 | && apt-get update \ 15 | && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \ 16 | g++ make cmake git curl zip unzip doxygen graphviz \ 17 | libfftw3-dev pkg-config gfortran \ 18 | libuhd-dev=4.6.0.0-0ubuntu1~jammy1 \ 19 | uhd-host=4.6.0.0-0ubuntu1~jammy1 \ 20 | && apt-get autoremove -y \ 21 | && apt-get clean -y \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | # install dependencies from vcpkg 25 | RUN git clone https://github.com/microsoft/vcpkg /opt/vcpkg \ 26 | && /opt/vcpkg/bootstrap-vcpkg.sh 27 | ENV PATH="/opt/vcpkg:${PATH}" VCPKG_ROOT=/opt/vcpkg 28 | RUN cd /blah2/lib && vcpkg integrate install \ 29 | && vcpkg install --clean-after-build 30 | 31 | # install SDRplay API 32 | RUN chmod +x /blah2/lib/sdrplay-3.14.0/SDRplay_RSP_API-Linux-3.14.0.run \ 33 | && /blah2/lib/sdrplay-3.14.0/SDRplay_RSP_API-Linux-3.14.0.run --tar -xvf -C /blah2/lib/sdrplay-3.14.0 \ 34 | && cp /blah2/lib/sdrplay-3.14.0/x86_64/libsdrplay_api.so.3.14 /usr/local/lib/libsdrplay_api.so \ 35 | && cp /blah2/lib/sdrplay-3.14.0/x86_64/libsdrplay_api.so.3.14 /usr/local/lib/libsdrplay_api.so.3.14 \ 36 | && cp /blah2/lib/sdrplay-3.14.0/inc/* /usr/local/include \ 37 | && chmod 644 /usr/local/lib/libsdrplay_api.so /usr/local/lib/libsdrplay_api.so.3.14 \ 38 | && ldconfig 39 | 40 | # install UHD API 41 | RUN uhd_images_downloader 42 | 43 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | capture: 2 | fs: 2000000 3 | fc: 204640000 4 | device: 5 | type: "RspDuo" 6 | agcSetPoint: -20 7 | # 0 to disable AGC 8 | #bandwidthNumber: 0 9 | bandwidthNumber: 5 10 | gainReduction: [50, 45] 11 | lnaState: 1 12 | dabNotch: false 13 | rfNotch: false 14 | replay: 15 | state: false 16 | loop: true 17 | file: '/opt/blah2/replay/file.rspduo' 18 | 19 | process: 20 | data: 21 | cpi: 0.75 22 | buffer: 2 23 | overlap: 0 24 | ambiguity: 25 | delayMin: -10 26 | delayMax: 400 27 | dopplerMin: -200 28 | dopplerMax: 200 29 | clutter: 30 | enable: true 31 | delayMin: -10 32 | delayMax: 400 33 | detection: 34 | enable: true 35 | pfa: 0.00001 36 | nGuard: 2 37 | nTrain: 6 38 | minDelay: 5 39 | minDoppler: 15 40 | nCentroid: 6 41 | tracker: 42 | enable: false 43 | initiate: 44 | M: 3 45 | N: 5 46 | maxAcc: 10 47 | delete: 10 48 | smooth: "none" 49 | 50 | network: 51 | ip: 0.0.0.0 52 | ports: 53 | api: 3000 54 | map: 3001 55 | detection: 3002 56 | track: 3003 57 | timestamp: 4000 58 | timing: 4001 59 | iqdata: 4002 60 | config: 4003 61 | 62 | truth: 63 | adsb: 64 | enabled: true 65 | tar1090: 'adsb.30hours.dev' 66 | adsb2dd: 'adsb2dd.30hours.dev' 67 | ais: 68 | enabled: false 69 | ip: 0.0.0.0 70 | port: 30001 71 | 72 | location: 73 | rx: 74 | latitude: -34.9286 75 | longitude: 138.5999 76 | altitude: 50 77 | name: "Adelaide" 78 | tx: 79 | latitude: -34.9810 80 | longitude: 138.7081 81 | altitude: 750 82 | name: "Mount Lofty" 83 | 84 | save: 85 | iq: true 86 | map: false 87 | detection: false 88 | timing: false 89 | path: "/blah2/save/" 90 | -------------------------------------------------------------------------------- /src/capture/Source.cpp: -------------------------------------------------------------------------------- 1 | #include "Source.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | Source::Source() 11 | { 12 | } 13 | 14 | // constructor 15 | Source::Source(std::string _type, uint32_t _fc, uint32_t _fs, 16 | std::string _path, bool *_saveIq) 17 | { 18 | type = _type; 19 | fc = _fc; 20 | fs = _fs; 21 | path = _path; 22 | saveIq = _saveIq; 23 | } 24 | 25 | std::string Source::open_file() 26 | { 27 | // get string of timestamp in YYYYmmdd-HHMMSS 28 | auto currentTime = std::chrono::system_clock::to_time_t( 29 | std::chrono::system_clock::now()); 30 | std::tm* timeInfo = std::localtime(¤tTime); 31 | std::ostringstream oss; 32 | oss << std::put_time(timeInfo, "%Y%m%d-%H%M%S"); 33 | std::string timestamp = oss.str(); 34 | 35 | // create file path 36 | std::string typeLower = type; 37 | std::transform(typeLower.begin(), typeLower.end(), 38 | typeLower.begin(), ::tolower); 39 | std::string file = path + timestamp + "." + typeLower + ".iq"; 40 | 41 | saveIqFile.open(file, std::ios::binary); 42 | 43 | if (!saveIqFile.is_open()) 44 | { 45 | std::cerr << "Error: Can not open file: " << file << std::endl; 46 | exit(1); 47 | } 48 | std::cout << "Ready to record IQ to file: " << file << std::endl; 49 | 50 | return file; 51 | } 52 | 53 | void Source::close_file() 54 | { 55 | if (!saveIqFile.is_open()) 56 | { 57 | saveIqFile.close(); 58 | } 59 | 60 | // switch member with blank file stream 61 | std::ofstream blankFile; 62 | std::swap(saveIqFile, blankFile); 63 | } 64 | 65 | void Source::kill() 66 | { 67 | if (type == "RspDuo") 68 | { 69 | stop(); 70 | } else if (type == "HackRF") 71 | { 72 | stop(); 73 | } 74 | exit(0); 75 | } 76 | -------------------------------------------------------------------------------- /src/process/spectrum/SpectrumAnalyser.cpp: -------------------------------------------------------------------------------- 1 | #include "SpectrumAnalyser.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // constructor 9 | SpectrumAnalyser::SpectrumAnalyser(uint32_t _n, double _bandwidth) 10 | { 11 | // input 12 | n = _n; 13 | bandwidth = _bandwidth; 14 | 15 | // compute nfft 16 | decimation = n/bandwidth; 17 | nSpectrum = n/decimation; 18 | nfft = nSpectrum*decimation; 19 | 20 | // compute FFTW plans in constructor 21 | dataX = new std::complex[nfft]; 22 | fftX = fftw_plan_dft_1d(nfft, reinterpret_cast(dataX), 23 | reinterpret_cast(dataX), FFTW_FORWARD, FFTW_ESTIMATE); 24 | } 25 | 26 | SpectrumAnalyser::~SpectrumAnalyser() 27 | { 28 | fftw_destroy_plan(fftX); 29 | } 30 | 31 | void SpectrumAnalyser::process(IqData *x) 32 | { 33 | // load data and FFT 34 | uint32_t i; 35 | std::deque> data = x->get_data(); 36 | for (i = 0; i < nfft; i++) 37 | { 38 | dataX[i] = data[i]; 39 | } 40 | fftw_execute(fftX); 41 | 42 | // fftshift 43 | std::vector> fftshift; 44 | for (i = 0; i < nfft; i++) 45 | { 46 | fftshift.push_back(dataX[(i + int(nfft / 2) + 1) % nfft]); 47 | } 48 | 49 | // decimate 50 | std::vector> spectrum; 51 | for (i = 0; i < nfft; i+=decimation) 52 | { 53 | spectrum.push_back(fftshift[i]); 54 | } 55 | x->update_spectrum(spectrum); 56 | 57 | // update frequency 58 | std::vector frequency; 59 | double offset = 0; 60 | if (decimation % 2 == 0) 61 | { 62 | offset = bandwidth/2; 63 | } 64 | for (i = -nSpectrum/2; i < nSpectrum/2; i++) 65 | { 66 | frequency.push_back(((i*bandwidth)+offset+204640000)/1000); 67 | } 68 | x->update_frequency(frequency); 69 | 70 | return; 71 | } 72 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # blah2 Test 2 | 3 | A set of tests are provided for development/debugging. 4 | 5 | ## Framework 6 | 7 | The test framework is [catch2](https://github.com/catchorg/Catch2). 8 | 9 | ## Types 10 | 11 | The test files are split across directories defined by the type of test. 12 | 13 | - **Unit tests** will test the class in isolation. The directory structure mirrors *src*. 14 | - **Functional tests** will test that expected outputs are achieved from defined inputs. An example would be checking the program turns a specific IQ data set to a specific delay-Doppler map. This test category will rely on golden data. 15 | - **Comparison tests** will compare different methods of performing the same task. An example would be comparing 2 methods of clutter filtering. Metrics to be compared may include time and performance. Note there is no specific pass/fail criteria for comparison tests - this is purely for information. A comparison test will pass if executed successfully. Any comparison testing on input parameters for a single class will be handled in the unit test. 16 | 17 | ## Usage 18 | 19 | All tests are compiled when building, however tests be run manually. 20 | 21 | - Run a single unit test for "TestClass". 22 | 23 | ``` 24 | sudo docker exec -it blah2 /blah2/bin/test/unit/testClass 25 | ``` 26 | 27 | - Run a single functional test for "TestFunctional". 28 | 29 | ``` 30 | sudo docker exec -it blah2 /blah2/bin/test/functional/testFunctional 31 | ``` 32 | 33 | - Run a single comparison test for "TestComparison". 34 | 35 | ``` 36 | sudo docker exec -it blah2 /blah2/bin/test/comparison/testComparison 37 | ``` 38 | 39 | - *TODO:* Run all test cases. 40 | 41 | ``` 42 | sudo docker exec -it blah2 /blah2/bin/test/runall.sh 43 | sudo docker exec -it blah2 /blah2/bin/test/unit/runall.sh 44 | sudo docker exec -it blah2 /blah2/bin/test/functional/runall.sh 45 | sudo docker exec -it blah2 /blah2/bin/test/comparison/runall.sh 46 | ``` -------------------------------------------------------------------------------- /api/stash/maxhold.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | var nCpi = 20; 4 | var map = []; 5 | var maxhold = ''; 6 | var timestamp = ''; 7 | const options_timestamp = { 8 | host: '127.0.0.1', 9 | path: '/api/timestamp', 10 | port: 3000 11 | }; 12 | const options_map = { 13 | host: '127.0.0.1', 14 | path: '/api/map', 15 | port: 3000 16 | }; 17 | 18 | function process(matrixArray) { 19 | 20 | const result = []; 21 | 22 | for (let i = 0; i < matrixArray[0].length; i++) { 23 | const row = []; 24 | for (let j = 0; j < matrixArray[0][0].length; j++) { 25 | let maxVal = matrixArray[0][i][j]; 26 | for (let k = 1; k < matrixArray.length; k++) { 27 | maxVal = Math.max(maxVal, matrixArray[k][i][j]); 28 | } 29 | row.push(maxVal); 30 | } 31 | result.push(row); 32 | } 33 | 34 | return result; 35 | } 36 | 37 | function update_data() { 38 | 39 | // check if timestamp is updated 40 | http.get(options_timestamp, function(res) { 41 | res.setEncoding('utf8'); 42 | res.on('data', function (body) { 43 | if (timestamp != body) 44 | { 45 | timestamp = body; 46 | http.get(options_map, function(res) { 47 | let body_map = ''; 48 | res.setEncoding('utf8'); 49 | res.on('data', (chunk) => { 50 | body_map += chunk; 51 | }); 52 | res.on('end', () => { 53 | try { 54 | maxhold = JSON.parse(body_map); 55 | map.push(maxhold.data); 56 | if (map.length > nCpi) { 57 | map.shift(); 58 | } 59 | maxhold.data = process(map); 60 | } catch (e) { 61 | console.error(e.message); 62 | } 63 | }); 64 | }); 65 | } 66 | }); 67 | }); 68 | 69 | }; 70 | 71 | setInterval(update_data, 100); 72 | 73 | function get_data() { 74 | return maxhold; 75 | }; 76 | 77 | module.exports.get_data_map = get_data; -------------------------------------------------------------------------------- /src/process/detection/CfarDetector1D.h: -------------------------------------------------------------------------------- 1 | /// @file CfarDetector1D.h 2 | /// @class CfarDetector1D 3 | /// @brief A class to implement a 1D CFAR detector. 4 | /// @details Converts an AmbiguityMap to DetectionData. 1D CFAR operates across delay, to minimise detections from the zero-Doppler line. 5 | /// @author 30hours 6 | /// @todo Actually implement the min delay and Doppler. 7 | 8 | #ifndef CFARDETECTOR1D_H 9 | #define CFARDETECTOR1D_H 10 | 11 | #include "data/Map.h" 12 | #include "data/Detection.h" 13 | #include 14 | #include 15 | #include 16 | 17 | class CfarDetector1D 18 | { 19 | private: 20 | /// @brief Probability of false alarm, numeric in [0,1] 21 | double pfa; 22 | 23 | /// @brief Number of single-sided guard cells. 24 | int8_t nGuard; 25 | 26 | /// @brief Number of single-sided training cells. 27 | int8_t nTrain; 28 | 29 | /// @brief Minimum delay to process detections (bins). 30 | int8_t minDelay; 31 | 32 | /// @brief Minimum absolute Doppler to process detections (Hz). 33 | double minDoppler; 34 | 35 | /// @brief Pointer to detection data to store result. 36 | Detection *detection; 37 | 38 | public: 39 | /// @brief Constructor. 40 | /// @param pfa Probability of false alarm, numeric in [0,1]. 41 | /// @param nGuard Number of single-sided guard cells. 42 | /// @param nTrain Number of single-sided training cells. 43 | /// @param minDelay Minimum delay to process detections (bins). 44 | /// @param minDoppler Minimum absolute Doppler to process detections (Hz). 45 | /// @return The object. 46 | CfarDetector1D(double pfa, int8_t nGuard, int8_t nTrain, int8_t minDelay, double minDoppler); 47 | 48 | /// @brief Destructor. 49 | /// @return Void. 50 | ~CfarDetector1D(); 51 | 52 | /// @brief Implement the 1D CFAR detector. 53 | /// @param x Ambiguity map data of IQ samples. 54 | /// @return Detections from the 1D CFAR detector. 55 | std::unique_ptr process(Map> *x); 56 | }; 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /api/stash/iqdata.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | var nCpi = 20; 4 | var spectrum = []; 5 | frequency = []; 6 | var timestamp = []; 7 | var ts = ''; 8 | var output = []; 9 | const options_timestamp = { 10 | host: '127.0.0.1', 11 | path: '/api/timestamp', 12 | port: 3000 13 | }; 14 | const options_iqdata = { 15 | host: '127.0.0.1', 16 | path: '/api/iqdata', 17 | port: 3000 18 | }; 19 | 20 | function update_data() { 21 | 22 | // check if timestamp is updated 23 | http.get(options_timestamp, function(res) { 24 | res.setEncoding('utf8'); 25 | res.on('data', function (body) { 26 | if (ts != body) 27 | { 28 | ts = body; 29 | http.get(options_iqdata, function(res) { 30 | let body_map = ''; 31 | res.setEncoding('utf8'); 32 | res.on('data', (chunk) => { 33 | body_map += chunk; 34 | }); 35 | res.on('end', () => { 36 | try { 37 | output = JSON.parse(body_map); 38 | // spectrum 39 | spectrum.push(output.spectrum); 40 | if (spectrum.length > nCpi) { 41 | spectrum.shift(); 42 | } 43 | output.spectrum = spectrum; 44 | // frequency 45 | frequency.push(output.frequency); 46 | if (frequency.length > nCpi) { 47 | frequency.shift(); 48 | } 49 | output.frequency = frequency; 50 | // timestamp 51 | timestamp.push(output.timestamp); 52 | if (timestamp.length > nCpi) { 53 | timestamp.shift(); 54 | } 55 | output.timestamp = timestamp; 56 | } catch (e) { 57 | console.error(e.message); 58 | } 59 | }); 60 | }); 61 | } 62 | }); 63 | }); 64 | 65 | }; 66 | 67 | setInterval(update_data, 100); 68 | 69 | function get_data() { 70 | return output; 71 | }; 72 | 73 | module.exports.get_data_iqdata = get_data; -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 |
23 |
24 | 34 | 35 |
36 |

37 |

38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/process/detection/Centroid.cpp: -------------------------------------------------------------------------------- 1 | #include "Centroid.h" 2 | #include 3 | #include 4 | #include 5 | 6 | // constructor 7 | Centroid::Centroid(uint16_t _nDelay, uint16_t _nDoppler, double _resolutionDoppler) 8 | { 9 | // input 10 | nDelay = _nDelay; 11 | nDoppler = _nDoppler; 12 | resolutionDoppler = _resolutionDoppler; 13 | } 14 | 15 | Centroid::~Centroid() 16 | { 17 | } 18 | 19 | std::unique_ptr Centroid::process(Detection *x) 20 | { 21 | // store detections temporarily 22 | std::vector delay, doppler, snr; 23 | delay = x->get_delay(); 24 | doppler = x->get_doppler(); 25 | snr = x->get_snr(); 26 | 27 | // centroid data 28 | uint16_t delayMin, delayMax; 29 | double dopplerMin, dopplerMax; 30 | bool isCentroid; 31 | std::vector delay2, doppler2, snr2; 32 | 33 | // loop over every detection 34 | for (size_t i = 0; i < snr.size(); i++) 35 | { 36 | delayMin = (int)(delay[i]) - nDelay; 37 | delayMax = (int)(delay[i]) + nDelay; 38 | dopplerMin = doppler[i] - (nDoppler * resolutionDoppler); 39 | dopplerMax = doppler[i] + (nDoppler * resolutionDoppler); 40 | isCentroid = true; 41 | 42 | // find detections to keep 43 | for (size_t j = 0; j < snr.size(); j++) 44 | { 45 | // skip same detection 46 | if (j == i) 47 | { 48 | continue; 49 | } 50 | // search detections close by 51 | if (delay[j] > delayMin && delay[j] < delayMax && 52 | doppler[j] > dopplerMin && doppler[j] < dopplerMax) 53 | { 54 | // remove if SNR is lower 55 | if (snr[i] < snr[j]) 56 | { 57 | isCentroid = false; 58 | break; 59 | } 60 | } 61 | } 62 | // store centroided detections 63 | if (isCentroid) 64 | { 65 | delay2.push_back(delay[i]); 66 | doppler2.push_back(doppler[i]); 67 | snr2.push_back(snr[i]); 68 | } 69 | } 70 | 71 | // create detection 72 | return std::make_unique(delay2, doppler2, snr2); 73 | } 74 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose 3 | { 4 | "name": "blah2 dev", 5 | 6 | // Update the 'dockerComposeFile' list if you have more compose files or use different names. 7 | // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. 8 | "dockerComposeFile": [ 9 | "docker-compose.yml" 10 | ], 11 | 12 | // The 'service' property is the name of the service for the container that VS Code should 13 | // use. Update this value and .devcontainer/docker-compose.yml to the real service name. 14 | "service": "blah2-dev", 15 | 16 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 17 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 18 | "workspaceFolder": "/workspace", 19 | 20 | // Features to add to the dev container. More info: https://containers.dev/features. 21 | // "features": {}, 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Uncomment the next line if you want start specific services in your Docker Compose config. 27 | // "runServices": [], 28 | 29 | // Uncomment the next line if you want to keep your containers running after VS Code shuts down. 30 | // "shutdownAction": "none", 31 | 32 | // Uncomment the next line to run commands after the container is created. 33 | // "postCreateCommand": "cat /etc/os-release", 34 | 35 | // Configure tool-specific properties. 36 | // "customizations": {}, 37 | "customizations": { 38 | "vscode": { 39 | // Add the IDs of extensions you want installed when the container is created. 40 | "extensions": [ 41 | "ms-vscode.cpptools-extension-pack", 42 | ], 43 | } 44 | }, 45 | 46 | // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. 47 | "remoteUser": "vscode" 48 | } 49 | -------------------------------------------------------------------------------- /docker/Dockerfile-uhd: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 as blah2_env 2 | LABEL maintainer="30hours " 3 | LABEL org.opencontainers.image.source https://github.com/30hours/blah2 4 | 5 | WORKDIR /blah2 6 | ADD lib lib 7 | RUN apt-get update && apt-get install -y software-properties-common \ 8 | && apt-add-repository ppa:ettusresearch/uhd \ 9 | && apt-get update \ 10 | && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \ 11 | g++ make cmake git curl zip unzip doxygen graphviz \ 12 | libfftw3-dev pkg-config gfortran \ 13 | ccache dpdk libboost-all-dev libdpdk-dev \ 14 | libudev-dev libusb-1.0.0-dev python3-dev \ 15 | python3-docutils python3-mako python3-numpy \ 16 | python3-pip python3-requests \ 17 | && apt-get autoremove -y \ 18 | && apt-get clean -y \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # install UHD from source 22 | ENV UHD_TAG=v4.6.0.0 23 | RUN git clone https://github.com/EttusResearch/uhd.git /uhd \ 24 | && cd /uhd/ && git checkout $UHD_TAG \ 25 | && mkdir -p /uhd/host/build \ 26 | && cd /uhd/host/build \ 27 | && cmake .. -DENABLE_PYTHON3=ON -DUHD_RELEASE_MODE=release -DCMAKE_INSTALL_PREFIX=/usr \ 28 | && make -j $(echo nproc) \ 29 | && make test \ 30 | && make install 31 | 32 | # install dependencies from vcpkg 33 | ENV VCPKG_ROOT=/opt/vcpkg 34 | RUN export PATH="/opt/vcpkg:${PATH}" \ 35 | && git clone https://github.com/microsoft/vcpkg /opt/vcpkg \ 36 | && if [ "$(uname -m)" = "aarch64" ]; then export VCPKG_FORCE_SYSTEM_BINARIES=1; fi \ 37 | && /opt/vcpkg/bootstrap-vcpkg.sh -disableMetrics \ 38 | && cd /blah2/lib && vcpkg integrate install \ 39 | && vcpkg install --clean-after-build 40 | 41 | # install UHD API 42 | RUN uhd_images_downloader 43 | 44 | FROM blah2_env as blah2 45 | LABEL maintainer="30hours " 46 | 47 | ADD src src 48 | ADD test test 49 | ADD CMakeLists.txt CMakePresets.json Doxyfile /blah2/ 50 | RUN mkdir -p build && cd build && cmake -S . --preset prod-release \ 51 | -DCMAKE_PREFIX_PATH=$(echo /blah2/lib/vcpkg_installed/*/share) .. \ 52 | && cd prod-release && make 53 | RUN chmod +x bin/blah2 54 | -------------------------------------------------------------------------------- /src/data/Detection.h: -------------------------------------------------------------------------------- 1 | /// @file Detection.h 2 | /// @class Detection 3 | /// @brief A class to store detection data. 4 | /// @author 30hours 5 | 6 | #ifndef DETECTION_H 7 | #define DETECTION_H 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | class Detection 14 | { 15 | private: 16 | /// @brief Detections in delay (bins). 17 | std::vector delay; 18 | 19 | /// @brief Detections in Doppler (Hz). 20 | std::vector doppler; 21 | 22 | /// @brief Detections in SNR. 23 | std::vector snr; 24 | 25 | public: 26 | /// @brief Constructor. 27 | /// @param delay Detections in delay (bins). 28 | /// @param doppler Detections in Doppler (Hz). 29 | /// @return The object. 30 | Detection(std::vector delay, std::vector doppler, std::vector snr); 31 | 32 | /// @brief Constructor for single detection. 33 | /// @param delay Detection in delay (bins). 34 | /// @param doppler Detection in Doppler (Hz). 35 | /// @return The object. 36 | Detection(double delay, double doppler, double snr); 37 | 38 | /// @brief Get detections in delay. 39 | /// @return Detections in delay (bins). 40 | std::vector get_delay(); 41 | 42 | /// @brief Get detections in Doppler. 43 | /// @return Detections in Doppler (Hz). 44 | std::vector get_doppler(); 45 | 46 | /// @brief Detections in SNR. 47 | /// @return Detections in SNR. 48 | std::vector get_snr(); 49 | 50 | /// @brief Get number of detections. 51 | /// @return Number of detections 52 | size_t get_nDetections(); 53 | 54 | /// @brief Generate JSON of the detections and metadata. 55 | /// @param timestamp Current time (POSIX ms). 56 | /// @return JSON string. 57 | std::string to_json(uint64_t timestamp); 58 | 59 | /// @brief Update JSON to convert delay bins to km. 60 | /// @param json Input JSON string with delay field. 61 | /// @param fs Sampling frequency (Hz). 62 | /// @return JSON string. 63 | std::string delay_bin_to_km(std::string json, uint32_t fs); 64 | 65 | /// @brief Append the detections to a save file. 66 | /// @param json JSON string of detections and metadata. 67 | /// @param path Path of file to save. 68 | /// @return True is save is successful. 69 | bool save(std::string json, std::string path); 70 | }; 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /src/capture/usrp/Usrp.h: -------------------------------------------------------------------------------- 1 | /// @file Usrp.h 2 | /// @class Usrp 3 | /// @brief A class to capture data on the Ettus Research USRP. 4 | /// @details Uses the UHD C API to extract samples into the processing chain. 5 | /// 6 | /// Should work on all USRP models. 7 | /// Networked models require an IP address in the config file. 8 | /// Requires a USB 3.0 cable for higher data rates. 9 | /// 10 | /// @author 30hours 11 | /// @todo Add replay to Usrp. 12 | /// @todo Fix single overflow per CPI. 13 | /// @todo Fix occasional timeout ERROR_CODE_TIMEOUT. 14 | 15 | #ifndef USRP_H 16 | #define USRP_H 17 | 18 | #include "capture/Source.h" 19 | #include "data/IqData.h" 20 | 21 | #include 22 | #include 23 | 24 | class Usrp : public Source 25 | { 26 | private: 27 | 28 | /// @brief Address of USRP device. 29 | /// @details "localhost" if USB, else IP address. 30 | std::string address; 31 | 32 | /// @brief Subdevice string for USRP. 33 | /// @details See docs. 34 | std::string subdev; 35 | 36 | /// @brief Antenna string for each channel. 37 | std::vector antenna; 38 | 39 | /// @brief USRP gain for each channel. 40 | std::vector gain; 41 | 42 | public: 43 | 44 | /// @brief Constructor. 45 | /// @param fc Center frequency (Hz). 46 | /// @param path Path to save IQ data. 47 | /// @return The object. 48 | Usrp(std::string type, uint32_t fc, uint32_t fs, std::string path, 49 | bool *saveIq, std::string address, std::string subdev, 50 | std::vector antenna, std::vector gain); 51 | 52 | /// @brief Implement capture function on USRP. 53 | /// @param buffer1 Pointer to reference buffer. 54 | /// @param buffer2 Pointer to surveillance buffer. 55 | /// @return Void. 56 | void process(IqData *buffer1, IqData *buffer2); 57 | 58 | /// @brief Call methods to start capture. 59 | /// @return Void. 60 | void start(); 61 | 62 | /// @brief Call methods to gracefully stop capture. 63 | /// @return Void. 64 | void stop(); 65 | 66 | /// @brief Implement replay function on RSPduo. 67 | /// @param buffer1 Pointer to reference buffer. 68 | /// @param buffer2 Pointer to surveillance buffer. 69 | /// @param file Path to file to replay data from. 70 | /// @param loop True if samples should loop at EOF. 71 | /// @return Void. 72 | void replay(IqData *buffer1, IqData *buffer2, std::string file, bool loop); 73 | 74 | }; 75 | 76 | #endif -------------------------------------------------------------------------------- /api/stash/detection.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | var time = 300; 4 | var map = []; 5 | var timestamp = []; 6 | var delay = []; 7 | var doppler = []; 8 | var detection = ''; 9 | var ts = ''; 10 | var output = []; 11 | const options_timestamp = { 12 | host: '127.0.0.1', 13 | path: '/api/timestamp', 14 | port: 3000 15 | }; 16 | const options_detection = { 17 | host: '127.0.0.1', 18 | path: '/api/detection', 19 | port: 3000 20 | }; 21 | 22 | function update_data() { 23 | 24 | // check if timestamp is updated 25 | http.get(options_timestamp, function(res) { 26 | res.setEncoding('utf8'); 27 | res.on('data', function (body) { 28 | if (ts != body) 29 | { 30 | ts = body; 31 | http.get(options_detection, function(res) { 32 | let body_map = ''; 33 | res.setEncoding('utf8'); 34 | res.on('data', (chunk) => { 35 | body_map += chunk; 36 | }); 37 | res.on('end', () => { 38 | try { 39 | detection = JSON.parse(body_map); 40 | map.push(detection); 41 | for (i = 0; i < map.length; i++) 42 | { 43 | if ((ts - map[i].timestamp)/1000 > time) 44 | { 45 | map.shift(); 46 | } 47 | else 48 | { 49 | break; 50 | } 51 | } 52 | delay = []; 53 | doppler = []; 54 | timestamp = []; 55 | snr = []; 56 | for (var i = 0; i < map.length; i++) 57 | { 58 | for (var j = 0; j < map[i].delay.length; j++) 59 | { 60 | delay.push(map[i].delay[j]); 61 | doppler.push(map[i].doppler[j]); 62 | snr.push(map[i].snr[j]); 63 | timestamp.push(map[i].timestamp); 64 | } 65 | } 66 | output = { 67 | timestamp: timestamp, 68 | delay: delay, 69 | doppler: doppler, 70 | snr: snr 71 | }; 72 | } catch (e) { 73 | console.error(e.message); 74 | } 75 | }); 76 | }); 77 | } 78 | }); 79 | }); 80 | 81 | }; 82 | 83 | setInterval(update_data, 100); 84 | 85 | function get_data() { 86 | return output; 87 | }; 88 | 89 | module.exports.get_data_detection = get_data; -------------------------------------------------------------------------------- /src/capture/Capture.h: -------------------------------------------------------------------------------- 1 | /// @file Capture.h 2 | /// @class Capture 3 | /// @brief A class for a generic IQ capture device. 4 | /// @author 30hours 5 | 6 | #ifndef CAPTURE_H 7 | #define CAPTURE_H 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include // optional header, provided for std:: interop 14 | #include // needed for the examples below 15 | 16 | #include "data/IqData.h" 17 | #include "capture/Source.h" 18 | 19 | class Capture 20 | { 21 | private: 22 | /// @brief The valid capture devices. 23 | static const std::string VALID_TYPE[4]; 24 | 25 | /// @brief The capture device type. 26 | std::string type; 27 | 28 | /// @brief True if IQ data to be saved. 29 | bool saveIq; 30 | 31 | /// @brief True if file replay is enabled. 32 | bool replay; 33 | 34 | /// @brief True if replay file should loop when complete. 35 | bool loop; 36 | 37 | /// @brief Absolute path of file to replay. 38 | std::string file; 39 | 40 | public: 41 | 42 | /// @brief Sampling frequency (Hz). 43 | uint32_t fs; 44 | 45 | /// @brief Center frequency (Hz). 46 | uint32_t fc; 47 | 48 | /// @brief Absolute path to IQ save location. 49 | std::string path; 50 | 51 | /// @brief Pointer to capture device. 52 | std::unique_ptr device; 53 | 54 | /// @brief Constructor. 55 | /// @param type The capture device type. 56 | /// @param fs Sampling frequency (Hz). 57 | /// @param fc Center frequency (Hz). 58 | /// @param path Absolute path to IQ save location. 59 | /// @return The object. 60 | Capture(std::string type, uint32_t fs, uint32_t fc, std::string path); 61 | 62 | /// @brief Implement the capture process. 63 | /// @param buffer1 Buffer for reference samples. 64 | /// @param buffer2 Buffer for surveillance samples. 65 | /// @param config Yaml config for device. 66 | /// @param ip_capture IP address of capture API. 67 | /// @param port_capture Port of capture API. 68 | /// @return Void. 69 | void process(IqData *buffer1, IqData *buffer2, c4::yml::NodeRef config, 70 | std::string ip_capture, uint16_t port_capture); 71 | 72 | std::unique_ptr factory_source(const std::string& type, 73 | c4::yml::NodeRef config); 74 | 75 | /// @brief Set parameters to enable file replay. 76 | /// @param loop True if replay file should loop when complete. 77 | /// @param file Absolute path of file to replay. 78 | /// @return Void. 79 | void set_replay(bool loop, std::string file); 80 | 81 | }; 82 | 83 | #endif -------------------------------------------------------------------------------- /src/capture/Source.h: -------------------------------------------------------------------------------- 1 | /// @file Source.h 2 | /// @class Source 3 | /// @brief An abstract class for capture sources. 4 | /// @author 30hours 5 | 6 | #ifndef SOURCE_H 7 | #define SOURCE_H 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "data/IqData.h" 14 | 15 | class Source 16 | { 17 | protected: 18 | 19 | /// @brief The capture device type. 20 | std::string type; 21 | 22 | /// @brief Center frequency (Hz). 23 | uint32_t fc; 24 | 25 | /// @brief Sampling frequency (Hz). 26 | uint32_t fs; 27 | 28 | /// @brief Absolute path to IQ save location. 29 | std::string path; 30 | 31 | /// @brief True if IQ data to be saved. 32 | bool *saveIq; 33 | 34 | /// @brief File stream to save IQ data. 35 | std::ofstream saveIqFile; 36 | 37 | public: 38 | 39 | Source(); 40 | 41 | /// @brief Constructor. 42 | /// @param type The capture device type. 43 | /// @param fs Sampling frequency (Hz). 44 | /// @param fc Center frequency (Hz). 45 | /// @param path Absolute path to IQ save location. 46 | /// @return The object. 47 | Source(std::string type, uint32_t fc, uint32_t fs, 48 | std::string path, bool *saveIq); 49 | 50 | /// @brief Implement the capture process. 51 | /// @param buffer1 Buffer for reference samples. 52 | /// @param buffer2 Buffer for surveillance samples. 53 | /// @return Void. 54 | virtual void process(IqData *buffer1, IqData *buffer2) = 0; 55 | 56 | /// @brief Call methods to start capture. 57 | /// @return Void. 58 | virtual void start() = 0; 59 | 60 | /// @brief Call methods to gracefully stop capture. 61 | /// @return Void. 62 | virtual void stop() = 0; 63 | 64 | /// @brief Implement replay function on RSPduo. 65 | /// @param buffer1 Pointer to reference buffer. 66 | /// @param buffer2 Pointer to surveillance buffer. 67 | /// @param file Path to file to replay data from. 68 | /// @param loop True if samples should loop at EOF. 69 | /// @return Void. 70 | virtual void replay(IqData *buffer1, IqData *buffer2, 71 | std::string file, bool loop) = 0; 72 | 73 | /// @brief Open a new file to record IQ. 74 | /// @details First creates a new file from current timestamp. 75 | /// Files are of format ..iq. 76 | /// @return String of full path to file. 77 | std::string open_file(); 78 | 79 | /// @brief Close IQ file gracefully. 80 | /// @return Void. 81 | void close_file(); 82 | 83 | /// @brief Graceful handler for SIGTERM. 84 | /// @return Void. 85 | void kill(); 86 | 87 | }; 88 | 89 | #endif -------------------------------------------------------------------------------- /src/process/clutter/WienerHopf.h: -------------------------------------------------------------------------------- 1 | /// @file WienerHopf.h 2 | /// @class WienerHopf 3 | /// @brief A class to implement a Wiener-Hopf clutter filter. 4 | /// @details Implements a Wiener-Hopf filter. 5 | /// Uses Cholesky decomposition to speed up matrix inversion, as the Toeplitz matrix is positive-definite and Hermitian. 6 | /// @author 30hours 7 | /// @todo Fix the segmentation fault from clutter filter numerical instability. 8 | 9 | #ifndef WIENERHOPF_H 10 | #define WIENERHOPF_H 11 | 12 | #include "data/IqData.h" 13 | #include 14 | #include 15 | #include 16 | 17 | class WienerHopf 18 | { 19 | private: 20 | /// @brief Minimum clutter filter delay (bins). 21 | int32_t delayMin; 22 | 23 | /// @brief Maximum clutter filter delay (bins). 24 | int32_t delayMax; 25 | 26 | /// @brief Number of bins (delayMax - delayMin + 1). 27 | uint32_t nBins; 28 | 29 | /// @brief Number of samples per CPI. 30 | uint32_t nSamples; 31 | 32 | /// @brief True if clutter filter processing is successful. 33 | bool success; 34 | 35 | /// @brief FFTW plans for clutter filter processing. 36 | /// @{ 37 | fftw_plan fftX, fftY, fftA, fftB, fftFiltX, fftFiltW, fftFilt; 38 | /// @} 39 | 40 | /// @brief FFTW storage for clutter filter processing. 41 | /// @{ 42 | std::complex *dataX, *dataY, *dataOutX, *dataOutY, *dataA, *dataB, *filtX, *filtW, *filt; 43 | /// @} 44 | 45 | /// @brief Deque storage for clutter filter processing. 46 | /// @{ 47 | std::deque> xData, yData; 48 | /// @} 49 | 50 | /// @brief Autocorrelation toeplitz matrix. 51 | arma::cx_mat A; 52 | 53 | /// @brief Autocorrelation vector. 54 | arma::cx_vec a; 55 | 56 | /// @brief Cross-correlation vector. 57 | arma::cx_vec b; 58 | 59 | /// @brief Weights vector. 60 | arma::cx_vec w; 61 | 62 | public: 63 | /// @brief Constructor. 64 | /// @param delayMin Minimum clutter filter delay (bins). 65 | /// @param delayMax Maximum clutter filter delay (bins). 66 | /// @param nSamples Number of samples per CPI. 67 | /// @return The object. 68 | WienerHopf(int32_t delayMin, int32_t delayMax, uint32_t nSamples); 69 | 70 | /// @brief Destructor. 71 | /// @return Void. 72 | ~WienerHopf(); 73 | 74 | /// @brief Implement the clutter filter. 75 | /// @param x Reference samples. 76 | /// @param y Surveillance samples. 77 | /// @return True if clutter filter successful. 78 | bool process(IqData *x, IqData *y); 79 | }; 80 | 81 | #endif -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 as blah2_env 2 | LABEL maintainer="30hours " 3 | LABEL org.opencontainers.image.source https://github.com/30hours/blah2 4 | 5 | WORKDIR /blah2 6 | ADD lib lib 7 | RUN apt-get update && apt-get install -y software-properties-common \ 8 | && apt-add-repository ppa:ettusresearch/uhd \ 9 | && apt-get update \ 10 | && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \ 11 | g++ make cmake git curl zip unzip doxygen graphviz \ 12 | libfftw3-dev pkg-config gfortran libhackrf-dev \ 13 | libuhd-dev=4.9.0.0-0ubuntu1~jammy2 \ 14 | uhd-host=4.9.0.0-0ubuntu1~jammy2 \ 15 | libusb-dev libusb-1.0.0-dev \ 16 | && apt-get autoremove -y \ 17 | && apt-get clean -y \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | # install dependencies from vcpkg 21 | ENV VCPKG_ROOT=/opt/vcpkg 22 | RUN export PATH="/opt/vcpkg:${PATH}" \ 23 | && git clone https://github.com/microsoft/vcpkg /opt/vcpkg \ 24 | && if [ "$(uname -m)" = "aarch64" ]; then export VCPKG_FORCE_SYSTEM_BINARIES=1; fi \ 25 | && /opt/vcpkg/bootstrap-vcpkg.sh -disableMetrics \ 26 | && cd /blah2/lib && vcpkg integrate install \ 27 | && vcpkg install --clean-after-build 28 | 29 | # install SDRplay API 30 | RUN export ARCH=$(uname -m) \ 31 | && if [ "$ARCH" = "x86_64" ]; then \ 32 | ARCH="amd64"; \ 33 | fi \ 34 | && export MAJVER="3.15" \ 35 | && export MINVER="2" \ 36 | && export VER=${MAJVER}.${MINVER} \ 37 | && cd /blah2/lib/sdrplay-${VER} \ 38 | && chmod +x SDRplay_RSP_API-Linux-${VER}.run \ 39 | && ./SDRplay_RSP_API-Linux-${MAJVER}.${MINVER}.run --tar -xvf -C /blah2/lib/sdrplay-${VER} \ 40 | && cp ${ARCH}/libsdrplay_api.so.${MAJVER} /usr/local/lib/libsdrplay_api.so \ 41 | && cp ${ARCH}/libsdrplay_api.so.${MAJVER} /usr/local/lib/libsdrplay_api.so.${MAJVER} \ 42 | && cp inc/* /usr/local/include \ 43 | && chmod 644 /usr/local/lib/libsdrplay_api.so /usr/local/lib/libsdrplay_api.so.${MAJVER} \ 44 | && ldconfig 45 | 46 | # install UHD API 47 | RUN uhd_images_downloader 48 | 49 | # install RTL-SDR API 50 | RUN git clone https://github.com/krakenrf/librtlsdr /opt/librtlsdr \ 51 | && cd /opt/librtlsdr && mkdir build && cd build \ 52 | && cmake ../ -DINSTALL_UDEV_RULES=ON -DDETACH_KERNEL_DRIVER=ON && make && make install && ldconfig 53 | 54 | FROM blah2_env as blah2 55 | LABEL maintainer="30hours " 56 | 57 | ADD src src 58 | ADD test test 59 | ADD CMakeLists.txt CMakePresets.json Doxyfile /blah2/ 60 | RUN mkdir -p build && cd build && cmake -S . --preset prod-release \ 61 | -DCMAKE_PREFIX_PATH=$(echo /blah2/lib/vcpkg_installed/*/share) .. \ 62 | && cd prod-release && make 63 | RUN chmod +x bin/blah2 64 | -------------------------------------------------------------------------------- /src/capture/hackrf/HackRf.h: -------------------------------------------------------------------------------- 1 | /// @file HackRf.h 2 | /// @class HackRf 3 | /// @brief A class to capture data on the HackRF. 4 | /// @author sdn-ninja 5 | /// @author 30hours 6 | /// @todo Replay functionality. 7 | 8 | #ifndef HACKRF_H 9 | #define HACKRF_H 10 | 11 | #include "capture/Source.h" 12 | #include "data/IqData.h" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | class HackRf : public Source 20 | { 21 | private: 22 | 23 | /// @brief Vector of serial numbers. 24 | /// @details Serial as given by hackrf_info. 25 | std::vector serial; 26 | 27 | /// @brief RX LNA (IF) gain, 0-40dB, 8dB steps. 28 | std::vector gainLna; 29 | 30 | /// @brief RX VGA (baseband) gain, 0-62dB, 2dB steps. 31 | std::vector gainVga; 32 | 33 | /// @brief Enable extra amplifier U13 on receive. 34 | std::vector ampEnable; 35 | 36 | /// @brief Check status of HackRF API returns. 37 | /// @param status Return code of API call. 38 | /// @param message Message if API call error. 39 | void check_status(uint8_t status, std::string message); 40 | 41 | protected: 42 | /// @brief Array of pointers to HackRF devices. 43 | hackrf_device* dev[2]; 44 | 45 | /// @brief Callback function for HackRF samples. 46 | /// @param transfer HackRF transfer object. 47 | /// @return Void. 48 | static int rx_callback(hackrf_transfer* transfer); 49 | 50 | public: 51 | 52 | /// @brief Constructor. 53 | /// @param fc Center frequency (Hz). 54 | /// @param path Path to save IQ data. 55 | /// @return The object. 56 | HackRf(std::string type, uint32_t fc, uint32_t fs, std::string path, 57 | bool *saveIq, std::vector serial, 58 | std::vector gainLna, std::vector gainVga, 59 | std::vector ampEnable); 60 | 61 | /// @brief Implement capture function on HackRF. 62 | /// @param buffer1 Pointer to reference buffer. 63 | /// @param buffer2 Pointer to surveillance buffer. 64 | /// @return Void. 65 | void process(IqData *buffer1, IqData *buffer2); 66 | 67 | /// @brief Call methods to start capture. 68 | /// @return Void. 69 | void start(); 70 | 71 | /// @brief Call methods to gracefully stop capture. 72 | /// @return Void. 73 | void stop(); 74 | 75 | /// @brief Implement replay function on HackRF. 76 | /// @param buffer1 Pointer to reference buffer. 77 | /// @param buffer2 Pointer to surveillance buffer. 78 | /// @param file Path to file to replay data from. 79 | /// @param loop True if samples should loop at EOF. 80 | /// @return Void. 81 | void replay(IqData *buffer1, IqData *buffer2, std::string file, bool loop); 82 | 83 | }; 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /test/unit/process/tracker/TestTracker.cpp: -------------------------------------------------------------------------------- 1 | /// @file TestTracker.cpp 2 | /// @brief Unit test for Tracker.cpp 3 | /// @author 30hours 4 | 5 | #include 6 | #include 7 | 8 | #include "data/Detection.h" 9 | #include "data/Track.h" 10 | #include "process/tracker/Tracker.h" 11 | #include "data/meta/Constants.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | /// @brief Test constructor. 19 | /// @details Check constructor parameters created correctly. 20 | TEST_CASE("Constructor", "[constructor]") 21 | { 22 | uint32_t m = 3; 23 | uint32_t n = 5; 24 | uint32_t nDelete = 5; 25 | double cpi = 1; 26 | double maxAccInit = 10; 27 | double fs = 2000000; 28 | double rangeRes = (double)Constants::c/fs; 29 | double fc = 204640000; 30 | double lambda = (double)Constants::c/fc; 31 | Tracker tracker = Tracker(m, n, nDelete, 32 | cpi, maxAccInit, rangeRes, lambda); 33 | } 34 | 35 | /// @brief Test process for an ACTIVE track. 36 | TEST_CASE("Process ACTIVE track constant acc", "[process]") 37 | { 38 | uint32_t m = 3; 39 | uint32_t n = 5; 40 | uint32_t nDelete = 5; 41 | double cpi = 1; 42 | double maxAccInit = 10; 43 | double fs = 2000000; 44 | double rangeRes = (double)Constants::c/fs; 45 | double fc = 204640000; 46 | double lambda = (double)Constants::c/fc; 47 | Tracker tracker = Tracker(m, n, nDelete, 48 | cpi, maxAccInit, rangeRes, lambda); 49 | 50 | 51 | // create detections with constant acc 5 Hz/s 52 | std::vector timestamp = {0,1,2,3,4,5,6,7,8,9,10}; 53 | std::vector delay = {10}; 54 | std::vector doppler = {-20,-15,-10,-5,0,5,10,15,20,25}; 55 | 56 | std::string state = "ACTIVE"; 57 | } 58 | 59 | /// @brief Test predict for kinematics equations. 60 | TEST_CASE("Test predict", "[predict]") 61 | { 62 | uint32_t m = 3; 63 | uint32_t n = 5; 64 | uint32_t nDelete = 5; 65 | double cpi = 1; 66 | double maxAccInit = 10; 67 | double fs = 2000000; 68 | double rangeRes = (double)Constants::c/fs; 69 | double fc = 204640000; 70 | double lambda = (double)Constants::c/fc; 71 | Tracker tracker = Tracker(m, n, nDelete, 72 | cpi, maxAccInit, rangeRes, lambda); 73 | 74 | Detection input = Detection(10, -20, 0); 75 | double acc = 5; 76 | double T = 1; 77 | Detection prediction = tracker.predict(input, acc, T); 78 | Detection prediction_truth = Detection(9.821, -15, 0); 79 | 80 | CHECK_THAT(prediction.get_delay().front(), 81 | Catch::Matchers::WithinAbs(prediction_truth.get_delay().front(), 0.01)); 82 | CHECK_THAT(prediction.get_doppler().front(), 83 | Catch::Matchers::WithinAbs(prediction_truth.get_doppler().front(), 0.01)); 84 | } -------------------------------------------------------------------------------- /src/data/IqData.h: -------------------------------------------------------------------------------- 1 | /// @file IqData.h 2 | /// @class IqData 3 | /// @brief A class to store IQ data. 4 | /// @details Implements a FIFO queue to store IQ samples. 5 | /// @author 30hours 6 | 7 | #ifndef IQDATA_H 8 | #define IQDATA_H 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class IqData 17 | { 18 | private: 19 | /// @brief Maximum number of samples. 20 | uint32_t n; 21 | 22 | /// @brief True if should not push to buffer (mutex). 23 | std::mutex mutex_lock; 24 | 25 | /// @brief Pointer to IQ data. 26 | std::deque> *data; 27 | 28 | /// @brief Minimum value. 29 | double min; 30 | 31 | /// @brief Maximum value. 32 | double max; 33 | 34 | /// @brief Mean value. 35 | double mean; 36 | 37 | /// @brief Spectrum vector. 38 | std::vector> spectrum; 39 | 40 | /// @brief Frequency vector (Hz). 41 | std::vector frequency; 42 | 43 | public: 44 | /// @brief Constructor. 45 | /// @param n Number of samples. 46 | /// @return The object. 47 | IqData(uint32_t n); 48 | 49 | /// @brief Getter for maximum number of samples. 50 | /// @return Maximum number of samples. 51 | uint32_t get_n(); 52 | 53 | /// @brief Getter for current data length. 54 | /// @return Number of samples currently in data. 55 | uint32_t get_length(); 56 | 57 | /// @brief Locker for mutex. 58 | /// @return Void. 59 | void lock(); 60 | 61 | /// @brief Unlocker for mutex. 62 | /// @return Void. 63 | void unlock(); 64 | 65 | /// @brief Getter for data. 66 | /// @return IQ data. 67 | std::deque> get_data(); 68 | 69 | /// @brief Push a sample to the queue. 70 | /// @param sample A single sample. 71 | /// @return Void. 72 | void push_back(std::complex sample); 73 | 74 | /// @brief Pop the front of the queue. 75 | /// @return Sample from the front of the queue. 76 | std::complex pop_front(); 77 | 78 | /// @brief Print to stdout (debug). 79 | /// @return Void. 80 | void print(); 81 | 82 | /// @brief Clear samples from the queue. 83 | /// @return Void. 84 | void clear(); 85 | 86 | /// @brief Update the time differences and names. 87 | /// @param spectrum Spectrum vector. 88 | /// @return Void. 89 | void update_spectrum(std::vector> spectrum); 90 | 91 | /// @brief Update the time differences and names. 92 | /// @param frequency Frequency vector. 93 | /// @return Void. 94 | void update_frequency(std::vector frequency); 95 | 96 | /// @brief Generate JSON of the signal and metadata. 97 | /// @param timestamp Current time (POSIX ms). 98 | /// @return JSON string. 99 | std::string to_json(uint64_t timestamp); 100 | }; 101 | 102 | #endif -------------------------------------------------------------------------------- /src/data/meta/Timing.cpp: -------------------------------------------------------------------------------- 1 | #include "Timing.h" 2 | #include 3 | #include 4 | 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/writer.h" 7 | #include "rapidjson/stringbuffer.h" 8 | #include "rapidjson/filewritestream.h" 9 | 10 | // constructor 11 | Timing::Timing(uint64_t _tStart) 12 | { 13 | tStart = _tStart; 14 | n = 0; 15 | } 16 | 17 | void Timing::update(uint64_t _tNow, std::vector _time, std::vector _name) 18 | { 19 | n = n + 1; 20 | tNow = _tNow; 21 | time = _time; 22 | name = _name; 23 | uptime = _tNow-tStart; 24 | } 25 | 26 | std::string Timing::to_json() 27 | { 28 | rapidjson::Document document; 29 | document.SetObject(); 30 | rapidjson::Document::AllocatorType &allocator = document.GetAllocator(); 31 | 32 | document.AddMember("timestamp", tNow, allocator); 33 | document.AddMember("nCpi", n, allocator); 34 | document.AddMember("uptime_s", uptime/1000.0, allocator); 35 | document.AddMember("uptime_days", uptime/1000.0/60/60/24, allocator); 36 | rapidjson::Value name_value; 37 | for (size_t i = 0; i < time.size(); i++) 38 | { 39 | name_value = rapidjson::StringRef(name[i].c_str()); 40 | document.AddMember(name_value, time[i], allocator); 41 | } 42 | 43 | rapidjson::StringBuffer strbuf; 44 | rapidjson::Writer writer(strbuf); 45 | writer.SetMaxDecimalPlaces(2); 46 | document.Accept(writer); 47 | 48 | return strbuf.GetString(); 49 | } 50 | 51 | bool Timing::save(std::string _json, std::string filename) 52 | { 53 | using namespace rapidjson; 54 | 55 | rapidjson::Document document; 56 | 57 | // create file if it doesn't exist 58 | if (FILE *fp = fopen(filename.c_str(), "r"); !fp) 59 | { 60 | if (fp = fopen(filename.c_str(), "w"); !fp) 61 | return false; 62 | fputs("[]", fp); 63 | fclose(fp); 64 | } 65 | 66 | // add the document to the file 67 | if (FILE *fp = fopen(filename.c_str(), "rb+"); fp) 68 | { 69 | // check if first is [ 70 | std::fseek(fp, 0, SEEK_SET); 71 | if (getc(fp) != '[') 72 | { 73 | std::fclose(fp); 74 | return false; 75 | } 76 | 77 | // is array empty? 78 | bool isEmpty = false; 79 | if (getc(fp) == ']') 80 | isEmpty = true; 81 | 82 | // check if last is ] 83 | std::fseek(fp, -1, SEEK_END); 84 | if (getc(fp) != ']') 85 | { 86 | std::fclose(fp); 87 | return false; 88 | } 89 | 90 | // replace ] by , 91 | fseek(fp, -1, SEEK_END); 92 | if (!isEmpty) 93 | fputc(',', fp); 94 | 95 | // add json element 96 | fwrite(_json.c_str(), sizeof(char), _json.length(), fp); 97 | 98 | // close the array 99 | std::fputc(']', fp); 100 | fclose(fp); 101 | return true; 102 | } 103 | return false; 104 | } 105 | -------------------------------------------------------------------------------- /src/process/detection/CfarDetector1D.cpp: -------------------------------------------------------------------------------- 1 | #include "CfarDetector1D.h" 2 | #include "data/Map.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // constructor 9 | CfarDetector1D::CfarDetector1D(double _pfa, int8_t _nGuard, int8_t _nTrain, int8_t _minDelay, double _minDoppler) 10 | { 11 | // input 12 | pfa = _pfa; 13 | nGuard = _nGuard; 14 | nTrain = _nTrain; 15 | minDelay = _minDelay; 16 | minDoppler = _minDoppler; 17 | } 18 | 19 | CfarDetector1D::~CfarDetector1D() 20 | { 21 | } 22 | 23 | std::unique_ptr CfarDetector1D::process(Map> *x) 24 | { 25 | int32_t nDelayBins = x->get_nCols(); 26 | int32_t nDopplerBins = x->get_nRows(); 27 | 28 | std::vector> mapRow; 29 | std::vector mapRowSquare, mapRowSnr; 30 | 31 | // store detections temporarily 32 | std::vector delay; 33 | std::vector doppler; 34 | std::vector snr; 35 | 36 | // loop over every cell 37 | for (int i = 0; i < nDopplerBins; i++) 38 | { 39 | // skip if less than min Doppler 40 | if (std::abs(x->doppler[i]) < minDoppler) 41 | { 42 | continue; 43 | } 44 | mapRow = x->get_row(i); 45 | for (int j = 0; j < nDelayBins; j++) 46 | { 47 | mapRowSquare.push_back((double) std::abs(mapRow[j]*mapRow[j])); 48 | mapRowSnr.push_back((double)10 * std::log10(std::abs(mapRow[j])) - x->noisePower); 49 | } 50 | for (int j = 0; j < nDelayBins; j++) 51 | { 52 | // skip if less than min delay 53 | if (x->delay[j] < minDelay) 54 | { 55 | continue; 56 | } 57 | // get train cell indices 58 | std::vector iTrain; 59 | for (int k = j-nGuard-nTrain; k < j-nGuard; k++) 60 | { 61 | if (k > 0 && k < nDelayBins) 62 | { 63 | iTrain.push_back(k); 64 | } 65 | } 66 | for (int k = j+nGuard+1; k < j+nGuard+nTrain+1; k++) 67 | { 68 | if (k >= 0 && k < nDelayBins) 69 | { 70 | iTrain.push_back(k); 71 | } 72 | } 73 | 74 | // compute threshold 75 | int nCells = iTrain.size(); 76 | double alpha = nCells * (pow(pfa, -1.0 / nCells) - 1); 77 | double trainNoise = 0.0; 78 | for (int k = 0; k < nCells; k++) 79 | { 80 | trainNoise += mapRowSquare[iTrain[k]]; 81 | } 82 | trainNoise /= nCells; 83 | double threshold = alpha * trainNoise; 84 | 85 | // detection if over threshold 86 | if (mapRowSquare[j] > threshold) 87 | { 88 | delay.push_back(j + x->delay[0]); 89 | doppler.push_back(x->doppler[i]); 90 | snr.push_back(mapRowSnr[j]); 91 | } 92 | iTrain.clear(); 93 | } 94 | mapRowSquare.clear(); 95 | mapRowSnr.clear(); 96 | } 97 | 98 | // create detection 99 | return std::make_unique(delay, doppler, snr); 100 | } 101 | -------------------------------------------------------------------------------- /src/capture/kraken/Kraken.h: -------------------------------------------------------------------------------- 1 | /// @file Kraken.h 2 | /// @class Kraken 3 | /// @brief A class to capture data on the Kraken SDR. 4 | /// @details Uses a custom librtlsdr API to extract samples. 5 | /// Uses 2 channels of the Kraken to capture IQ data. 6 | /// The noise source phase synchronisation is not required for 2 channel operation. 7 | /// Future work is to replicate the Heimdall DAQ phase syncronisation. 8 | /// This will enable a surveillance array of up to 4 antenna elements. 9 | /// Requires a custom librtlsdr which includes method rtlsdr_set_dithering(). 10 | /// The original steve-m/librtlsdr does not include this method. 11 | /// This is included in librtlsdr/librtlsdr or krakenrf/librtlsdr. 12 | /// Also works using 2 RTL-SDRs which have been clock synchronised. 13 | /// @author 30hours, Michael Brock, sdn-ninja 14 | /// @todo Add support for multiple surveillance channels. 15 | /// @todo Replay support. 16 | 17 | #ifndef KRAKEN_H 18 | #define KRAKEN_H 19 | 20 | #include "capture/Source.h" 21 | #include "data/IqData.h" 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | class Kraken : public Source 29 | { 30 | private: 31 | 32 | /// @brief Individual RTL-SDR devices. 33 | rtlsdr_dev_t* devs[5]; 34 | 35 | /// @brief Device indices for Kraken. 36 | std::vector channelIndex; 37 | 38 | /// @brief Gain for each channel. 39 | std::vector gain; 40 | 41 | /// @brief Check status of API returns. 42 | /// @param status Return code of API call. 43 | /// @param message Message if API call error. 44 | /// @return Void. 45 | void check_status(int status, std::string message); 46 | 47 | /// @brief Callback function when buffer is filled. 48 | /// @param buf Pointer to buffer of IQ data. 49 | /// @param len Length of buffer. 50 | /// @param ctx Context data for callback. 51 | /// @return Void. 52 | static void callback(unsigned char *buf, uint32_t len, void *ctx); 53 | 54 | public: 55 | 56 | /// @brief Constructor. 57 | /// @param fc Center frequency (Hz). 58 | /// @param path Path to save IQ data. 59 | /// @return The object. 60 | Kraken(std::string type, uint32_t fc, uint32_t fs, std::string path, 61 | bool *saveIq, std::vector gain); 62 | 63 | /// @brief Implement capture function on KrakenSDR. 64 | /// @param buffer Pointers to buffers for each channel. 65 | /// @return Void. 66 | void process(IqData *buffer1, IqData *buffer2); 67 | 68 | /// @brief Call methods to start capture. 69 | /// @return Void. 70 | void start(); 71 | 72 | /// @brief Call methods to gracefully stop capture. 73 | /// @return Void. 74 | void stop(); 75 | 76 | /// @brief Implement replay function on the Kraken. 77 | /// @param buffers Pointers to buffers for each channel. 78 | /// @param file Path to file to replay data from. 79 | /// @param loop True if samples should loop at EOF. 80 | /// @return Void. 81 | void replay(IqData *buffer1, IqData *buffer2, std::string file, bool loop); 82 | 83 | }; 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /src/data/IqData.cpp: -------------------------------------------------------------------------------- 1 | #include "IqData.h" 2 | #include 3 | #include 4 | 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/writer.h" 7 | #include "rapidjson/stringbuffer.h" 8 | #include "rapidjson/filewritestream.h" 9 | 10 | // constructor 11 | IqData::IqData(uint32_t _n) 12 | { 13 | n = _n; 14 | data = new std::deque>; 15 | } 16 | 17 | uint32_t IqData::get_n() 18 | { 19 | return n; 20 | } 21 | 22 | uint32_t IqData::get_length() 23 | { 24 | return data->size(); 25 | } 26 | 27 | void IqData::lock() 28 | { 29 | mutex_lock.lock(); 30 | } 31 | 32 | void IqData::unlock() 33 | { 34 | mutex_lock.unlock(); 35 | } 36 | 37 | std::deque> IqData::get_data() 38 | { 39 | return *data; 40 | } 41 | 42 | void IqData::push_back(std::complex sample) 43 | { 44 | if (data->size() < n) 45 | { 46 | data->push_back(sample); 47 | } 48 | else 49 | { 50 | data->pop_front(); 51 | data->push_back(sample); 52 | } 53 | } 54 | 55 | std::complex IqData::pop_front() 56 | { 57 | if (data->empty()) { 58 | throw std::runtime_error("Attempting to pop from an empty deque"); 59 | } 60 | std::complex sample = data->front(); 61 | data->pop_front(); 62 | return sample; 63 | } 64 | void IqData::print() 65 | { 66 | int n = data->size(); 67 | std::cout << data->size() << std::endl; 68 | for (int i = 0; i < n; i++) 69 | { 70 | std::cout << data->front() << std::endl; 71 | data->pop_front(); 72 | } 73 | } 74 | 75 | void IqData::clear() 76 | { 77 | while (!data->empty()) 78 | { 79 | data->pop_front(); 80 | } 81 | } 82 | 83 | void IqData::update_spectrum(std::vector> _spectrum) 84 | { 85 | spectrum = _spectrum; 86 | } 87 | 88 | void IqData::update_frequency(std::vector _frequency) 89 | { 90 | frequency = _frequency; 91 | } 92 | 93 | std::string IqData::to_json(uint64_t timestamp) 94 | { 95 | rapidjson::Document document; 96 | document.SetObject(); 97 | rapidjson::Document::AllocatorType &allocator = document.GetAllocator(); 98 | 99 | // store frequency array 100 | rapidjson::Value arrayFrequency(rapidjson::kArrayType); 101 | for (size_t i = 0; i < frequency.size(); i++) 102 | { 103 | arrayFrequency.PushBack(frequency[i], allocator); 104 | } 105 | 106 | // store spectrum array 107 | rapidjson::Value arraySpectrum(rapidjson::kArrayType); 108 | for (size_t i = 0; i < spectrum.size(); i++) 109 | { 110 | arraySpectrum.PushBack(10 * std::log10(std::abs(spectrum[i])), allocator); 111 | } 112 | 113 | document.AddMember("timestamp", timestamp, allocator); 114 | document.AddMember("min", min, allocator); 115 | document.AddMember("max", max, allocator); 116 | document.AddMember("mean", mean, allocator); 117 | document.AddMember("frequency", arrayFrequency, allocator); 118 | document.AddMember("spectrum", arraySpectrum, allocator); 119 | 120 | rapidjson::StringBuffer strbuf; 121 | rapidjson::Writer writer(strbuf); 122 | writer.SetMaxDecimalPlaces(2); 123 | document.Accept(writer); 124 | 125 | return strbuf.GetString(); 126 | } -------------------------------------------------------------------------------- /src/process/tracker/Tracker.h: -------------------------------------------------------------------------------- 1 | /// @file Tracker.h 2 | /// @class Tracker 3 | /// @brief A class to implement a bistatic tracker. 4 | /// @details Key functions are update, initiate, smooth and remove. 5 | /// @details Update before initiate to avoid duplicate tracks. 6 | /// @author 30hours 7 | /// @todo Add smoothing capability. 8 | /// @todo Fix units up. 9 | /// @todo I don't think I callback the true CPI time from ambiguity. 10 | 11 | #ifndef TRACKER_H 12 | #define TRACKER_H 13 | 14 | #include "data/Detection.h" 15 | #include "data/Track.h" 16 | 17 | #include 18 | #include 19 | 20 | class Tracker 21 | { 22 | private: 23 | /// @brief Track initiation constant for M of N detections. 24 | uint32_t m; 25 | 26 | /// @brief Track initiation constant for M of N detections. 27 | uint32_t n; 28 | 29 | /// @brief Number of missed predictions to delete a tentative track. 30 | uint32_t nDelete; 31 | 32 | /// @brief True CPI time for acceleration resolution(s). 33 | double cpi; 34 | 35 | /// @brief Maximum acceleration to initiate track (Hz/s). 36 | double maxAccInit; 37 | 38 | /// @brief Range resolution for kinematics equations (m). 39 | double rangeRes; 40 | 41 | /// @brief Wavelength for kinematics equations (m). 42 | double lambda; 43 | 44 | /// @brief Acceleration values to initiate track (Hz/s). 45 | std::vector accInit; 46 | 47 | /// @brief Index of detections already updated. 48 | std::vector doNotInitiate; 49 | 50 | /// @brief POSIX timestamp of last update (ms). 51 | uint64_t timestamp; 52 | 53 | /// @brief Track data. 54 | Track track; 55 | 56 | public: 57 | /// @brief Constructor. 58 | /// @param m Track initiation constant for M of N detections. 59 | /// @param n Track initiation constant for M of N detections. 60 | /// @param nDelete Number of missed predictions to delete a tentative track. 61 | /// @param cpi True CPI time for acceleration resolution(s). 62 | /// @param maxAccInit Maximum acceleration to initiate track (Hz/s). 63 | /// @param rangeRes Range resolution for kinematics equations (m). 64 | /// @param lambda Wavelength for kinematics equations (m). 65 | /// @return The object. 66 | Tracker(uint32_t m, uint32_t n, uint32_t nDelete, double cpi, 67 | double maxAccInit, double rangeRes, double lambda); 68 | 69 | /// @brief Destructor. 70 | /// @return Void. 71 | ~Tracker(); 72 | 73 | /// @brief Run through key functions of tracker. 74 | /// @param detection Detection data for last CPI. 75 | /// @param timestamp POSIX timestamp (ms). 76 | /// @return Pointer to track data. 77 | std::unique_ptr process(Detection *detection, uint64_t timestamp); 78 | 79 | /// @brief Update tracks by associating detections. 80 | /// @param detection Detection data for last CPI. 81 | /// @param timestamp POSIX timestamp (ms). 82 | /// @return Void. 83 | void update(Detection *detection, uint64_t timestamp); 84 | 85 | /// @brief Predict next bistatic position using kinematics equations. 86 | /// @param current Current position of track. 87 | /// @param acc Acceleration hypothesis of track. 88 | /// @param T Time elapsed from previous CPI. 89 | /// @return Predicted position of track. 90 | Detection predict(Detection current, double acc, double T); 91 | 92 | /// @brief Initiate new tentative tracks from detections. 93 | /// @param detection Detection data for last CPI. 94 | /// @return Void. 95 | void initiate(Detection *detection); 96 | }; 97 | 98 | #endif -------------------------------------------------------------------------------- /src/data/Map.h: -------------------------------------------------------------------------------- 1 | /// @file Map.h 2 | /// @class Map 3 | /// @brief A class to store an ambiguity map. 4 | /// @details References: 5 | /// 6 | /// Append to an existing array using rapidjson. 7 | /// @author 30hours 8 | 9 | #ifndef MAP_H 10 | #define MAP_H 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | template 18 | 19 | class Map 20 | { 21 | private: 22 | /// @brief Number of rows. 23 | uint32_t nRows; 24 | 25 | /// @brief Number of columns. 26 | uint32_t nCols; 27 | 28 | public: 29 | /// @brief Map data to store. 30 | std::vector> data; 31 | 32 | /// @brief Delay units of map data (bins). 33 | std::deque delay; 34 | 35 | /// @brief Doppler units of map data (Hz). 36 | std::deque doppler; 37 | 38 | /// @brief Noise power of map (dB). 39 | double noisePower; 40 | 41 | /// @brief Dynamic range of map (dB). 42 | double maxPower; 43 | 44 | /// @brief Constructor. 45 | /// @param nRows Number of rows. 46 | /// @param nCols Number of columns. 47 | /// @return The object. 48 | Map(uint32_t nRows, uint32_t nCols); 49 | 50 | /// @brief Update a row in the 2D map. 51 | /// @param i Index of row to update. 52 | /// @param row Data to update. 53 | /// @return Void. 54 | void set_row(uint32_t i, std::vector row); 55 | 56 | /// @brief Update a column in the 2D map. 57 | /// @param i Index of column to update. 58 | /// @param col Data to update. 59 | /// @return Void. 60 | void set_col(uint32_t i, std::vector col); 61 | 62 | /// @brief Create map metrics (noise power, dynamic range). 63 | /// @return Void. 64 | void set_metrics(); 65 | 66 | /// @brief Get the number of rows in the map. 67 | /// @return Number of rows. 68 | uint32_t get_nRows(); 69 | 70 | /// @brief Get the number of columns in the map. 71 | /// @return Number of columns. 72 | uint32_t get_nCols(); 73 | 74 | /// @brief Get a row from the 2D map. 75 | /// @param row Index of row to get. 76 | /// @return Vector of data. 77 | std::vector get_row(uint32_t row); 78 | 79 | /// @brief Get a column from the 2D map. 80 | /// @param col Index of column to get. 81 | /// @return Vector of data. 82 | std::vector get_col(uint32_t col); 83 | 84 | /// @brief Get a copy of the map in dB units. 85 | /// @return Pointer to dB map. 86 | Map *get_map_db(); 87 | 88 | /// @brief Print the map to stdout (for debugging). 89 | /// @return Void. 90 | void print(); 91 | 92 | /// @brief Convert a Doppler value from Hz to bins. 93 | /// @param dopplerHz Doppler value (Hz). 94 | /// @return dopplerBin Doppler value (bins). 95 | uint32_t doppler_hz_to_bin(double dopplerHz); 96 | 97 | /// @brief Generate JSON of the map and metadata. 98 | /// @return JSON string. 99 | std::string to_json(uint64_t timestamp); 100 | 101 | /// @brief Update JSON to convert delay bins to km. 102 | /// @param json Input JSON string with delay field. 103 | /// @param fs Sampling frequency (Hz). 104 | /// @return JSON string. 105 | std::string delay_bin_to_km(std::string json, uint32_t fs); 106 | 107 | /// @brief Append the map to a save file. 108 | /// @param json JSON string of map and metadata. 109 | /// @param path Path of file to save. 110 | /// @return True is save is successful. 111 | bool save(std::string json, std::string path); 112 | }; 113 | 114 | #endif 115 | -------------------------------------------------------------------------------- /src/process/detection/Interpolate.cpp: -------------------------------------------------------------------------------- 1 | #include "Interpolate.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // constructor 9 | Interpolate::Interpolate(bool _doDelay, bool _doDoppler) 10 | { 11 | // input 12 | doDelay = _doDelay; 13 | doDoppler = _doDoppler; 14 | } 15 | 16 | Interpolate::~Interpolate() 17 | { 18 | } 19 | 20 | std::unique_ptr Interpolate::process(Detection *x, Map> *y) 21 | { 22 | // store detections temporarily 23 | std::vector delay, doppler, snr; 24 | delay = x->get_delay(); 25 | doppler = x->get_doppler(); 26 | snr = x->get_snr(); 27 | 28 | // interpolate data 29 | double intDelay, intDoppler, intSnrDelay, intSnrDoppler, intSnr[3]; 30 | std::vector delay2, doppler2, snr2; 31 | std::deque indexDelay = y->delay; 32 | std::deque indexDoppler = y->doppler; 33 | 34 | // loop over every detection 35 | for (size_t i = 0; i < snr.size(); i++) 36 | { 37 | // initialise interpolated values for bool flags 38 | intDelay = delay[i]; 39 | intDoppler = doppler[i]; 40 | intSnrDelay = snr[i]; 41 | intSnrDoppler = snr[i]; 42 | // interpolate in delay 43 | if (doDelay) 44 | { 45 | // check not on boundary 46 | if (delay[i] == indexDelay[0] || delay[i] == indexDelay.back()) 47 | { 48 | continue; 49 | } 50 | intSnr[0] = (double)10*std::log10(std::abs(y->data[y->doppler_hz_to_bin(doppler[i])][delay[i]-1-indexDelay[0]]))-y->noisePower; 51 | intSnr[1] = (double)10*std::log10(std::abs(y->data[y->doppler_hz_to_bin(doppler[i])][delay[i]-indexDelay[0]]))-y->noisePower; 52 | intSnr[2] = (double)10*std::log10(std::abs(y->data[y->doppler_hz_to_bin(doppler[i])][delay[i]+1-indexDelay[0]]))-y->noisePower; 53 | // check detection has peak SNR of neighbours 54 | if (intSnr[1] < intSnr[0] || intSnr[1] < intSnr[2]) 55 | { 56 | std::cout << "Detection dropped (SNR of peak lower)" << std::endl; 57 | continue; 58 | } 59 | intDelay = (intSnr[0]-intSnr[2])/(2*(intSnr[0]-(2*intSnr[1])+intSnr[2])); 60 | intSnrDelay = intSnr[1] - (((intSnr[0]-intSnr[2])*intDelay)/4); 61 | intDelay = delay[i] + intDelay; 62 | } 63 | // interpolate in Doppler 64 | if (doDoppler) 65 | { 66 | // check not on boundary 67 | if (doppler[i] == indexDoppler[0] || doppler[i] == indexDoppler.back()) 68 | { 69 | continue; 70 | } 71 | intSnr[0] = (double)10*std::log10(std::abs(y->data[y->doppler_hz_to_bin(doppler[i])-1][delay[i]-indexDelay[0]]))-y->noisePower; 72 | intSnr[1] = (double)10*std::log10(std::abs(y->data[y->doppler_hz_to_bin(doppler[i])][delay[i]-indexDelay[0]]))-y->noisePower; 73 | intSnr[2] = (double)10*std::log10(std::abs(y->data[y->doppler_hz_to_bin(doppler[i])+1][delay[i]-indexDelay[0]]))-y->noisePower; 74 | // check detection has peak SNR of neighbours 75 | if (intSnr[1] < intSnr[0] || intSnr[1] < intSnr[2]) 76 | { 77 | continue; 78 | } 79 | intDoppler = (intSnr[0]-intSnr[2])/(2*(intSnr[0]-(2*intSnr[1])+intSnr[2])); 80 | intSnrDelay = intSnr[1] - (((intSnr[0]-intSnr[2])*intDoppler)/4); 81 | intDoppler = doppler[i] + ((indexDoppler[1]-indexDoppler[0])*intDoppler); 82 | } 83 | // store interpolated detections 84 | delay2.push_back(intDelay); 85 | doppler2.push_back(intDoppler); 86 | snr2.push_back(std::max(std::max(intSnrDelay, intSnrDoppler), snr[i])); 87 | } 88 | 89 | // create detection 90 | return std::make_unique(delay2, doppler2, snr2); 91 | } 92 | -------------------------------------------------------------------------------- /html/js/plot_spectrum.js: -------------------------------------------------------------------------------- 1 | var timestamp = -1; 2 | var nRows = 3; 3 | var host = window.location.hostname; 4 | var isLocalHost = is_localhost(host); 5 | var range_x = []; 6 | var range_y = []; 7 | 8 | // setup API 9 | var urlTimestamp; 10 | var urlMap; 11 | if (isLocalHost) { 12 | urlTimestamp = '//' + host + ':3000/api/timestamp'; 13 | } else { 14 | urlTimestamp = '//' + host + '/api/timestamp'; 15 | } 16 | if (isLocalHost) { 17 | urlMap = '//' + host + ':3000' + '/stash/iqdata'; 18 | } else { 19 | urlMap = '//' + host + '/stash/iqdata'; 20 | } 21 | 22 | // setup plotly 23 | var layout = { 24 | autosize: true, 25 | margin: { 26 | l: 50, 27 | r: 50, 28 | b: 50, 29 | t: 10, 30 | pad: 0 31 | }, 32 | hoverlabel: { 33 | namelength: 0 34 | }, 35 | plot_bgcolor: "rgba(0,0,0,0)", 36 | paper_bgcolor: "rgba(0,0,0,0)", 37 | annotations: [], 38 | displayModeBar: false, 39 | xaxis: { 40 | title: { 41 | text: 'Frequency (MHz)', 42 | font: { 43 | size: 24 44 | } 45 | }, 46 | ticks: '', 47 | side: 'bottom' 48 | }, 49 | yaxis: { 50 | title: { 51 | text: 'Timestamp', 52 | font: { 53 | size: 24 54 | } 55 | }, 56 | ticks: '', 57 | ticksuffix: ' ', 58 | autosize: false, 59 | categoryorder: "total descending" 60 | } 61 | }; 62 | var config = { 63 | responsive: true, 64 | displayModeBar: false 65 | } 66 | 67 | // setup plotly data 68 | var data = [ 69 | { 70 | z: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 71 | colorscale: 'Jet', 72 | type: 'heatmap' 73 | } 74 | ]; 75 | var detection = []; 76 | 77 | Plotly.newPlot('data', data, layout, config); 78 | 79 | // callback function 80 | var intervalId = window.setInterval(function () { 81 | 82 | // check if timestamp is updated 83 | $.get(urlTimestamp, function () { }) 84 | 85 | .done(function (data) { 86 | if (timestamp != data) { 87 | timestamp = data; 88 | 89 | // get new map data 90 | $.getJSON(urlMap, function () { }) 91 | .done(function (data) { 92 | 93 | // case draw new plot 94 | if (data.nRows != nRows) { 95 | nRows = data.nRows; 96 | // timestamp posix to js 97 | for (i = 0; i < data.timestamp.length; i++) 98 | { 99 | data.timestamp[i] = new Date(data.timestamp[i]); 100 | } 101 | var trace1 = { 102 | y: data.timestamp, 103 | z: data.spectrum, 104 | colorscale: 'Jet', 105 | zauto: false, 106 | type: 'heatmap' 107 | }; 108 | 109 | var data_trace = [trace1]; 110 | Plotly.newPlot('data', data_trace, layout, config); 111 | } 112 | // case update plot 113 | else { 114 | // timestamp posix to js 115 | for (i = 0; i < data.timestamp.length; i++) 116 | { 117 | data.timestamp[i] = new Date(data.timestamp[i]); 118 | } 119 | var trace_update = { 120 | y: [data.timestamp], 121 | z: [data.spectrum] 122 | }; 123 | Plotly.update('data', trace_update); 124 | } 125 | 126 | }) 127 | .fail(function () { 128 | }) 129 | .always(function () { 130 | }); 131 | } 132 | }) 133 | .fail(function () { 134 | }) 135 | .always(function () { 136 | }); 137 | }, 100); 138 | -------------------------------------------------------------------------------- /src/process/ambiguity/Ambiguity.h: -------------------------------------------------------------------------------- 1 | /// @file Ambiguity.h 2 | /// @class Ambiguity 3 | /// @brief A class to implement a ambiguity map processing. 4 | /// @details Implements a the batches algorithm as described in Principles of Modern Radar, Volume II, Chapter 17. 5 | /// See Fundamentals of Radar Signal Processing (Richards) for more on the pulse-Doppler processing method. 6 | /// @author 30hours 7 | /// @todo Ambiguity maps are still offset by 1 bin. 8 | /// @todo Write a performance test for hamming assisted ambiguity processing. 9 | /// @todo If delayMin > delayMax = trouble, what's the exception policy? 10 | 11 | #include "data/IqData.h" 12 | #include "data/Map.h" 13 | #include "process/meta/HammingNumber.h" 14 | #include 15 | #include 16 | #include 17 | 18 | class Ambiguity 19 | { 20 | 21 | public: 22 | 23 | using Complex = std::complex; 24 | 25 | /// @brief Constructor. 26 | /// @param delayMin Minimum delay (bins). 27 | /// @param delayMax Maximum delay (bins). 28 | /// @param dopplerMin Minimum Doppler (Hz). 29 | /// @param dopplerMax Maximum Doppler (Hz). 30 | /// @param fs Sampling frequency (Hz). 31 | /// @param n Number of samples. 32 | /// @param roundHamming Round the correlation FFT length to a Hamming number for performance. 33 | /// @return The object. 34 | Ambiguity(int32_t delayMin, int32_t delayMax, int32_t dopplerMin, int32_t dopplerMax, uint32_t fs, uint32_t n, bool roundHamming = false); 35 | 36 | /// @brief Destructor. 37 | /// @return Void. 38 | ~Ambiguity(); 39 | 40 | /// @brief Implement the ambiguity processor. 41 | /// @param x Reference samples. 42 | /// @param y Surveillance samples. 43 | /// @return Ambiguity map data of IQ samples. 44 | Map *process(IqData *x, IqData *y); 45 | 46 | double get_doppler_middle() const; 47 | 48 | uint16_t get_n_delay_bins() const; 49 | 50 | uint16_t get_n_doppler_bins() const; 51 | 52 | uint16_t get_n_corr() const; 53 | 54 | double get_cpi() const; 55 | 56 | uint32_t get_nfft() const; 57 | 58 | uint32_t get_n_samples() const; 59 | 60 | private: 61 | /// @brief Minimum delay (bins). 62 | int32_t delayMin; 63 | 64 | /// @brief Maximum delay (bins). 65 | int32_t delayMax; 66 | 67 | /// @brief Minimum Doppler (Hz). 68 | int32_t dopplerMin; 69 | 70 | /// @brief Maximum Doppler (Hz). 71 | int32_t dopplerMax; 72 | 73 | /// @brief Sampling frequency (Hz). 74 | uint32_t fs; 75 | 76 | /// @brief Number of samples. 77 | uint32_t nSamples; 78 | 79 | /// @brief Number of delay bins. 80 | uint16_t nDelayBins; 81 | 82 | /// @brief Center of Doppler bins (Hz). 83 | double dopplerMiddle; 84 | 85 | /// @brief Number of Doppler bins. 86 | uint16_t nDopplerBins; 87 | 88 | /// @brief Number of correlation samples per pulse. 89 | uint16_t nCorr; 90 | 91 | /// @brief True CPI time (s). 92 | double cpi; 93 | 94 | /// @brief FFTW plans for ambiguity processing. 95 | fftw_plan fftXi; 96 | fftw_plan fftYi; 97 | fftw_plan fftZi; 98 | fftw_plan fftDoppler; 99 | 100 | /// @brief FFTW storage for ambiguity processing. 101 | /// @{ 102 | std::vector dataXi; 103 | std::vector dataYi; 104 | std::vector dataZi; 105 | std::vector dataCorr; 106 | std::vector dataDoppler; 107 | /// @} 108 | 109 | /// @brief Number of samples to perform FFT per pulse. 110 | uint32_t nfft; 111 | 112 | /// @brief Vector storage for ambiguity processing 113 | /// @{ 114 | std::vector corr; 115 | std::vector delayProfile; 116 | /// @} 117 | 118 | /// @brief Map to store result. 119 | std::unique_ptr> map; 120 | 121 | }; 122 | -------------------------------------------------------------------------------- /src/capture/usrp/Usrp.cpp: -------------------------------------------------------------------------------- 1 | #include "Usrp.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // constructor 10 | Usrp::Usrp(std::string _type, uint32_t _fc, uint32_t _fs, 11 | std::string _path, bool *_saveIq, std::string _address, 12 | std::string _subdev, std::vector _antenna, 13 | std::vector _gain) 14 | : Source(_type, _fc, _fs, _path, _saveIq) 15 | { 16 | address = _address; 17 | subdev = _subdev; 18 | antenna = _antenna; 19 | gain = _gain; 20 | } 21 | 22 | void Usrp::start() 23 | { 24 | } 25 | 26 | void Usrp::stop() 27 | { 28 | } 29 | 30 | void Usrp::process(IqData *buffer1, IqData *buffer2) 31 | { 32 | // create a USRP object 33 | uhd::usrp::multi_usrp::sptr usrp = 34 | uhd::usrp::multi_usrp::make(address); 35 | 36 | usrp->set_rx_subdev_spec(uhd::usrp::subdev_spec_t(subdev), 0); 37 | 38 | usrp->set_rx_antenna(antenna[0], 0); 39 | usrp->set_rx_antenna(antenna[1], 1); 40 | 41 | // set sample rate across all channels 42 | usrp->set_rx_rate((double(fs))); 43 | 44 | // set the center frequency 45 | double centerFrequency = (double)fc; 46 | usrp->set_rx_freq(centerFrequency, 0); 47 | usrp->set_rx_freq(centerFrequency, 1); 48 | 49 | // set the gain 50 | usrp->set_rx_gain(gain[0], 0); 51 | usrp->set_rx_gain(gain[1], 1); 52 | 53 | // create a receive streamer 54 | uhd::stream_args_t streamArgs("fc32", "sc16"); 55 | streamArgs.channels = {0, 1}; 56 | uhd::rx_streamer::sptr rxStreamer = usrp->get_rx_stream(streamArgs); 57 | 58 | // allocate buffers to receive with samples (one buffer per channel) 59 | const size_t samps_per_buff = rxStreamer->get_max_num_samps(); 60 | std::vector> usrpBuffer1(samps_per_buff); 61 | std::vector> usrpBuffer2(samps_per_buff); 62 | 63 | // create a vector of pointers to point to each of the channel buffers 64 | std::vector*> buff_ptrs; 65 | buff_ptrs.push_back(&usrpBuffer1.front()); 66 | buff_ptrs.push_back(&usrpBuffer2.front()); 67 | 68 | // setup stream 69 | uhd::rx_metadata_t metadata; 70 | uhd::stream_cmd_t streamCmd = uhd::stream_cmd_t::STREAM_MODE_START_CONTINUOUS; 71 | streamCmd.stream_now = false; 72 | streamCmd.time_spec = usrp->get_time_now() + uhd::time_spec_t(0.05); 73 | rxStreamer->issue_stream_cmd(streamCmd); 74 | 75 | while(true) 76 | { 77 | // receive samples 78 | size_t nReceived = rxStreamer->recv(buff_ptrs, samps_per_buff, metadata); 79 | 80 | // print errors 81 | if (metadata.error_code != uhd::rx_metadata_t::ERROR_CODE_NONE) { 82 | std::cerr << "Error: " << metadata.strerror() << std::endl; 83 | } 84 | 85 | buffer1->lock(); 86 | buffer2->lock(); 87 | for (size_t i = 0; i < nReceived; i++) 88 | { 89 | buffer1->push_back({(double)buff_ptrs[0][i].real(), (double)buff_ptrs[0][i].imag()}); 90 | buffer2->push_back({(double)buff_ptrs[1][i].real(), (double)buff_ptrs[1][i].imag()}); 91 | } 92 | buffer1->unlock(); 93 | buffer2->unlock(); 94 | 95 | // save IQ data to file 96 | if (*saveIq) 97 | { 98 | for (const auto& bufferPtr : buff_ptrs) 99 | { 100 | // Write the buffer data to the file 101 | saveIqFile.write(reinterpret_cast( 102 | bufferPtr), samps_per_buff * sizeof(std::complex)); 103 | } 104 | } 105 | } 106 | } 107 | 108 | void Usrp::replay(IqData *buffer1, IqData *buffer2, std::string _file, bool _loop) 109 | { 110 | return; 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blah2 2 | 3 | A real-time radar which can support various SDR platforms. See a live instance at [http://radar4.30hours.dev](http://radar4.30hours.dev). 4 | 5 | ![blah2 example display](./example.png "blah2") 6 | 7 | ## Features 8 | 9 | - 2 channel processing for a reference and surveillance signal. 10 | - Designed to be used with external RF source (for passive radar or active radar). 11 | - Outputs delay-Doppler maps to a web front-end. 12 | - Record raw IQ data by pressing spacebar on the web front-end. 13 | - Saves delay-Doppler maps in a JSON array. 14 | 15 | ## SDR Support 16 | 17 | - [SDRplay RSPDuo](https://www.sdrplay.com/rspduo/). 18 | - [USRP](https://www.ettus.com/products/) (only tested on the B210). 19 | - 2x [HackRF](https://greatscottgadgets.com/hackrf/) with clock synchronisation and hardware trigger. 20 | - 2x [RTL-SDR](https://www.rtl-sdr.com/) with clock synchronisation. 21 | - [KrakenSDR](https://www.krakenrf.com/) with 2x channels only. 22 | 23 | ## Services 24 | 25 | The build environment consists of a docker-compose.yml file running the following services; 26 | 27 | - The radar processor responsible for IQ capture and processing. 28 | - The API middleware responsible for reading TCP ports for delay-Doppler map data, and exposing this on a REST API. 29 | - The web front-end displaying processed radar data. 30 | 31 | ## Usage 32 | 33 | Building the code using the following instructions; 34 | 35 | - Install docker and docker-compose on the host machine. 36 | - Clone this repository to some directory. 37 | - Install SDRplay API to run service on host. 38 | - Edit the `config/config.yml` for desired processing parameters. 39 | - Run the docker-compose command. 40 | 41 | ```bash 42 | sudo git clone http://github.com/30hours/blah2 /opt/blah2 43 | cd /opt/blah2 44 | sudo chown -R $USER . 45 | sudo chmod a+x ./lib/sdrplay-3.15.2/SDRplay_RSP_API-Linux-3.15.2.run 46 | sudo ./lib/sdrplay-3.15.2/SDRplay_RSP_API-Linux-3.15.2.run --tar -xvf -C ./lib/sdrplay-3.15.2 47 | cd lib/sdrplay-3.15.2/ && sudo ./install_lib.sh && cd ../../ 48 | sudo docker network create blah2 49 | sudo systemctl enable docker 50 | sudo docker compose up -d --build 51 | ``` 52 | 53 | Alternatively avoid building and use the pre-built Docker packages; 54 | 55 | ```bash 56 | sudo docker pull ghcr.io/30hours/blah2:latest 57 | vim docker-compose.yml 58 | --- build: . 59 | +++ image: ghcr.io/30hours/blah2:latest 60 | sudo docker compose up -d 61 | ``` 62 | 63 | The radar processing output is available on [http://localhost:49152](http://localhost:49152). 64 | 65 | ## Documentation 66 | 67 | - See `doxygen` pages hosted at [http://doc.30hours.dev/blah2](http://doc.30hours.dev/blah2). 68 | 69 | ## Future Work 70 | 71 | - Add a tracker in delay-Doppler space. 72 | - Support for the HackRF/RTL-SDR using a front-end mixer, to sample 2 RF channels in 1 stream. 73 | - Support for the Kraken SDR with all 5 channels. 74 | - Add [SoapySDR](https://github.com/pothosware/SoapySDR) support for the [C++ API](https://github.com/pothosware/SoapySDR/wiki/Cpp_API_Example) to include a wide range of SDR platforms. 75 | 76 | ## FAQ 77 | 78 | - If the SDRplay RSPduo does not capture data, restart the API service (on the host) using the script `sudo ./script/blah2_rspduo_restart.bash`. 79 | 80 | ## Contributing 81 | 82 | Pull requests are welcome - especially for adding support for a new SDR. 83 | 84 | - Currently have an issue where the USRP B210 is timing out after 5-10 mins and crashes the code. Convinced it's an issue with my usage of the API - contact me for more info. 85 | 86 | ## Links 87 | 88 | - Join the [Discord](https://discord.gg/ewNQbeK5Zn) chat for sharing results and support. 89 | 90 | - Watch a [Youtube video](https://www.youtube.com/watch?v=FF2n28qoTQM) showing the hardware and software setup. 91 | 92 | ## License 93 | 94 | [MIT](https://choosealicense.com/licenses/mit/) 95 | -------------------------------------------------------------------------------- /html/js/plot_detection.js: -------------------------------------------------------------------------------- 1 | var timestamp; 2 | var nRows = 3; 3 | var host = window.location.hostname; 4 | var isLocalHost = is_localhost(host); 5 | var range_x = []; 6 | var range_y = []; 7 | 8 | // setup API 9 | var urlTimestamp; 10 | var urlDetection; 11 | if (isLocalHost) { 12 | urlTimestamp = '//' + host + ':3000/api/timestamp'; 13 | } else { 14 | urlTimestamp = '//' + host + '/api/timestamp'; 15 | } 16 | if (isLocalHost) { 17 | urlDetection = '//' + host + ':3000/stash/detection'; 18 | } else { 19 | urlDetection = '//' + host + '/stash/detection'; 20 | } 21 | 22 | // setup plotly 23 | var layout = { 24 | autosize: false, 25 | margin: { 26 | l: 50, 27 | r: 50, 28 | b: 50, 29 | t: 10, 30 | pad: 0 31 | }, 32 | hoverlabel: { 33 | namelength: 0 34 | }, 35 | width: document.getElementById('data').offsetWidth, 36 | height: document.getElementById('data').offsetHeight, 37 | plot_bgcolor: "rgba(0,0,0,0)", 38 | paper_bgcolor: "rgba(0,0,0,0)", 39 | annotations: [], 40 | displayModeBar: false, 41 | xaxis: { 42 | title: { 43 | text: xTitle, 44 | font: { 45 | size: 24 46 | } 47 | }, 48 | showgrid: false, 49 | ticks: '', 50 | side: 'bottom' 51 | }, 52 | yaxis: { 53 | title: { 54 | text: yTitle, 55 | font: { 56 | size: 24 57 | } 58 | }, 59 | showgrid: false, 60 | ticks: '', 61 | ticksuffix: ' ', 62 | autosize: false, 63 | categoryorder: "total descending" 64 | } 65 | }; 66 | var config = { 67 | displayModeBar: false, 68 | scrollZoom: true 69 | } 70 | 71 | // setup plotly data 72 | var data = [ 73 | { 74 | z: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 75 | colorscale: 'Jet', 76 | type: 'heatmap' 77 | } 78 | ]; 79 | 80 | Plotly.newPlot('data', data, layout, config); 81 | 82 | // callback function 83 | var intervalId = window.setInterval(function () { 84 | 85 | // check if timestamp is updated 86 | var timestampData = $.get(urlTimestamp, function () { }) 87 | 88 | .done(function (data) { 89 | if (timestamp != data) { 90 | timestamp = data; 91 | 92 | // get new data 93 | var apiData = $.getJSON(urlDetection, function () { }) 94 | .done(function (data) { 95 | 96 | // case draw new plot 97 | if (data.nRows != nRows) { 98 | nRows = data.nRows; 99 | 100 | // timestamp posix to js 101 | if (xVariable === "timestamp") 102 | { 103 | for (i = 0; i < data[xVariable].length; i++) 104 | { 105 | data[xVariable][i] = new Date(data[xVariable][i]); 106 | } 107 | } 108 | 109 | var trace1 = { 110 | x: data[xVariable], 111 | y: data[yVariable], 112 | mode: 'markers', 113 | type: 'scatter' 114 | }; 115 | 116 | var data_trace = [trace1]; 117 | Plotly.newPlot('data', data_trace, layout, config); 118 | } 119 | // case update plot 120 | else { 121 | // timestamp posix to js 122 | if (xVariable === "timestamp") 123 | { 124 | for (i = 0; i < data[xVariable].length; i++) 125 | { 126 | data[xVariable][i] = new Date(data[xVariable][i]); 127 | } 128 | } 129 | var trace_update = { 130 | x: [data[xVariable]], 131 | y: [data[yVariable]] 132 | }; 133 | Plotly.update('data', trace_update); 134 | } 135 | 136 | }) 137 | .fail(function () { 138 | }) 139 | .always(function () { 140 | }); 141 | } 142 | }) 143 | .fail(function () { 144 | }) 145 | .always(function () { 146 | }); 147 | }, 100); 148 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | if(COMMAND cmake_policy) 3 | cmake_policy(SET CMP0003 NEW) 4 | endif(COMMAND cmake_policy) 5 | 6 | project(blah2) 7 | include(CMakePrintHelpers) 8 | include(CTest) 9 | 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror") 11 | 12 | find_package(Threads REQUIRED) 13 | find_package(asio REQUIRED) 14 | find_path(RAPIDJSON_INCLUDE_DIRS "rapidjson/allocators.h") 15 | find_package(ryml CONFIG REQUIRED) 16 | find_package(httplib CONFIG REQUIRED) 17 | find_package(Armadillo CONFIG REQUIRED) 18 | find_package(Catch2 CONFIG REQUIRED) 19 | 20 | set(CMAKE_PREFIX_PATH "/opt/uhd" ${CMAKE_PREFIX_PATH}) 21 | find_package(UHD "4.8.0.0" CONFIG REQUIRED) 22 | 23 | set(PROJECT_ROOT "${PROJECT_SOURCE_DIR}") 24 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_ROOT}/bin") 25 | set(PROJECT_BINARY_TEST_DIR "${PROJECT_ROOT}/bin/test") 26 | set(PROJECT_BINARY_TEST_UNIT_DIR "${PROJECT_BINARY_TEST_DIR}/unit") 27 | set(PROJECT_BINARY_TEST_FUNCTIONAL_DIR "${PROJECT_BINARY_TEST_DIR}/functional") 28 | set(PROJECT_BINARY_TEST_COMPARISON_DIR "${PROJECT_BINARY_TEST_DIR}/comparison") 29 | 30 | message("Binary path: ${PROJECT_BINARY_DIR}") 31 | message("Binary test path: ${PROJECT_BINARY_TEST_DIR}") 32 | 33 | # include from top-level src dir 34 | include_directories(src ${UHD_INCLUDE_DIRS}) 35 | 36 | # TODO: create FindSdrplay.cmake for this 37 | add_library(sdrplay /usr/local/include/sdrplay_api.h) 38 | set_target_properties(sdrplay PROPERTIES LINKER_LANGUAGE C) 39 | target_link_libraries(sdrplay PUBLIC /usr/local/lib/libsdrplay_api.so.3.15) 40 | 41 | # TODO: Move to separate src/CMakeLists.txt 42 | add_executable(blah2 43 | src/blah2.cpp 44 | src/capture/Capture.cpp 45 | src/capture/Source.cpp 46 | src/capture/rspduo/RspDuo.cpp 47 | src/capture/usrp/Usrp.cpp 48 | src/capture/hackrf/HackRf.cpp 49 | src/capture/kraken/Kraken.cpp 50 | src/process/ambiguity/Ambiguity.cpp 51 | src/process/clutter/WienerHopf.cpp 52 | src/process/detection/CfarDetector1D.cpp 53 | src/process/detection/Centroid.cpp 54 | src/process/detection/Interpolate.cpp 55 | src/process/tracker/Tracker.cpp 56 | src/process/spectrum/SpectrumAnalyser.cpp 57 | src/process/meta/HammingNumber.cpp 58 | src/process/utility/Socket.cpp 59 | src/data/IqData.cpp 60 | src/data/Map.cpp 61 | src/data/Detection.cpp 62 | src/data/Track.cpp 63 | src/data/meta/Timing.cpp 64 | ) 65 | 66 | target_link_libraries(blah2 PRIVATE 67 | Threads::Threads 68 | asio::asio 69 | ryml::ryml 70 | httplib::httplib 71 | armadillo 72 | ${UHD_LIBRARIES} 73 | fftw3 74 | fftw3_threads 75 | sdrplay 76 | hackrf 77 | rtlsdr 78 | ) 79 | target_include_directories(blah2 PRIVATE RAPIDJSON_INCLUDE_DIRS "rapidjson/allocators.h") 80 | 81 | # unit tests 82 | add_executable(testAmbiguity 83 | test/unit/process/ambiguity/TestAmbiguity.cpp 84 | src/data/IqData.cpp 85 | src/data/Map.cpp 86 | src/process/ambiguity/Ambiguity.cpp 87 | src/process/meta/HammingNumber.cpp 88 | ) 89 | target_link_libraries(testAmbiguity PRIVATE 90 | Catch2::Catch2WithMain 91 | fftw3 92 | fftw3_threads 93 | ) 94 | set_target_properties(testAmbiguity PROPERTIES 95 | RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_TEST_UNIT_DIR}") 96 | 97 | add_executable(testTracker 98 | test/unit/process/tracker/TestTracker.cpp 99 | src/data/Detection.cpp 100 | src/data/Track.cpp 101 | src/process/tracker/Tracker.cpp 102 | ) 103 | target_link_libraries(testTracker PRIVATE 104 | Catch2::Catch2WithMain 105 | ) 106 | set_target_properties(testTracker PROPERTIES 107 | RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_TEST_UNIT_DIR}") 108 | 109 | add_executable(testHammingNumber 110 | test/unit/process/meta/TestHammingNumber.cpp 111 | src/process/meta/HammingNumber.cpp 112 | ) 113 | target_link_libraries(testHammingNumber PRIVATE 114 | Catch2::Catch2WithMain 115 | ) 116 | set_target_properties(testHammingNumber PROPERTIES 117 | RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_TEST_UNIT_DIR}") 118 | 119 | # TODO: Unsure if will be using CTest. 120 | add_test(NAME testAmbiguity COMMAND testAmbiguity) 121 | add_test(NAME testTracker COMMAND testTracker) 122 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 21, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "cmake-pedantic", 11 | "hidden": true, 12 | "warnings": { 13 | "dev": true, 14 | "deprecated": true, 15 | "unusedCli": true, 16 | "systemVars": false 17 | }, 18 | "errors": { 19 | "dev": true, 20 | "deprecated": true 21 | } 22 | }, 23 | { 24 | "name": "cppcheck", 25 | "hidden": true, 26 | "cacheVariables": { 27 | "CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr" 28 | } 29 | }, 30 | { 31 | "name": "clang-tidy", 32 | "hidden": true, 33 | "cacheVariables": { 34 | "CMAKE_CXX_CLANG_TIDY": "clang-tidy", 35 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" 36 | } 37 | }, 38 | { 39 | "name": "ci-std", 40 | "description": "This preset makes sure the project actually builds with at least the specified standard", 41 | "hidden": true, 42 | "cacheVariables": { 43 | "CMAKE_CXX_EXTENSIONS": "OFF", 44 | "CMAKE_CXX_STANDARD": "20", 45 | "CMAKE_CXX_STANDARD_REQUIRED": "ON" 46 | } 47 | }, 48 | { 49 | "name": "ci-vcpkg", 50 | "hidden": true, 51 | "description": "Bootstrap the toolchain with vcpkg installed paths", 52 | "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" 53 | }, 54 | { 55 | "name": "ci-build", 56 | "binaryDir": "${sourceDir}/build/${presetName}", 57 | "inherits": ["cmake-pedantic"], 58 | "hidden": true 59 | }, 60 | { 61 | "name": "ci-unix", 62 | "generator": "Unix Makefiles", 63 | "hidden": true, 64 | "condition": { 65 | "type": "equals", 66 | "lhs": "${hostSystemName}", 67 | "rhs": "Linux" 68 | }, 69 | "inherits": [ 70 | "ci-std", 71 | "ci-build", 72 | "ci-vcpkg" 73 | ] 74 | }, 75 | { 76 | "name": "ci-release", 77 | "hidden": true, 78 | "cacheVariables": { 79 | "CMAKE_BUILD_TYPE": "Release" 80 | } 81 | }, 82 | { 83 | "name": "ci-debug", 84 | "hidden": true, 85 | "cacheVariables": { 86 | "CMAKE_BUILD_TYPE": "Debug" 87 | } 88 | }, 89 | { 90 | "name": "dev-debug", 91 | "inherits": [ 92 | "ci-unix", 93 | "ci-debug" 94 | ] 95 | }, 96 | { 97 | "name": "dev-release", 98 | "inherits": [ 99 | "ci-unix", 100 | "ci-release", 101 | "clang-tidy" 102 | ] 103 | }, 104 | { 105 | "name": "prod-release", 106 | "installDir": "$env{HOME}/.local", 107 | "inherits": [ 108 | "ci-unix", 109 | "ci-release" 110 | ] 111 | } 112 | ], 113 | "buildPresets": [ 114 | { 115 | "name": "dev-debug", 116 | "configurePreset": "dev-debug", 117 | "configuration": "Debug", 118 | "jobs": 4 119 | }, 120 | { 121 | "name": "dev-release", 122 | "configurePreset": "dev-release", 123 | "configuration": "Release", 124 | "jobs": 4 125 | }, 126 | { 127 | "name": "prod-release", 128 | "configurePreset": "prod-release", 129 | "configuration": "Release", 130 | "jobs": 4 131 | } 132 | ], 133 | "testPresets": [ 134 | { 135 | "name": "test-all-unix-release", 136 | "configurePreset": "prod-release", 137 | "output": { 138 | "outputOnFailure": true 139 | }, 140 | "execution": { 141 | "jobs": 1 142 | } 143 | } 144 | ] 145 | } -------------------------------------------------------------------------------- /html/controller/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blah2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 |
19 |
20 | 31 | 43 |
44 | 53 |
54 |
55 | 62 |
63 |
64 |
65 | 66 | 67 | 83 | 84 | -------------------------------------------------------------------------------- /src/data/Detection.cpp: -------------------------------------------------------------------------------- 1 | #include "Detection.h" 2 | #include "data/meta/Constants.h" 3 | #include 4 | #include 5 | #include 6 | 7 | #include "rapidjson/document.h" 8 | #include "rapidjson/writer.h" 9 | #include "rapidjson/stringbuffer.h" 10 | #include "rapidjson/filewritestream.h" 11 | 12 | // constructor 13 | Detection::Detection(std::vector _delay, std::vector _doppler, std::vector _snr) 14 | { 15 | delay = _delay; 16 | doppler = _doppler; 17 | snr = _snr; 18 | } 19 | 20 | Detection::Detection(double _delay, double _doppler, double _snr) 21 | { 22 | delay.push_back(_delay); 23 | doppler.push_back(_doppler); 24 | snr.push_back(_snr); 25 | } 26 | 27 | std::vector Detection::get_delay() 28 | { 29 | return delay; 30 | } 31 | 32 | std::vector Detection::get_doppler() 33 | { 34 | return doppler; 35 | } 36 | 37 | std::vector Detection::get_snr() 38 | { 39 | return snr; 40 | } 41 | 42 | size_t Detection::get_nDetections() 43 | { 44 | return delay.size(); 45 | } 46 | 47 | std::string Detection::to_json(uint64_t timestamp) 48 | { 49 | rapidjson::Document document; 50 | document.SetObject(); 51 | rapidjson::Document::AllocatorType &allocator = document.GetAllocator(); 52 | 53 | // store delay array 54 | rapidjson::Value arrayDelay(rapidjson::kArrayType); 55 | for (size_t i = 0; i < get_nDetections(); i++) 56 | { 57 | arrayDelay.PushBack(delay[i], allocator); 58 | } 59 | 60 | // store Doppler array 61 | rapidjson::Value arrayDoppler(rapidjson::kArrayType); 62 | for (size_t i = 0; i < get_nDetections(); i++) 63 | { 64 | arrayDoppler.PushBack(doppler[i], allocator); 65 | } 66 | 67 | // store snr array 68 | rapidjson::Value arraySnr(rapidjson::kArrayType); 69 | for (size_t i = 0; i < get_nDetections(); i++) 70 | { 71 | arraySnr.PushBack(snr[i], allocator); 72 | } 73 | 74 | document.AddMember("timestamp", timestamp, allocator); 75 | document.AddMember("delay", arrayDelay, allocator); 76 | document.AddMember("doppler", arrayDoppler, allocator); 77 | document.AddMember("snr", arraySnr, allocator); 78 | 79 | rapidjson::StringBuffer strbuf; 80 | rapidjson::Writer writer(strbuf); 81 | writer.SetMaxDecimalPlaces(2); 82 | document.Accept(writer); 83 | 84 | return strbuf.GetString(); 85 | } 86 | 87 | std::string Detection::delay_bin_to_km(std::string json, uint32_t fs) 88 | { 89 | rapidjson::Document document; 90 | document.SetObject(); 91 | rapidjson::Document::AllocatorType &allocator = document.GetAllocator(); 92 | document.Parse(json.c_str()); 93 | 94 | document["delay"].Clear(); 95 | for (size_t i = 0; i < delay.size(); i++) 96 | { 97 | document["delay"].PushBack(1.0*delay[i]*(Constants::c/(double)fs)/1000, allocator); 98 | } 99 | 100 | rapidjson::StringBuffer strbuf; 101 | rapidjson::Writer writer(strbuf); 102 | writer.SetMaxDecimalPlaces(2); 103 | document.Accept(writer); 104 | 105 | return strbuf.GetString(); 106 | } 107 | 108 | bool Detection::save(std::string _json, std::string filename) 109 | { 110 | using namespace rapidjson; 111 | 112 | rapidjson::Document document; 113 | 114 | // create file if it doesn't exist 115 | if (FILE *fp = fopen(filename.c_str(), "r"); !fp) 116 | { 117 | if (fp = fopen(filename.c_str(), "w"); !fp) 118 | return false; 119 | fputs("[]", fp); 120 | fclose(fp); 121 | } 122 | 123 | // add the document to the file 124 | if (FILE *fp = fopen(filename.c_str(), "rb+"); fp) 125 | { 126 | // check if first is [ 127 | std::fseek(fp, 0, SEEK_SET); 128 | if (getc(fp) != '[') 129 | { 130 | std::fclose(fp); 131 | return false; 132 | } 133 | 134 | // is array empty? 135 | bool isEmpty = false; 136 | if (getc(fp) == ']') 137 | isEmpty = true; 138 | 139 | // check if last is ] 140 | std::fseek(fp, -1, SEEK_END); 141 | if (getc(fp) != ']') 142 | { 143 | std::fclose(fp); 144 | return false; 145 | } 146 | 147 | // replace ] by , 148 | fseek(fp, -1, SEEK_END); 149 | if (!isEmpty) 150 | fputc(',', fp); 151 | 152 | // add json element 153 | fwrite(_json.c_str(), sizeof(char), _json.length(), fp); 154 | 155 | // close the array 156 | std::fputc(']', fp); 157 | fclose(fp); 158 | return true; 159 | } 160 | return false; 161 | } 162 | -------------------------------------------------------------------------------- /html/js/plot_timing.js: -------------------------------------------------------------------------------- 1 | var timestamp = -1; 2 | var nRows = 3; 3 | var host = window.location.hostname; 4 | var isLocalHost = is_localhost(host); 5 | 6 | // setup API 7 | var urlTimestamp; 8 | var urlTiming; 9 | if (isLocalHost) { 10 | urlTimestamp = '//' + host + ':3000/api/timestamp'; 11 | } else { 12 | urlTimestamp = '//' + host + '/api/timestamp'; 13 | } 14 | if (isLocalHost) { 15 | urlTiming = '//' + host + ':3000/stash/timing'; 16 | } else { 17 | urlTiming = '//' + host + '/stash/timing'; 18 | } 19 | 20 | // setup plotly 21 | var layout = { 22 | autosize: false, 23 | margin: { 24 | l: 50, 25 | r: 50, 26 | b: 50, 27 | t: 10, 28 | pad: 0 29 | }, 30 | hoverlabel: { 31 | namelength: 0 32 | }, 33 | width: document.getElementById('data').offsetWidth, 34 | height: document.getElementById('data').offsetHeight, 35 | plot_bgcolor: "rgba(0,0,0,0)", 36 | paper_bgcolor: "rgba(0,0,0,0)", 37 | annotations: [], 38 | displayModeBar: false, 39 | xaxis: { 40 | title: { 41 | text: xTitle, 42 | font: { 43 | size: 24 44 | } 45 | }, 46 | showgrid: false, 47 | ticks: '', 48 | side: 'bottom' 49 | }, 50 | yaxis: { 51 | title: { 52 | text: yTitle, 53 | font: { 54 | size: 24 55 | } 56 | }, 57 | showgrid: false, 58 | ticks: '', 59 | ticksuffix: ' ', 60 | autosize: false, 61 | categoryorder: "total descending" 62 | }, 63 | legend: { 64 | orientation: "h", 65 | bgcolor: "#f78c58", 66 | bordercolor: "#000000", 67 | borderwidth: 2 68 | } 69 | }; 70 | var config = { 71 | displayModeBar: false, 72 | scrollZoom: true 73 | } 74 | 75 | // setup plotly data 76 | var data = [ 77 | { 78 | z: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 79 | colorscale: 'Jet', 80 | type: 'heatmap' 81 | } 82 | ]; 83 | 84 | Plotly.newPlot('data', data, layout, config); 85 | 86 | // callback function 87 | var intervalId = window.setInterval(function () { 88 | 89 | // check if timestamp is updated 90 | $.get(urlTimestamp, function () { }) 91 | 92 | .done(function (data) { 93 | if (timestamp != data) { 94 | timestamp = data; 95 | 96 | // get new data 97 | $.getJSON(urlTiming, function () { }) 98 | .done(function (data) { 99 | 100 | // case draw new plot 101 | if (data.nRows != nRows) { 102 | nRows = data.nRows; 103 | 104 | // timestamp posix to js 105 | for (i = 0; i < data["timestamp"].length; i++) 106 | { 107 | data["timestamp"][i] = new Date(data["timestamp"][i]); 108 | } 109 | 110 | data_trace = []; 111 | keys = Object.keys(data); 112 | keys = keys.filter(item => item !== "timestamp" && item !== "uptime_s" && item !== "uptime_days"); 113 | for (i = 0; i < keys.length; i++) { 114 | var trace = { 115 | x: data["timestamp"], 116 | y: data[keys[i]], 117 | mode: 'lines+markers', 118 | type: 'scatter', 119 | name: keys[i], 120 | line: { 121 | width: 5 122 | }, 123 | marker: { 124 | size: 12 125 | }, 126 | }; 127 | data_trace.push(trace); 128 | } 129 | 130 | Plotly.newPlot('data', data_trace, layout, config); 131 | } 132 | // case update plot 133 | else { 134 | // timestamp posix to js 135 | for (i = 0; i < data["timestamp"].length; i++) 136 | { 137 | data["timestamp"][i] = new Date(data["timestamp"][i]); 138 | } 139 | var xVec = []; 140 | var yVec = []; 141 | for (i = 0; i < keys.length; i++) { 142 | xVec.push(data["timestamp"]); 143 | yVec.push(data[keys[i]]); 144 | } 145 | var trace_update = { 146 | x: xVec, 147 | y: yVec 148 | }; 149 | Plotly.update('data', trace_update); 150 | } 151 | 152 | }) 153 | .fail(function () { 154 | }) 155 | .always(function () { 156 | }); 157 | } 158 | }) 159 | .fail(function () { 160 | }) 161 | .always(function () { 162 | }); 163 | }, 100); 164 | -------------------------------------------------------------------------------- /src/capture/kraken/Kraken.cpp: -------------------------------------------------------------------------------- 1 | #include "Kraken.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // constructor 9 | Kraken::Kraken(std::string _type, uint32_t _fc, uint32_t _fs, 10 | std::string _path, bool *_saveIq, std::vector _gain) 11 | : Source(_type, _fc, _fs, _path, _saveIq) 12 | { 13 | // convert gain to tenths of dB 14 | for (size_t i = 0; i < _gain.size(); i++) 15 | { 16 | gain.push_back(static_cast(_gain[i]*10)); 17 | channelIndex.push_back(i); 18 | } 19 | std::vector devs(channelIndex.size()); 20 | 21 | // store all valid gains 22 | std::vector validGains; 23 | int nGains, status; 24 | status = rtlsdr_open(&devs[0], 0); 25 | check_status(status, "Failed to open device for available gains."); 26 | nGains = rtlsdr_get_tuner_gains(devs[0], nullptr); 27 | check_status(nGains, "Failed to get number of gains."); 28 | std::unique_ptr _validGains(new int[nGains]); 29 | status = rtlsdr_get_tuner_gains(devs[0], _validGains.get()); 30 | check_status(status, "Failed to get number of gains."); 31 | validGains.assign(_validGains.get(), _validGains.get() + nGains); 32 | status = rtlsdr_close(devs[0]); 33 | check_status(status, "Failed to close device for available gains."); 34 | 35 | // update gains to next value if invalid 36 | for (size_t i = 0; i < _gain.size(); i++) 37 | { 38 | int adjustedGain = static_cast(_gain[i] * 10); 39 | auto it = std::lower_bound(validGains.begin(), 40 | validGains.end(), adjustedGain); 41 | if (it != validGains.end()) { 42 | gain.push_back(*it); 43 | } else { 44 | gain.push_back(validGains.back()); 45 | } 46 | std::cout << "[Kraken] Gain update on channel " << i << " from " << 47 | adjustedGain << " to " << gain[i] << "." << std::endl; 48 | } 49 | } 50 | 51 | void Kraken::start() 52 | { 53 | int status; 54 | for (size_t i = 0; i < channelIndex.size(); i++) 55 | { 56 | std::cout << "[Kraken] Setting up channel " << i << "." << std::endl; 57 | 58 | status = rtlsdr_open(&devs[i], i); 59 | check_status(status, "Failed to open device."); 60 | 61 | status = rtlsdr_set_center_freq(devs[i], fc); 62 | check_status(status, "Failed to set center frequency."); 63 | status = rtlsdr_set_sample_rate(devs[i], fs); 64 | check_status(status, "Failed to set sample rate."); 65 | status = rtlsdr_set_dithering(devs[i], 0); // disable dither 66 | check_status(status, "Failed to disable dithering."); 67 | status = rtlsdr_set_tuner_gain_mode(devs[i], 1); // disable AGC 68 | check_status(status, "Failed to disable AGC."); 69 | status = rtlsdr_set_tuner_gain(devs[i], gain[i]); 70 | check_status(status, "Failed to set gain."); 71 | status = rtlsdr_reset_buffer(devs[i]); 72 | check_status(status, "Failed to reset buffer."); 73 | } 74 | } 75 | 76 | void Kraken::stop() 77 | { 78 | int status; 79 | for (size_t i = 0; i < channelIndex.size(); i++) 80 | { 81 | status = rtlsdr_cancel_async(devs[i]); 82 | check_status(status, "Failed to stop async read."); 83 | } 84 | } 85 | 86 | void Kraken::process(IqData *buffer1, IqData *buffer2) 87 | { 88 | std::vector threads; 89 | threads.emplace_back(rtlsdr_read_async, devs[0], callback, buffer1, 0, 16 * 16384); 90 | threads.emplace_back(rtlsdr_read_async, devs[1], callback, buffer2, 0, 16 * 16384); 91 | // join threads 92 | for (auto& thread : threads) { 93 | thread.join(); 94 | } 95 | } 96 | 97 | void Kraken::callback(unsigned char *buf, uint32_t len, void *ctx) 98 | { 99 | IqData* buffer_blah2 = (IqData*)ctx; 100 | int8_t* buffer_kraken = (int8_t*)buf; 101 | 102 | buffer_blah2->lock(); 103 | 104 | for (size_t i = 0; i < len; i += 2) { 105 | double iqi = static_cast(buffer_kraken[i]); 106 | double iqq = static_cast(buffer_kraken[i + 1]); 107 | 108 | buffer_blah2->push_back({iqi, iqq}); 109 | } 110 | 111 | buffer_blah2->unlock(); 112 | } 113 | 114 | void Kraken::replay(IqData *buffer1, IqData *buffer2, std::string _file, bool _loop) 115 | { 116 | // todo 117 | } 118 | 119 | void Kraken::check_status(int status, std::string message) 120 | { 121 | if (status < 0) 122 | { 123 | throw std::runtime_error("[Kraken] " + message); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/capture/hackrf/HackRf.cpp: -------------------------------------------------------------------------------- 1 | #include "HackRf.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // constructor 9 | HackRf::HackRf(std::string _type, uint32_t _fc, uint32_t _fs, 10 | std::string _path, bool *_saveIq, std::vector _serial, 11 | std::vector _gainLna, std::vector _gainVga, 12 | std::vector _ampEnable) 13 | : Source(_type, _fc, _fs, _path, _saveIq) 14 | { 15 | serial = _serial; 16 | ampEnable = _ampEnable; 17 | 18 | // validate LNA gain 19 | std::unordered_set validLna; 20 | for (uint32_t gain = 0; gain <= 40; gain += 8) { 21 | validLna.insert(gain); 22 | } 23 | for (uint32_t gain : _gainLna) { 24 | if (validLna.find(gain) == validLna.end()) { 25 | throw std::invalid_argument("Invalid LNA gain value"); 26 | } 27 | } 28 | gainLna = _gainLna; 29 | 30 | // validate VGA gain 31 | std::unordered_set validVga; 32 | for (uint32_t gain = 0; gain <= 62; gain += 2) { 33 | validVga.insert(gain); 34 | } 35 | for (uint32_t gain : _gainVga) { 36 | if (validVga.find(gain) == validVga.end()) { 37 | throw std::invalid_argument("Invalid LNA gain value"); 38 | } 39 | } 40 | gainVga = _gainVga; 41 | } 42 | 43 | void HackRf::check_status(uint8_t status, std::string message) 44 | { 45 | if (status != HACKRF_SUCCESS) 46 | { 47 | throw std::runtime_error("[HackRF] " + message); 48 | } 49 | } 50 | 51 | void HackRf::start() 52 | { 53 | // global hackrf config 54 | int status; 55 | status = hackrf_init(); 56 | check_status(status, "Failed to initialise HackRF"); 57 | hackrf_device_list_t *list; 58 | list = hackrf_device_list(); 59 | if (!list || list->devicecount < 2) 60 | { 61 | check_status(-1, "Failed to find 2 HackRF devices."); 62 | } 63 | 64 | // surveillance config 65 | status = hackrf_open_by_serial(serial[1].c_str(), &dev[1]); 66 | check_status(status, "Failed to open device."); 67 | status = hackrf_set_freq(dev[1], fc); 68 | check_status(status, "Failed to set frequency."); 69 | status = hackrf_set_sample_rate(dev[1], fs); 70 | check_status(status, "Failed to set sample rate."); 71 | status = hackrf_set_amp_enable(dev[1], ampEnable[1] ? 1 : 0); 72 | check_status(status, "Failed to set AMP status."); 73 | status = hackrf_set_lna_gain(dev[1], gainLna[1]); 74 | check_status(status, "Failed to set LNA gain."); 75 | status = hackrf_set_vga_gain(dev[1], gainVga[1]); 76 | check_status(status, "Failed to set VGA gain."); 77 | status = hackrf_set_hw_sync_mode(dev[1], 1); 78 | check_status(status, "Failed to enable hardware synchronising."); 79 | status = hackrf_set_clkout_enable(dev[1], 1); 80 | check_status(status, "Failed to set CLKOUT on survillance device"); 81 | 82 | 83 | // reference config 84 | status = hackrf_open_by_serial(serial[0].c_str(), &dev[0]); 85 | check_status(status, "Failed to open device."); 86 | status = hackrf_set_freq(dev[0], fc); 87 | check_status(status, "Failed to set frequency."); 88 | status = hackrf_set_sample_rate(dev[0], fs); 89 | check_status(status, "Failed to set sample rate."); 90 | status = hackrf_set_amp_enable(dev[0], ampEnable[0] ? 1 : 0); 91 | check_status(status, "Failed to set AMP status."); 92 | status = hackrf_set_lna_gain(dev[0], gainLna[0]); 93 | check_status(status, "Failed to set LNA gain."); 94 | status = hackrf_set_vga_gain(dev[0], gainVga[0]); 95 | check_status(status, "Failed to set VGA gain."); 96 | } 97 | 98 | void HackRf::stop() 99 | { 100 | hackrf_stop_rx(dev[0]); 101 | hackrf_stop_rx(dev[1]); 102 | hackrf_close(dev[0]); 103 | hackrf_close(dev[1]); 104 | hackrf_exit(); 105 | } 106 | 107 | void HackRf::process(IqData *buffer1, IqData *buffer2) 108 | { 109 | int status; 110 | status = hackrf_start_rx(dev[1], rx_callback, buffer2); 111 | check_status(status, "Failed to start RX streaming."); 112 | status = hackrf_start_rx(dev[0], rx_callback, buffer1); 113 | check_status(status, "Failed to start RX streaming."); 114 | } 115 | 116 | int HackRf::rx_callback(hackrf_transfer* transfer) 117 | { 118 | IqData* buffer_blah2 = (IqData*)transfer->rx_ctx; 119 | int8_t* buffer_hackrf = (int8_t*) transfer->buffer; 120 | 121 | buffer_blah2->lock(); 122 | 123 | for (int i = 0; i < transfer->buffer_length; i=i+2) 124 | { 125 | double iqi = static_cast(buffer_hackrf[i]); 126 | double iqq = static_cast(buffer_hackrf[i+1]); 127 | buffer_blah2->push_back({iqi, iqq}); 128 | } 129 | 130 | buffer_blah2->unlock(); 131 | 132 | return 0; 133 | } 134 | 135 | void HackRf::replay(IqData *buffer1, IqData *buffer2, std::string _file, bool _loop) 136 | { 137 | return; 138 | } 139 | 140 | -------------------------------------------------------------------------------- /src/process/tracker/Tracker.cpp: -------------------------------------------------------------------------------- 1 | #include "Tracker.h" 2 | #include 3 | 4 | // constructor 5 | Tracker::Tracker(uint32_t _m, uint32_t _n, uint32_t _nDelete, 6 | double _cpi, double _maxAccInit, double _rangeRes, double _lambda) 7 | { 8 | m = _m; 9 | n = _n; 10 | nDelete = _nDelete; 11 | cpi = _cpi; 12 | maxAccInit = _maxAccInit; 13 | timestamp = 0; 14 | rangeRes = _rangeRes; 15 | lambda = _lambda; 16 | 17 | double resolutionAcc = 1/(cpi*cpi); 18 | uint16_t nAcc = (int)maxAccInit/resolutionAcc; 19 | for (int i = 0; i < 2*nAcc+1; i++) 20 | { 21 | accInit.push_back(resolutionAcc*(i-nAcc)); 22 | } 23 | 24 | Track track{}; 25 | } 26 | 27 | Tracker::~Tracker() 28 | { 29 | } 30 | 31 | std::unique_ptr Tracker::process(Detection *detection, uint64_t currentTime) 32 | { 33 | doNotInitiate.clear(); 34 | for (size_t i = 0; i < detection->get_nDetections(); i++) 35 | { 36 | doNotInitiate.push_back(false); 37 | } 38 | 39 | if (track.get_n() > 0) 40 | { 41 | update(detection, currentTime); 42 | } 43 | else 44 | { 45 | timestamp = currentTime; 46 | } 47 | initiate(detection); 48 | 49 | return std::make_unique(track); 50 | } 51 | 52 | void Tracker::update(Detection *detection, uint64_t current) 53 | { 54 | std::vector delay = detection->get_delay(); 55 | std::vector doppler = detection->get_doppler(); 56 | std::vector snr = detection->get_snr(); 57 | 58 | // init 59 | double delayPredict = 0.0; 60 | double dopplerPredict = 0.0; 61 | double acc = 0.0; 62 | uint32_t nRemove = 0; 63 | std::string state; 64 | 65 | // get time between detections 66 | double T = ((double)(current - timestamp))/1000; 67 | timestamp = current; 68 | 69 | // loop over each track 70 | for (uint64_t i = 0; i < track.get_n(); i++) 71 | { 72 | // predict next position 73 | Detection detectionCurrent = track.get_current(i); 74 | acc = track.get_acceleration(i); 75 | Detection prediction = predict(detectionCurrent, acc, T); 76 | 77 | // loop over detections to associate 78 | for (size_t j = 0; j < detection->get_nDetections(); j++) 79 | { 80 | // associate detections 81 | if (delay[j] > delayPredict-1 && 82 | delay[j] < delayPredict+1 && 83 | doppler[j] > dopplerPredict-1*(1/cpi) && 84 | doppler[j] < dopplerPredict+1*(1/cpi)) 85 | { 86 | Detection associated(delay[j], doppler[j], snr[j]); 87 | track.set_current(i, associated); 88 | track.set_acceleration(i, (doppler[j]-detectionCurrent.get_doppler().front())/T); 89 | track.set_nInactive(i, 0); 90 | doNotInitiate[j] = true; 91 | state = "ASSOCIATED"; 92 | track.set_state(i, state); 93 | // promote track if passes threshold 94 | track.promote(i, m, n); 95 | break; 96 | } 97 | } 98 | 99 | // update state if no detections associated 100 | track.set_current(i, prediction); 101 | if (track.get_state(i) == "ACTIVE") 102 | { 103 | state = "COASTING"; 104 | track.set_state(i, state); 105 | } 106 | else if (track.get_state(i) == "ASSOCIATED") 107 | { 108 | state = "TENTATIVE"; 109 | track.set_state(i, state); 110 | } 111 | else 112 | { 113 | track.set_state(i, track.get_state(i)); 114 | } 115 | track.set_nInactive(i, track.get_nInactive(i)+1); 116 | 117 | // remove if tentative or coasting too long 118 | if (track.get_nInactive(i) > nDelete) 119 | { 120 | track.remove(i-nRemove); 121 | nRemove++; 122 | } 123 | } 124 | } 125 | 126 | Detection Tracker::predict(Detection current, double acc, double T) 127 | { 128 | double delayTrack = current.get_delay().front(); 129 | double dopplerTrack = current.get_doppler().front(); 130 | double delayPredict = delayTrack+((dopplerTrack*T*lambda)+ 131 | (0.5*acc*T*T))/rangeRes; 132 | double dopplerPredict = dopplerTrack+(acc*T); 133 | Detection prediction(delayPredict, dopplerPredict, 0); 134 | return prediction; 135 | } 136 | 137 | void Tracker::initiate(Detection *detection) 138 | { 139 | std::vector delay = detection->get_delay(); 140 | std::vector doppler = detection->get_doppler(); 141 | std::vector snr = detection->get_snr(); 142 | uint64_t index; 143 | 144 | // loop over new detections 145 | for (size_t i = 0; i < detection->get_nDetections(); i++) 146 | { 147 | // skip if detection used in update 148 | if (doNotInitiate.at(i)) 149 | { 150 | continue; 151 | } 152 | // add tentative detection for each acc 153 | for (size_t j = 0; j < accInit.size(); j++) 154 | { 155 | Detection detectionCurrent(delay[i], doppler[i], snr[i]); 156 | index = track.add(detectionCurrent); 157 | track.set_acceleration(index, accInit[j]); 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /src/capture/Capture.cpp: -------------------------------------------------------------------------------- 1 | #include "Capture.h" 2 | #include "rspduo/RspDuo.h" 3 | #include "usrp/Usrp.h" 4 | #include "hackrf/HackRf.h" 5 | #include "kraken/Kraken.h" 6 | #include 7 | #include 8 | #include 9 | 10 | // constants 11 | const std::string Capture::VALID_TYPE[4] = {"RspDuo", "Usrp", "HackRF", "Kraken"}; 12 | 13 | // constructor 14 | Capture::Capture(std::string _type, uint32_t _fs, uint32_t _fc, std::string _path) 15 | { 16 | type = _type; 17 | fs = _fs; 18 | fc = _fc; 19 | path = _path; 20 | replay = false; 21 | saveIq = false; 22 | } 23 | 24 | void Capture::process(IqData *buffer1, IqData *buffer2, c4::yml::NodeRef config, 25 | std::string ip_capture, uint16_t port_capture) 26 | { 27 | std::cout << "Setting up device " + type << std::endl; 28 | 29 | device = factory_source(type, config); 30 | 31 | // capture status thread 32 | std::thread t1([&]{ 33 | while (true) 34 | { 35 | httplib::Client cli("http://" + ip_capture + ":" 36 | + std::to_string(port_capture)); 37 | httplib::Result res = cli.Get("/capture"); 38 | 39 | // if capture status changed 40 | if ((res->body == "true") != saveIq) 41 | { 42 | saveIq = res->body == "true"; 43 | if (saveIq) 44 | { 45 | device->open_file(); 46 | } 47 | else 48 | { 49 | device->close_file(); 50 | } 51 | } 52 | sleep(1); 53 | } 54 | }); 55 | 56 | if (!replay) 57 | { 58 | device->start(); 59 | device->process(buffer1, buffer2); 60 | } 61 | else 62 | { 63 | device->replay(buffer1, buffer2, file, loop); 64 | } 65 | t1.join(); 66 | } 67 | 68 | std::unique_ptr Capture::factory_source(const std::string& type, c4::yml::NodeRef config) 69 | { 70 | // SDRplay RSPduo 71 | if (type == VALID_TYPE[0]) 72 | { 73 | int agcSetPoint, bandwidthNumber, gainReductionA, gainReductionB, lnaState; 74 | bool dabNotch, rfNotch; 75 | config["agcSetPoint"] >> agcSetPoint; 76 | config["bandwidthNumber"] >> bandwidthNumber; 77 | config["gainReduction"][0] >> gainReductionA; 78 | config["gainReduction"][1] >> gainReductionB; 79 | config["lnaState"] >> lnaState; 80 | config["dabNotch"] >> dabNotch; 81 | config["rfNotch"] >> rfNotch; 82 | return std::make_unique(type, fc, fs, path, &saveIq, 83 | agcSetPoint, bandwidthNumber, gainReductionA, gainReductionB, lnaState, 84 | dabNotch, rfNotch); 85 | } 86 | // Usrp 87 | else if (type == VALID_TYPE[1]) 88 | { 89 | std::string address, subdev; 90 | std::vector antenna; 91 | std::vector gain; 92 | std::string _antenna; 93 | double _gain; 94 | config["address"] >> address; 95 | config["subdev"] >> subdev; 96 | config["antenna"][0] >> _antenna; 97 | antenna.push_back(_antenna); 98 | config["antenna"][1] >> _antenna; 99 | antenna.push_back(_antenna); 100 | config["gain"][0] >> _gain; 101 | gain.push_back(_gain); 102 | config["gain"][1] >> _gain; 103 | gain.push_back(_gain); 104 | return std::make_unique(type, fc, fs, path, &saveIq, 105 | address, subdev, antenna, gain); 106 | } 107 | // HackRF 108 | else if (type == VALID_TYPE[2]) 109 | { 110 | std::vector serial; 111 | std::vector gainLna, gainVga; 112 | std::vector ampEnable; 113 | std::string _serial; 114 | uint32_t gain; 115 | int _gain; 116 | bool _ampEnable; 117 | config["serial"][0] >> _serial; 118 | serial.push_back(_serial); 119 | config["serial"][1] >> _serial; 120 | serial.push_back(_serial); 121 | config["gain_lna"][0] >> _gain; 122 | gain = static_cast (_gain); 123 | gainLna.push_back(gain); 124 | config["gain_lna"][1] >> _gain; 125 | gain = static_cast(_gain); 126 | gainLna.push_back(gain); 127 | config["gain_vga"][0] >> _gain; 128 | gain = static_cast(_gain); 129 | gainVga.push_back(gain); 130 | config["gain_vga"][1] >> _gain; 131 | gain = static_cast(_gain); 132 | gainVga.push_back(gain); 133 | config["amp_enable"][0] >> _ampEnable; 134 | ampEnable.push_back(_ampEnable); 135 | config["amp_enable"][1] >> _ampEnable; 136 | ampEnable.push_back(_ampEnable); 137 | return std::make_unique(type, fc, fs, path, &saveIq, 138 | serial, gainLna, gainVga, ampEnable); 139 | } 140 | // Kraken 141 | else if (type == VALID_TYPE[3]) 142 | { 143 | std::vector gain; 144 | float _gain; 145 | for (auto child : config["gain"].children()) 146 | { 147 | c4::atof(child.val(), &_gain); 148 | gain.push_back(static_cast(_gain)); 149 | } 150 | return std::make_unique(type, fc, fs, path, &saveIq, gain); 151 | } 152 | // handle unknown type 153 | std::cerr << "Error: Source type does not exist." << std::endl; 154 | return nullptr; 155 | } 156 | 157 | void Capture::set_replay(bool _loop, std::string _file) 158 | { 159 | replay = true; 160 | loop = _loop; 161 | file = _file; 162 | } 163 | -------------------------------------------------------------------------------- /src/process/clutter/WienerHopf.cpp: -------------------------------------------------------------------------------- 1 | #include "WienerHopf.h" 2 | #include 3 | #include 4 | #include 5 | 6 | // constructor 7 | WienerHopf::WienerHopf(int32_t _delayMin, int32_t _delayMax, uint32_t _nSamples) 8 | { 9 | // input 10 | delayMin = _delayMin; 11 | delayMax = _delayMax; 12 | nBins = delayMax - delayMin; 13 | nSamples = _nSamples; 14 | 15 | // initialise data 16 | A = arma::cx_mat(nBins, nBins); 17 | a = arma::cx_vec(nBins); 18 | b = arma::cx_vec(nBins); 19 | w = arma::cx_vec(nBins); 20 | 21 | // compute FFTW plans in constructor 22 | dataX = new std::complex[nSamples]; 23 | dataY = new std::complex[nSamples]; 24 | dataOutX = new std::complex[nSamples]; 25 | dataOutY = new std::complex[nSamples]; 26 | dataA = new std::complex[nSamples]; 27 | dataB = new std::complex[nSamples]; 28 | filtX = new std::complex[nBins + nSamples + 1]; 29 | filtW = new std::complex[nBins + nSamples + 1]; 30 | filt = new std::complex[nBins + nSamples + 1]; 31 | fftX = fftw_plan_dft_1d(nSamples, reinterpret_cast(dataX), 32 | reinterpret_cast(dataOutX), FFTW_FORWARD, FFTW_ESTIMATE); 33 | fftY = fftw_plan_dft_1d(nSamples, reinterpret_cast(dataY), 34 | reinterpret_cast(dataOutY), FFTW_FORWARD, FFTW_ESTIMATE); 35 | fftA = fftw_plan_dft_1d(nSamples, reinterpret_cast(dataA), 36 | reinterpret_cast(dataA), FFTW_BACKWARD, FFTW_ESTIMATE); 37 | fftB = fftw_plan_dft_1d(nSamples, reinterpret_cast(dataB), 38 | reinterpret_cast(dataB), FFTW_BACKWARD, FFTW_ESTIMATE); 39 | fftFiltX = fftw_plan_dft_1d(nBins + nSamples + 1, reinterpret_cast(filtX), 40 | reinterpret_cast(filtX), FFTW_FORWARD, FFTW_ESTIMATE); 41 | fftFiltW = fftw_plan_dft_1d(nBins + nSamples + 1, reinterpret_cast(filtW), 42 | reinterpret_cast(filtW), FFTW_FORWARD, FFTW_ESTIMATE); 43 | fftFilt = fftw_plan_dft_1d(nBins + nSamples + 1, reinterpret_cast(filt), 44 | reinterpret_cast(filt), FFTW_BACKWARD, FFTW_ESTIMATE); 45 | } 46 | 47 | WienerHopf::~WienerHopf() 48 | { 49 | fftw_destroy_plan(fftX); 50 | fftw_destroy_plan(fftY); 51 | fftw_destroy_plan(fftA); 52 | fftw_destroy_plan(fftB); 53 | fftw_destroy_plan(fftFiltX); 54 | fftw_destroy_plan(fftFiltW); 55 | fftw_destroy_plan(fftFilt); 56 | } 57 | 58 | bool WienerHopf::process(IqData *x, IqData *y) 59 | { 60 | uint32_t i, j; 61 | xData = x->get_data(); 62 | yData = y->get_data(); 63 | 64 | // change deque to std::complex 65 | for (i = 0; i < nSamples; i++) 66 | { 67 | dataX[i] = xData[(((i - delayMin) % nSamples) + nSamples) % nSamples]; 68 | dataY[i] = yData[i]; 69 | } 70 | 71 | // pre-compute FFT of signals 72 | fftw_execute(fftX); 73 | fftw_execute(fftY); 74 | 75 | // auto-correlation matrix A 76 | for (i = 0; i < nSamples; i++) 77 | { 78 | dataA[i] = (dataOutX[i] * std::conj(dataOutX[i])); 79 | } 80 | fftw_execute(fftA); 81 | for (i = 0; i < nBins; i++) 82 | { 83 | a[i] = std::conj(dataA[i]) / (double)nSamples; 84 | } 85 | A = arma::toeplitz(a); 86 | 87 | // conjugate upper diagonal as arma does not 88 | for (i = 0; i < nBins; i++) 89 | { 90 | for (j = 0; j < nBins; j++) 91 | { 92 | if (i > j) 93 | { 94 | A(i, j) = std::conj(A(i, j)); 95 | } 96 | } 97 | } 98 | 99 | // cross-correlation vector b 100 | for (i = 0; i < nSamples; i++) 101 | { 102 | dataB[i] = (dataOutY[i] * std::conj(dataOutX[i])); 103 | } 104 | fftw_execute(fftB); 105 | for (i = 0; i < nBins; i++) 106 | { 107 | b[i] = dataB[i] / (double)nSamples; 108 | } 109 | 110 | // compute weights 111 | success = arma::chol(A, A); 112 | if (!success) 113 | { 114 | std::cerr << "Chol decomposition failed, skip clutter filter" << std::endl; 115 | return false; 116 | } 117 | success = arma::solve(w, arma::trimatu(A), arma::solve(arma::trimatl(arma::trans(A)), b)); 118 | if (!success) 119 | { 120 | std::cerr << "Solve failed, skip clutter filter" << std::endl; 121 | return false; 122 | } 123 | 124 | // assign and pad x 125 | for (i = 0; i < nSamples; i++) 126 | { 127 | filtX[i] = dataX[i]; 128 | } 129 | for (i = nSamples; i < nBins + nSamples + 1; i++) 130 | { 131 | filtX[i] = {0, 0}; 132 | } 133 | 134 | // assign and pad w 135 | for (i = 0; i < nBins; i++) 136 | { 137 | filtW[i] = w[i]; 138 | } 139 | for (i = nBins; i < nBins + nSamples + 1; i++) 140 | { 141 | filtW[i] = {0, 0}; 142 | } 143 | 144 | // compute fft 145 | fftw_execute(fftFiltX); 146 | fftw_execute(fftFiltW); 147 | 148 | // compute convolution/filter 149 | for (i = 0; i < nBins + nSamples + 1; i++) 150 | { 151 | filt[i] = (filtW[i] * filtX[i]); 152 | } 153 | fftw_execute(fftFilt); 154 | 155 | // update surveillance signal 156 | y->clear(); 157 | for (i = 0; i < nSamples; i++) 158 | { 159 | y->push_back(dataY[i] - (filt[i] / (double)(nBins + nSamples + 1))); 160 | } 161 | 162 | return true; 163 | } -------------------------------------------------------------------------------- /src/data/Track.h: -------------------------------------------------------------------------------- 1 | /// @file Track.h 2 | /// @class Track 3 | /// @brief A class to store track data. 4 | /// @details The ID is 4 digit hexadecimal with 16^4 = 65536 combinations. 5 | /// @details The state can be TENTATIVE, ASSOCIATED, ACTIVE or COASTING. 6 | /// @details - TENTATIVE when a track is initialised. 7 | /// @details - TENTATIVE when an ASSOCIATED track fails to associate. 8 | /// @details - ASSOCIATED when a TENTATIVE track has an associated detection. 9 | /// @details - ACTIVE when track passes the promotion threshold. 10 | /// @details - COASTING when an ACTIVE track fails to associate. 11 | /// @details Current track is used for smoothing output. 12 | /// @author 30hours 13 | /// @todo I feel promote() should be implemented in the tracker. 14 | 15 | #ifndef TRACK_H 16 | #define TRACK_H 17 | 18 | #include "data/Detection.h" 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | class Track 26 | { 27 | private: 28 | /// @brief Track ID (4 digit alpha-numeric). 29 | std::vector id; 30 | 31 | /// @brief State history for each track. 32 | std::vector> state; 33 | 34 | /// @brief Curent track position. 35 | std::vector current; 36 | 37 | /// @brief Current acceleration (Hz/s). 38 | std::vector acceleration; 39 | 40 | /// @brief Associated detections in track. 41 | std::vector> associated; 42 | 43 | /// @brief Number of updates the track has been tentative/coasting. 44 | /// @details Forms criteria for track deletion. 45 | std::vector nInactive; 46 | 47 | /// @brief Next valid track index. 48 | uint64_t iNext; 49 | 50 | /// @brief Maximum integer index to wrap around. 51 | static const uint64_t MAX_INDEX; 52 | 53 | /// @brief String for state ACTIVE. 54 | static const std::string STATE_ACTIVE; 55 | 56 | /// @brief String for state TENTATIVE. 57 | static const std::string STATE_TENTATIVE; 58 | 59 | /// @brief String for state COASTING. 60 | static const std::string STATE_COASTING; 61 | 62 | /// @brief String for state ASSOCIATED. 63 | static const std::string STATE_ASSOCIATED; 64 | 65 | public: 66 | /// @brief Constructor. 67 | /// @return The object. 68 | Track(); 69 | 70 | /// @brief Destructor. 71 | /// @return Void. 72 | ~Track(); 73 | 74 | /// @brief Convert an unsigned int to hexadecimal. 75 | /// @details Max number is 16^4 = 65536 before wrap around. 76 | /// @param number Number to convert to hexadecimal. 77 | /// @return hex Hexadecimal number. 78 | std::string uint2hex(uint64_t number); 79 | 80 | /// @brief Set the state of the latest tracklet. 81 | /// @param index Index of track to change. 82 | /// @param state Updated state. 83 | /// @return Void. 84 | void set_state(uint64_t index, std::string state); 85 | 86 | /// @brief Set the current track position. 87 | /// @details Use to update smoothed current position. 88 | /// @param index Index of track to change. 89 | /// @param smoothed Updated state. 90 | /// @return Void. 91 | void set_current(uint64_t index, Detection smoothed); 92 | 93 | /// @brief Set the current acceleration. 94 | /// @param index Index of track to change. 95 | /// @param acceleration Updated acceleration. 96 | /// @return Void. 97 | void set_acceleration(uint64_t index, double acceleration); 98 | 99 | /// @brief Set the current inactivity. 100 | /// @param index Index of track to change. 101 | /// @param n Updated inactivity index. 102 | /// @return Void. 103 | void set_nInactive(uint64_t index, uint64_t n); 104 | 105 | /// @brief Get number of tracks with specified state. 106 | /// @param state State to check. 107 | /// @return Number of tracks with specified state. 108 | uint64_t get_nState(std::string state); 109 | 110 | /// @brief Get number of total tracks. 111 | /// @return Number of total tracks. 112 | uint64_t get_n(); 113 | 114 | /// @brief Get current track position for track index. 115 | /// @return Current detection. 116 | Detection get_current(uint64_t index); 117 | 118 | /// @brief Get current acceleration for track index. 119 | /// @return Current acceleration (Hz/s). 120 | double get_acceleration(uint64_t index); 121 | 122 | /// @brief Get current state for track index. 123 | /// @return Current state. 124 | std::string get_state(uint64_t index); 125 | 126 | /// @brief Get number of updates track has been tentative/coasting. 127 | /// @return Number of updates track has been tentative/coasting. 128 | uint64_t get_nInactive(uint64_t index); 129 | 130 | /// @brief Update an associated detection. 131 | /// @param index Index of track to change. 132 | /// @param update New associated detection. 133 | /// @return Void. 134 | void update(uint64_t index, Detection update); 135 | 136 | /// @brief Add track to the track set. 137 | /// @param initial Initial Detection. 138 | /// @details ID is incremented automatically. 139 | /// @details Initial state is always TENTATIVE. 140 | /// @return Index of last track. 141 | uint64_t add(Detection initial); 142 | 143 | /// @brief Promote track to state ACTIVE if applicable. 144 | /// @details Uses M of N rule for ACTIVE tracks. 145 | /// @param index Index of track to change. 146 | /// @return Void. 147 | void promote(uint64_t index, uint32_t m, uint32_t n); 148 | 149 | /// @brief Remove track based on index. 150 | /// @param index Index of track to remove. 151 | /// @return Void. 152 | void remove(uint64_t index); 153 | 154 | /// @brief Generate JSON of the map and metadata. 155 | /// @param timestamp Current time (POSIX ms). 156 | /// @return JSON string. 157 | std::string to_json(uint64_t timestamp); 158 | 159 | /// @brief Append the map to a save file. 160 | /// @param json JSON string of map and metadata. 161 | /// @param path Path of file to save. 162 | /// @return True is save is successful. 163 | bool save(std::string json, std::string path); 164 | }; 165 | 166 | #endif 167 | -------------------------------------------------------------------------------- /test/unit/process/ambiguity/TestAmbiguity.cpp: -------------------------------------------------------------------------------- 1 | /// @file TestAmbiguity.cpp 2 | /// @brief Unit test for Ambiguity.cpp 3 | /// @author 30hours 4 | /// @author Dan G 5 | /// @todo Add golden data IqData file for testing. 6 | /// @todo Declaration match to coding style? 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "process/ambiguity/Ambiguity.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | /// @brief Use random_device as RNG. 19 | std::random_device g_rd; 20 | 21 | /// @brief Generate random IQ data. 22 | /// @param iqData Address of IqData object. 23 | /// @details Have to use out ref parameter because there's no copy/move ctors. 24 | /// @return Void. 25 | void random_iq(IqData& iq_data) { 26 | std::mt19937 gen(g_rd()); 27 | std::uniform_real_distribution<> dist(-100.0, 100.0); 28 | 29 | for (uint32_t i = 0; i < iq_data.get_n(); ++i) { 30 | iq_data.push_back({dist(gen), dist(gen)}); 31 | } 32 | } 33 | 34 | /// @brief Read file to IqData buffer. 35 | /// @param buffer1 IqData buffer reference. 36 | /// @param buffer2 IqData buffer surveillance. 37 | /// @param file String of file name. 38 | /// @return Void. 39 | void read_file(IqData& buffer1, IqData& buffer2, const std::string& file) 40 | { 41 | short i1, q1, i2, q2; 42 | auto file_replay = fopen(file.c_str(), "rb"); 43 | if (!file_replay) { 44 | return; 45 | } 46 | 47 | auto read_short = [](short& v, FILE* fid) { 48 | auto rv{fread(&v, 1, sizeof(short), fid)}; 49 | return rv == sizeof(short); 50 | }; 51 | 52 | while (!feof(file_replay)) 53 | { 54 | if (!read_short(i1, file_replay)) break; 55 | if (!read_short(q1, file_replay)) break; 56 | if (!read_short(i2, file_replay)) break; 57 | if (!read_short(q2, file_replay)) break; 58 | 59 | buffer1.push_back({(double)i1, (double)q1}); 60 | buffer2.push_back({(double)i2, (double)q2}); 61 | 62 | // only read for the buffer length - this class is very poorly designed 63 | if (buffer1.get_length() == buffer1.get_n()) { 64 | break; 65 | } 66 | } 67 | 68 | fclose(file_replay); 69 | } 70 | 71 | /// @brief Test constructor. 72 | /// @details Check constructor parameters created correctly. 73 | TEST_CASE("Constructor", "[constructor]") 74 | { 75 | int32_t delayMin{-10}; 76 | int32_t delayMax{300}; 77 | int32_t dopplerMin{-300}; 78 | int32_t dopplerMax{300}; 79 | 80 | uint32_t fs{2'000'000}; 81 | float tCpi{0.5}; 82 | uint32_t nSamples = tCpi * fs; // narrow on purpose 83 | 84 | Ambiguity ambiguity(delayMin, delayMax, dopplerMin, 85 | dopplerMax, fs, nSamples); 86 | 87 | CHECK_THAT(ambiguity.get_cpi(), Catch::Matchers::WithinAbs(tCpi, 0.02)); 88 | CHECK(ambiguity.get_doppler_middle() == 0); 89 | CHECK(ambiguity.get_n_corr() == 3322); 90 | CHECK(ambiguity.get_n_delay_bins() == delayMax + std::abs(delayMin) + 1); 91 | CHECK(ambiguity.get_n_doppler_bins() == 301); 92 | CHECK(ambiguity.get_nfft() == 6643); 93 | } 94 | 95 | /// @brief Test constructor with rounded Hamming number FFT length. 96 | TEST_CASE("Constructor_Round", "[constructor]") 97 | { 98 | int32_t delayMin{-10}; 99 | int32_t delayMax{300}; 100 | int32_t dopplerMin{-300}; 101 | int32_t dopplerMax{300}; 102 | 103 | uint32_t fs{2'000'000}; 104 | float tCpi{0.5}; 105 | uint32_t nSamples = tCpi * fs; // narrow on purpose 106 | 107 | Ambiguity ambiguity(delayMin, delayMax, dopplerMin, 108 | dopplerMax, fs, nSamples, true); 109 | 110 | CHECK_THAT(ambiguity.get_cpi(), Catch::Matchers::WithinAbs(tCpi, 0.02)); 111 | CHECK(ambiguity.get_doppler_middle() == 0); 112 | CHECK(ambiguity.get_n_corr() == 3322); 113 | CHECK(ambiguity.get_n_delay_bins() == delayMax + std::abs(delayMin) + 1); 114 | CHECK(ambiguity.get_n_doppler_bins() == 301); 115 | CHECK(ambiguity.get_nfft() == 6750); 116 | } 117 | 118 | /// @brief Test simple ambiguity processing. 119 | TEST_CASE("Process_Simple", "[process]") 120 | { 121 | auto round_hamming = GENERATE(true, false); 122 | 123 | int32_t delayMin{-10}; 124 | int32_t delayMax{300}; 125 | int32_t dopplerMin{-300}; 126 | int32_t dopplerMax{300}; 127 | 128 | uint32_t fs{2'000'000}; 129 | float tCpi{0.5}; 130 | uint32_t nSamples = tCpi * fs; // narrow on purpose 131 | 132 | Ambiguity ambiguity(delayMin, delayMax, dopplerMin, 133 | dopplerMax, fs, nSamples, round_hamming); 134 | 135 | IqData x{nSamples}; 136 | IqData y{nSamples}; 137 | 138 | random_iq(x); 139 | random_iq(y); 140 | auto map{ambiguity.process(&x, &y)}; 141 | map->set_metrics(); 142 | CHECK(map->maxPower > 0.0); 143 | CHECK(map->noisePower > 0.0); 144 | } 145 | 146 | /// @brief Test processing from a file. 147 | TEST_CASE("Process_File", "[process]") 148 | { 149 | std::filesystem::path test_input_file("20231214-230611.rspduo"); 150 | // Bail if the test file doesn't exist 151 | if (!std::filesystem::exists(test_input_file)) { 152 | SKIP("Input test file does not exist."); 153 | } 154 | 155 | auto round_hamming = GENERATE(true, false); 156 | 157 | int32_t delayMin{-10}; 158 | int32_t delayMax{300}; 159 | int32_t dopplerMin{-300}; 160 | int32_t dopplerMax{300}; 161 | 162 | uint32_t fs{2'000'000}; 163 | float tCpi{0.5}; 164 | uint32_t nSamples = tCpi * fs; // narrow on purpose 165 | 166 | Ambiguity ambiguity(delayMin, delayMax, dopplerMin, 167 | dopplerMax, fs, nSamples, round_hamming); 168 | IqData x{nSamples}; 169 | IqData y{nSamples}; 170 | 171 | read_file(x, y, "20231214-230611.rspduo"); 172 | REQUIRE(x.get_length() == x.get_n()); 173 | 174 | auto map{ambiguity.process(&x ,&y)}; 175 | map->set_metrics(); 176 | CHECK_THAT(map->maxPower, Catch::Matchers::WithinAbs(30.2816, 0.001)); 177 | CHECK_THAT(map->noisePower, Catch::Matchers::WithinAbs(76.918, 0.001)); 178 | } 179 | -------------------------------------------------------------------------------- /src/process/ambiguity/Ambiguity.cpp: -------------------------------------------------------------------------------- 1 | #include "Ambiguity.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // constructor 11 | Ambiguity::Ambiguity(int32_t _delayMin, int32_t _delayMax, 12 | int32_t _dopplerMin, int32_t _dopplerMax, uint32_t _fs, 13 | uint32_t _n, bool _roundHamming) 14 | { 15 | // init 16 | delayMin = _delayMin; 17 | delayMax = _delayMax; 18 | dopplerMin = _dopplerMin; 19 | dopplerMax = _dopplerMax; 20 | fs = _fs; 21 | nSamples = _n; 22 | nDelayBins = static_cast(_delayMax - _delayMin + 1); 23 | dopplerMiddle = (_dopplerMin + _dopplerMax) / 2.0; 24 | 25 | // doppler calculations 26 | std::deque doppler; 27 | double resolutionDoppler = 1.0 / (static_cast(_n) / static_cast(_fs)); 28 | doppler.push_back(dopplerMiddle); 29 | int i = 1; 30 | while (dopplerMiddle + (i * resolutionDoppler) <= dopplerMax) 31 | { 32 | doppler.push_back(dopplerMiddle + (i * resolutionDoppler)); 33 | doppler.push_front(dopplerMiddle - (i * resolutionDoppler)); 34 | i++; 35 | } 36 | nDopplerBins = doppler.size(); 37 | 38 | // batches constants 39 | nCorr = _n / nDopplerBins; 40 | cpi = (static_cast(nCorr) * nDopplerBins) / fs; 41 | 42 | // update doppler bins to true cpi time 43 | resolutionDoppler = 1.0 / cpi; 44 | 45 | // create ambiguity map 46 | map = std::make_unique>(nDopplerBins, nDelayBins); 47 | 48 | // delay calculations 49 | map->delay.resize(nDelayBins); 50 | std::iota(map->delay.begin(), map->delay.end(), delayMin); 51 | 52 | map->doppler.push_front(dopplerMiddle); 53 | i = 1; 54 | while (map->doppler.size() < nDopplerBins) 55 | { 56 | map->doppler.push_back(dopplerMiddle + (i * resolutionDoppler)); 57 | map->doppler.push_front(dopplerMiddle - (i * resolutionDoppler)); 58 | i++; 59 | } 60 | 61 | // other setup 62 | nfft = 2 * nCorr - 1; 63 | if (_roundHamming) { 64 | nfft = next_hamming(nfft); 65 | } 66 | dataCorr.resize(2 * nDelayBins + 1); 67 | 68 | // compute FFTW plans in constructor 69 | dataXi.resize(nfft); 70 | dataYi.resize(nfft); 71 | dataZi.resize(nfft); 72 | dataDoppler.resize(nfft); 73 | fftXi = fftw_plan_dft_1d(nfft, reinterpret_cast(dataXi.data()), 74 | reinterpret_cast(dataXi.data()), FFTW_FORWARD, FFTW_ESTIMATE); 75 | fftYi = fftw_plan_dft_1d(nfft, reinterpret_cast(dataYi.data()), 76 | reinterpret_cast(dataYi.data()), FFTW_FORWARD, FFTW_ESTIMATE); 77 | fftZi = fftw_plan_dft_1d(nfft, reinterpret_cast(dataZi.data()), 78 | reinterpret_cast(dataZi.data()), FFTW_BACKWARD, FFTW_ESTIMATE); 79 | fftDoppler = fftw_plan_dft_1d(nDopplerBins, reinterpret_cast(dataDoppler.data()), 80 | reinterpret_cast(dataDoppler.data()), FFTW_FORWARD, FFTW_ESTIMATE); 81 | 82 | } 83 | 84 | Ambiguity::~Ambiguity() 85 | { 86 | fftw_destroy_plan(fftXi); 87 | fftw_destroy_plan(fftYi); 88 | fftw_destroy_plan(fftZi); 89 | fftw_destroy_plan(fftDoppler); 90 | } 91 | 92 | Map> *Ambiguity::process(IqData *x, IqData *y) 93 | { 94 | // shift reference if not 0 centered 95 | if (dopplerMiddle != 0) 96 | { 97 | std::complex j = {0, 1}; 98 | for (uint32_t i = 0; i < x->get_length(); i++) 99 | { 100 | x->push_back(x->pop_front() * std::exp(1.0 * j * 2.0 * M_PI * dopplerMiddle * ((double)i / fs))); 101 | } 102 | } 103 | 104 | // range processing 105 | nSamples = nDopplerBins * nCorr; 106 | for (uint16_t i = 0; i < nDopplerBins; i++) 107 | { 108 | for (uint16_t j = 0; j < nCorr; j++) 109 | { 110 | dataXi[j] = x->pop_front(); 111 | dataYi[j] = y->pop_front(); 112 | } 113 | 114 | for (uint16_t j = nCorr; j < nfft; j++) 115 | { 116 | dataXi[j] = {0, 0}; 117 | dataYi[j] = {0, 0}; 118 | } 119 | 120 | fftw_execute(fftXi); 121 | fftw_execute(fftYi); 122 | 123 | // compute correlation 124 | for (uint32_t j = 0; j < nfft; j++) 125 | { 126 | dataZi[j] = (dataYi[j] * std::conj(dataXi[j])) / (double)nfft; 127 | } 128 | 129 | fftw_execute(fftZi); 130 | 131 | // extract center of corr 132 | for (uint16_t j = 0; j < nDelayBins; j++) 133 | { 134 | dataCorr[j] = dataZi[nfft - nDelayBins + j]; 135 | } 136 | for (uint16_t j = 0; j < nDelayBins + 1; j++) 137 | { 138 | dataCorr[j + nDelayBins] = dataZi[j]; 139 | } 140 | 141 | // cast from std::complex to std::vector 142 | corr.clear(); 143 | for (uint16_t j = 0; j < nDelayBins; j++) 144 | { 145 | corr.push_back(dataCorr[nDelayBins + delayMin + j - 1 + 1]); 146 | } 147 | 148 | map->set_row(i, corr); 149 | } 150 | 151 | // doppler processing 152 | for (uint16_t i = 0; i < nDelayBins; i++) 153 | { 154 | delayProfile = map->get_col(i); 155 | for (uint16_t j = 0; j < nDopplerBins; j++) 156 | { 157 | dataDoppler[j] = {delayProfile[j].real(), delayProfile[j].imag()}; 158 | } 159 | 160 | fftw_execute(fftDoppler); 161 | 162 | corr.clear(); 163 | for (uint16_t j = 0; j < nDopplerBins; j++) 164 | { 165 | corr.push_back(dataDoppler[(j + int(nDopplerBins / 2) + 1) % nDopplerBins]); 166 | } 167 | 168 | map->set_col(i, corr); 169 | } 170 | 171 | return map.get(); 172 | } 173 | 174 | double Ambiguity::get_doppler_middle() const { 175 | return dopplerMiddle; 176 | } 177 | 178 | uint16_t Ambiguity::get_n_delay_bins() const { 179 | return nDelayBins; 180 | } 181 | 182 | uint16_t Ambiguity::get_n_doppler_bins() const { 183 | return nDopplerBins; 184 | } 185 | 186 | uint16_t Ambiguity::get_n_corr() const { 187 | return nCorr; 188 | } 189 | 190 | double Ambiguity::get_cpi() const { 191 | return cpi; 192 | } 193 | 194 | uint32_t Ambiguity::get_nfft() const { 195 | return nfft; 196 | } 197 | 198 | uint32_t Ambiguity::get_n_samples() const { 199 | return nSamples; 200 | } -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const net = require("net"); 3 | const fs = require('fs'); 4 | const yaml = require('js-yaml'); 5 | const dns = require('dns'); 6 | 7 | // parse config file 8 | var config; 9 | try { 10 | const file = process.argv[2]; 11 | config = yaml.load(fs.readFileSync(file, 'utf8')); 12 | } catch (e) { 13 | console.error('Error reading or parsing the YAML file:', e); 14 | } 15 | 16 | var stash_map = require('./stash/maxhold.js'); 17 | var stash_detection = require('./stash/detection.js'); 18 | var stash_iqdata = require('./stash/iqdata.js'); 19 | var stash_timing = require('./stash/timing.js'); 20 | 21 | // constants 22 | const PORT = config.network.ports.api; 23 | const HOST = config.network.ip; 24 | var map = ''; 25 | var detection = ''; 26 | var track = ''; 27 | var timestamp = ''; 28 | var timing = ''; 29 | var iqdata = ''; 30 | var data_map; 31 | var data_detection; 32 | var data_tracker; 33 | var data_timestamp; 34 | var data_timing; 35 | var data_iqdata; 36 | var capture = false; 37 | 38 | // api server 39 | const app = express(); 40 | // header on all requests 41 | app.use(function(req, res, next) { 42 | res.header("Access-Control-Allow-Origin", "*"); 43 | res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); 44 | res.header('Expires', '-1'); 45 | res.header('Pragma', 'no-cache'); 46 | next(); 47 | }); 48 | app.get('/', (req, res) => { 49 | res.send('Hello World'); 50 | }); 51 | app.get('/api/map', (req, res) => { 52 | res.send(map); 53 | }); 54 | app.get('/api/detection', (req, res) => { 55 | res.send(detection); 56 | }); 57 | app.get('/api/tracker', (req, res) => { 58 | res.send(track); 59 | }); 60 | app.get('/api/timestamp', (req, res) => { 61 | res.send(timestamp); 62 | }); 63 | app.get('/api/timing', (req, res) => { 64 | res.send(timing); 65 | }); 66 | app.get('/api/iqdata', (req, res) => { 67 | res.send(iqdata); 68 | }); 69 | app.get('/api/config', (req, res) => { 70 | res.send(config); 71 | }); 72 | app.get('/api/adsb2dd', (req, res) => { 73 | if (config.truth.adsb.enabled == true) { 74 | const api_url = "http://" + config.truth.adsb.adsb2dd + "/api/dd"; 75 | const api_query = 76 | api_url + 77 | "?rx=" + config.location.rx.latitude + "," + 78 | config.location.rx.longitude + "," + 79 | config.location.rx.altitude + 80 | "&tx=" + config.location.tx.latitude + "," + 81 | config.location.tx.longitude + "," + 82 | config.location.tx.altitude + 83 | "&fc=" + (config.capture.fc / 1000000) + 84 | "&server=" + "http://" + config.truth.adsb.tar1090; 85 | const jsonResponse = { 86 | url: api_query 87 | }; 88 | res.json(jsonResponse); 89 | } 90 | else { 91 | res.status(400).end(); 92 | } 93 | }); 94 | 95 | // stash API 96 | app.get('/stash/map', (req, res) => { 97 | res.send(stash_map.get_data_map()); 98 | }); 99 | app.get('/stash/detection', (req, res) => { 100 | res.send(stash_detection.get_data_detection()); 101 | }); 102 | app.get('/stash/iqdata', (req, res) => { 103 | res.send(stash_iqdata.get_data_iqdata()); 104 | }); 105 | app.get('/stash/timing', (req, res) => { 106 | res.send(stash_timing.get_data_timing()); 107 | }); 108 | 109 | // read state of capture 110 | app.get('/capture', (req, res) => { 111 | res.send(capture); 112 | }); 113 | // toggle state of capture 114 | app.get('/capture/toggle', (req, res) => { 115 | capture = !capture; 116 | res.send('{}'); 117 | }); 118 | app.listen(PORT, HOST, () => { 119 | console.log(`Running on http://${HOST}:${PORT}`); 120 | }); 121 | 122 | // tcp listener map 123 | const server_map = net.createServer((socket)=>{ 124 | socket.on("data",(msg)=>{ 125 | data_map = data_map + msg.toString(); 126 | if (data_map.slice(-1) === "}") 127 | { 128 | map = data_map; 129 | data_map = ''; 130 | } 131 | }); 132 | socket.on("close",()=>{ 133 | console.log("Connection closed."); 134 | }) 135 | }); 136 | server_map.listen(config.network.ports.map); 137 | 138 | // tcp listener detection 139 | const server_detection = net.createServer((socket)=>{ 140 | socket.on("data",(msg)=>{ 141 | data_detection = data_detection + msg.toString(); 142 | if (data_detection.slice(-1) === "}") 143 | { 144 | detection = data_detection; 145 | data_detection = ''; 146 | } 147 | }); 148 | socket.on("close",()=>{ 149 | console.log("Connection closed."); 150 | }) 151 | }); 152 | server_detection.listen(config.network.ports.detection); 153 | 154 | // tcp listener tracker 155 | const server_tracker = net.createServer((socket)=>{ 156 | socket.on("data",(msg)=>{ 157 | data_tracker = data_tracker + msg.toString(); 158 | if (data_tracker.slice(-1) === "}") 159 | { 160 | track = data_tracker; 161 | data_tracker = ''; 162 | } 163 | }); 164 | socket.on("close",()=>{ 165 | console.log("Connection closed."); 166 | }) 167 | }); 168 | server_tracker.listen(config.network.ports.track); 169 | 170 | // tcp listener timestamp 171 | const server_timestamp = net.createServer((socket)=>{ 172 | socket.on("data",(msg)=>{ 173 | data_timestamp = data_timestamp + msg.toString(); 174 | timestamp = data_timestamp; 175 | data_timestamp = ''; 176 | }); 177 | socket.on("close",()=>{ 178 | console.log("Connection closed."); 179 | }) 180 | }); 181 | server_timestamp.listen(config.network.ports.timestamp); 182 | 183 | // tcp listener timing 184 | const server_timing = net.createServer((socket)=>{ 185 | socket.on("data",(msg)=>{ 186 | data_timing = data_timing + msg.toString(); 187 | if (data_timing.slice(-1) === "}") 188 | { 189 | timing = data_timing; 190 | data_timing = ''; 191 | } 192 | }); 193 | socket.on("close",()=>{ 194 | console.log("Connection closed."); 195 | }) 196 | }); 197 | server_timing.listen(config.network.ports.timing); 198 | 199 | // tcp listener iqdata metadata 200 | const server_iqdata = net.createServer((socket)=>{ 201 | socket.on("data",(msg)=>{ 202 | data_iqdata = data_iqdata + msg.toString(); 203 | if (data_iqdata.slice(-1) === "}") 204 | { 205 | iqdata = data_iqdata; 206 | data_iqdata = ''; 207 | } 208 | }); 209 | socket.on("close",()=>{ 210 | console.log("Connection closed."); 211 | }) 212 | }); 213 | server_iqdata.listen(config.network.ports.iqdata); 214 | 215 | process.on('SIGTERM', () => { 216 | console.log('SIGTERM signal received.'); 217 | process.exit(0); 218 | }); -------------------------------------------------------------------------------- /html/js/plot_map.js: -------------------------------------------------------------------------------- 1 | var timestamp = -1; 2 | var nRows = 3; 3 | var host = window.location.hostname; 4 | var isLocalHost = is_localhost(host); 5 | var range_x = []; 6 | var range_y = []; 7 | 8 | // setup API 9 | var urlTimestamp; 10 | var urlDetection; 11 | var urlAdsb; 12 | var urlAdsbLink; 13 | var urlConfig; 14 | if (isLocalHost) { 15 | urlTimestamp = '//' + host + ':3000/api/timestamp'; 16 | } else { 17 | urlTimestamp = '//' + host + '/api/timestamp'; 18 | } 19 | if (isLocalHost) { 20 | urlDetection = '//' + host + ':3000/api/detection'; 21 | } else { 22 | urlDetection = '//' + host + '/api/detection'; 23 | } 24 | if (isLocalHost) { 25 | urlMap = '//' + host + ':3000' + urlMap; 26 | } else { 27 | urlMap = '//' + host + urlMap; 28 | } 29 | if (isLocalHost) { 30 | urlAdsbLink = '//' + host + ':3000/api/adsb2dd'; 31 | } else { 32 | urlAdsbLink = '//' + host + '/api/adsb2dd'; 33 | } 34 | if (isLocalHost) { 35 | urlConfig = '//' + host + ':3000/api/config'; 36 | } else { 37 | urlConfig = '//' + host + '/api/config'; 38 | } 39 | 40 | // get truth flag 41 | var isTruth = false; 42 | $.getJSON(urlConfig, function () { }) 43 | .done(function (data_config) { 44 | if (data_config.truth.adsb.enabled === true) { 45 | isTruth = true; 46 | $.getJSON(urlAdsbLink, function () { }) 47 | .done(function (data) { 48 | urlAdsb = data.url; 49 | if (!is_localhost(new URL(urlAdsb).hostname)) { 50 | urlAdsb = urlAdsb.replace(/^http:/, 'https:'); 51 | } 52 | }) 53 | } 54 | }); 55 | 56 | // setup plotly 57 | var layout = { 58 | autosize: true, 59 | margin: { 60 | l: 50, 61 | r: 50, 62 | b: 50, 63 | t: 10, 64 | pad: 0 65 | }, 66 | hoverlabel: { 67 | namelength: 0 68 | }, 69 | plot_bgcolor: "rgba(0,0,0,0)", 70 | paper_bgcolor: "rgba(0,0,0,0)", 71 | annotations: [], 72 | displayModeBar: false, 73 | xaxis: { 74 | title: { 75 | text: 'Bistatic Range (km)', 76 | font: { 77 | size: 24 78 | } 79 | }, 80 | ticks: '', 81 | side: 'bottom' 82 | }, 83 | yaxis: { 84 | title: { 85 | text: 'Bistatic Doppler (Hz)', 86 | font: { 87 | size: 24 88 | } 89 | }, 90 | ticks: '', 91 | ticksuffix: ' ', 92 | autosize: false, 93 | categoryorder: "total descending" 94 | }, 95 | showlegend: false 96 | }; 97 | var config = { 98 | responsive: true, 99 | displayModeBar: false 100 | //scrollZoom: true 101 | } 102 | 103 | // setup plotly data 104 | var data = [ 105 | { 106 | z: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 107 | colorscale: 'Jet', 108 | type: 'heatmap' 109 | } 110 | ]; 111 | var detection = []; 112 | var adsb = {}; 113 | 114 | Plotly.newPlot('data', data, layout, config); 115 | 116 | // callback function 117 | var intervalId = window.setInterval(function () { 118 | 119 | // check if timestamp is updated 120 | $.get(urlTimestamp, function () { }) 121 | 122 | .done(function (data) { 123 | if (timestamp != data) { 124 | timestamp = data; 125 | 126 | // get detection data (no detection lag) 127 | $.getJSON(urlDetection, function () { }) 128 | .done(function (data_detection) { 129 | detection = data_detection; 130 | }); 131 | 132 | // get ADS-B data if enabled in config 133 | if (isTruth) { 134 | $.getJSON(urlAdsb, function () { }) 135 | .done(function (data_adsb) { 136 | adsb['delay'] = []; 137 | adsb['doppler'] = []; 138 | adsb['flight'] = []; 139 | for (const aircraft in data_adsb) { 140 | if ('doppler' in data_adsb[aircraft]) { 141 | adsb['delay'].push(data_adsb[aircraft]['delay']) 142 | adsb['doppler'].push(data_adsb[aircraft]['doppler']) 143 | adsb['flight'].push(data_adsb[aircraft]['flight']) 144 | } 145 | } 146 | }); 147 | } 148 | 149 | // get new map data 150 | $.getJSON(urlMap, function () { }) 151 | .done(function (data) { 152 | 153 | // case draw new plot 154 | if (data.nRows != nRows) { 155 | nRows = data.nRows; 156 | 157 | // lock range before other trace 158 | var layout_update = { 159 | 'xaxis.range': [data.delay[0], data.delay.slice(-1)[0]], 160 | 'yaxis.range': [data.doppler[0], data.doppler.slice(-1)[0]] 161 | }; 162 | Plotly.relayout('data', layout_update); 163 | 164 | var trace1 = { 165 | z: data.data, 166 | x: data.delay, 167 | y: data.doppler, 168 | colorscale: 'Viridis', 169 | zauto: false, 170 | zmin: 0, 171 | zmax: Math.max(13, data.maxPower), 172 | type: 'heatmap' 173 | }; 174 | var trace2 = { 175 | x: detection.delay, 176 | y: detection.doppler, 177 | mode: 'markers', 178 | type: 'scatter', 179 | marker: { 180 | size: 16, 181 | opacity: 0.6 182 | } 183 | }; 184 | var trace3 = { 185 | x: adsb.delay, 186 | y: adsb.doppler, 187 | mode: 'markers', 188 | type: 'scatter', 189 | marker: { 190 | size: 16, 191 | opacity: 0.6 192 | } 193 | }; 194 | 195 | var data_trace = [trace1, trace2, trace3]; 196 | Plotly.newPlot('data', data_trace, layout, config); 197 | } 198 | // case update plot 199 | else { 200 | var trace_update = { 201 | x: [data.delay, detection.delay, adsb.delay], 202 | y: [data.doppler, detection.doppler, adsb.doppler], 203 | z: [data.data, [], []], 204 | zmax: [Math.max(13, data.maxPower), [], []], 205 | text: [[], [], adsb.flight] 206 | }; 207 | Plotly.update('data', trace_update); 208 | } 209 | 210 | }) 211 | .fail(function () { 212 | }) 213 | .always(function () { 214 | }); 215 | } 216 | }) 217 | .fail(function () { 218 | }) 219 | .always(function () { 220 | }); 221 | }, 100); 222 | -------------------------------------------------------------------------------- /src/data/Map.cpp: -------------------------------------------------------------------------------- 1 | #include "Map.h" 2 | #include "data/meta/Constants.h" 3 | #include 4 | #include 5 | #include 6 | 7 | #include "rapidjson/document.h" 8 | #include "rapidjson/writer.h" 9 | #include "rapidjson/stringbuffer.h" 10 | #include "rapidjson/filewritestream.h" 11 | 12 | // constructor 13 | template 14 | Map::Map(uint32_t _nRows, uint32_t _nCols) 15 | { 16 | nRows = _nRows; 17 | nCols = _nCols; 18 | std::vector> tmp(nRows, std::vector(nCols, {1})); 19 | data = tmp; 20 | } 21 | 22 | template 23 | void Map::set_row(uint32_t i, std::vector row) 24 | { 25 | //data[i].swap(row); 26 | for (uint32_t j = 0; j < nCols; j++) 27 | { 28 | data[i][j] = row[j]; 29 | } 30 | } 31 | 32 | template 33 | void Map::set_col(uint32_t i, std::vector col) 34 | { 35 | for (uint32_t j = 0; j < nRows; j++) 36 | { 37 | data[j][i] = col[j]; 38 | } 39 | } 40 | 41 | template 42 | uint32_t Map::get_nRows() 43 | { 44 | return nRows; 45 | } 46 | 47 | template 48 | uint32_t Map::get_nCols() 49 | { 50 | return nCols; 51 | } 52 | 53 | template 54 | std::vector Map::get_row(uint32_t row) 55 | { 56 | return data[row]; 57 | } 58 | 59 | template 60 | std::vector Map::get_col(uint32_t col) 61 | { 62 | std::vector colData; 63 | 64 | for (uint32_t i = 0; i < nRows; i++) 65 | { 66 | colData.push_back(data[i][col]); 67 | } 68 | 69 | return colData; 70 | } 71 | 72 | template 73 | Map *Map::get_map_db() 74 | { 75 | Map *map = new Map(nRows, nCols); 76 | 77 | for (uint32_t i = 0; i < nRows; i++) 78 | { 79 | for (uint32_t j = 0; j < nCols; j++) 80 | { 81 | map->data[i][j] = (double)10 * std::log10(std::abs(data[i][j])); 82 | } 83 | } 84 | 85 | return map; 86 | } 87 | 88 | template 89 | void Map::print() 90 | { 91 | for (uint32_t i = 0; i < nRows; i++) 92 | { 93 | for (uint32_t j = 0; j < nCols; j++) 94 | { 95 | std::cout << data[i][j]; 96 | std::cout << " "; 97 | } 98 | std::cout << std::endl; 99 | } 100 | } 101 | 102 | template 103 | uint32_t Map::doppler_hz_to_bin(double dopplerHz) 104 | { 105 | for (size_t i = 0; i < doppler.size(); i++) 106 | { 107 | if (dopplerHz == doppler[i]) 108 | { 109 | return (int) i; 110 | } 111 | } 112 | return 0; 113 | } 114 | 115 | template 116 | std::string Map::to_json(uint64_t timestamp) 117 | { 118 | rapidjson::Document document; 119 | document.SetObject(); 120 | rapidjson::Document::AllocatorType &allocator = document.GetAllocator(); 121 | 122 | // store data array 123 | rapidjson::Value array(rapidjson::kArrayType); 124 | for (size_t i = 0; i < data.size(); i++) 125 | { 126 | rapidjson::Value subarray(rapidjson::kArrayType); 127 | for (size_t j = 0; j < data[i].size(); j++) 128 | { 129 | subarray.PushBack(10 * std::log10(std::abs(data[i][j])) - noisePower, document.GetAllocator()); 130 | } 131 | array.PushBack(subarray, document.GetAllocator()); 132 | } 133 | 134 | // store delay array 135 | rapidjson::Value arrayDelay(rapidjson::kArrayType); 136 | for (size_t i = 0; i < delay.size(); i++) 137 | { 138 | arrayDelay.PushBack(delay[i], allocator); 139 | } 140 | 141 | // store Doppler array 142 | rapidjson::Value arrayDoppler(rapidjson::kArrayType); 143 | for (uint32_t i = 0; i < get_nRows(); i++) 144 | { 145 | arrayDoppler.PushBack(doppler[i], allocator); 146 | } 147 | 148 | document.AddMember("timestamp", timestamp, allocator); 149 | document.AddMember("nRows", nRows, allocator); 150 | document.AddMember("nCols", nCols, allocator); 151 | document.AddMember("noisePower", noisePower, allocator); 152 | document.AddMember("maxPower", maxPower, allocator); 153 | document.AddMember("delay", arrayDelay, allocator); 154 | document.AddMember("doppler", arrayDoppler, allocator); 155 | document.AddMember(rapidjson::Value("data", document.GetAllocator()).Move(), array, document.GetAllocator()); 156 | 157 | rapidjson::StringBuffer strbuf; 158 | rapidjson::Writer writer(strbuf); 159 | writer.SetMaxDecimalPlaces(2); 160 | document.Accept(writer); 161 | 162 | return strbuf.GetString(); 163 | } 164 | 165 | template 166 | std::string Map::delay_bin_to_km(std::string json, uint32_t fs) 167 | { 168 | rapidjson::Document document; 169 | document.SetObject(); 170 | rapidjson::Document::AllocatorType &allocator = document.GetAllocator(); 171 | document.Parse(json.c_str()); 172 | 173 | document["delay"].Clear(); 174 | for (size_t i = 0; i < delay.size(); i++) 175 | { 176 | document["delay"].PushBack(1.0*delay[i]*(Constants::c/(double)fs)/1000, allocator); 177 | } 178 | 179 | rapidjson::StringBuffer strbuf; 180 | rapidjson::Writer writer(strbuf); 181 | writer.SetMaxDecimalPlaces(2); 182 | document.Accept(writer); 183 | 184 | return strbuf.GetString(); 185 | } 186 | 187 | template 188 | void Map::set_metrics() 189 | { 190 | // get map noise level 191 | double value; 192 | double noisePower = 0; 193 | double maxPower = 0; 194 | for (uint32_t i = 0; i < nRows; i++) 195 | { 196 | for (uint32_t j = 0; j < nCols; j++) 197 | { 198 | value = 10 * std::log10(std::abs(data[i][j])); 199 | noisePower = noisePower + value; 200 | maxPower = (maxPower < value) ? value : maxPower; 201 | } 202 | } 203 | noisePower = noisePower / (nRows * nCols); 204 | this->noisePower = noisePower; 205 | this->maxPower = maxPower - noisePower; 206 | } 207 | 208 | template 209 | bool Map::save(std::string _json, std::string filename) 210 | { 211 | using namespace rapidjson; 212 | 213 | rapidjson::Document document; 214 | 215 | // create file if it doesn't exist 216 | if (FILE *fp = fopen(filename.c_str(), "r"); !fp) 217 | { 218 | if (fp = fopen(filename.c_str(), "w"); !fp) 219 | return false; 220 | fputs("[]", fp); 221 | fclose(fp); 222 | } 223 | 224 | // add the document to the file 225 | if (FILE *fp = fopen(filename.c_str(), "rb+"); fp) 226 | { 227 | // check if first is [ 228 | std::fseek(fp, 0, SEEK_SET); 229 | if (getc(fp) != '[') 230 | { 231 | std::fclose(fp); 232 | return false; 233 | } 234 | 235 | // is array empty? 236 | bool isEmpty = false; 237 | if (getc(fp) == ']') 238 | isEmpty = true; 239 | 240 | // check if last is ] 241 | std::fseek(fp, -1, SEEK_END); 242 | if (getc(fp) != ']') 243 | { 244 | std::fclose(fp); 245 | return false; 246 | } 247 | 248 | // replace ] by , 249 | fseek(fp, -1, SEEK_END); 250 | if (!isEmpty) 251 | fputc(',', fp); 252 | 253 | // add json element 254 | fwrite(_json.c_str(), sizeof(char), _json.length(), fp); 255 | 256 | // close the array 257 | std::fputc(']', fp); 258 | fclose(fp); 259 | return true; 260 | } 261 | return false; 262 | } 263 | 264 | // allowed types 265 | template class Map>; 266 | template class Map; 267 | --------------------------------------------------------------------------------