314 lines
17 KiB
Markdown
314 lines
17 KiB
Markdown
# Radar System
|
||
|
||
A real-time radar acquisition and imaging system for vector-network-analyzer (VNA)
|
||
and ADC front-ends. It drives the radar through a frequency sweep, calibrates and
|
||
preprocesses the measured S-parameters, runs a selectable imaging/detection
|
||
processor, and presents the live result in a desktop GUI and an in-process web
|
||
interface. The same application runs interactively on a workstation or unattended
|
||
as a headless Raspberry Pi appliance.
|
||
|
||
## Overview
|
||
|
||
The system is split into a **C++ real-time pipeline** and a **Python application**
|
||
that supervises it and handles visualization and control.
|
||
|
||
The pipeline runs as three independent processes connected by lock-free POSIX
|
||
shared-memory ring buffers. Each stage advances on its own, and a slow consumer
|
||
never blocks a producer — the rings are latest-wins, so a reader that falls behind
|
||
simply skips to the freshest frame:
|
||
|
||
1. **Acquisition** — `sweep_orchestrator` (or a hardware-specific producer)
|
||
configures the radar, performs the frequency sweep, and publishes raw S11/S21
|
||
traces.
|
||
2. **Preprocessing** — `data_preprocessor` applies through/OSL calibration,
|
||
reference subtraction, optional frequency-domain notch filtering, and an
|
||
optional object-based MIMO correction of S21.
|
||
3. **Processing** — `data_processor` runs the selected processing mode and emits
|
||
results (pass-through traces, B-scan images, GPR images with detected objects,
|
||
or 3D SAR views). Detected objects are additionally broadcast to subscribers
|
||
over a TCP locator server.
|
||
|
||
The **Python application** (`python_app`) starts and monitors the C++ binaries,
|
||
reads the result rings, renders the live view with pyqtgraph, and exposes the same
|
||
controls plus a pixel-identical live plot through an embedded web UI.
|
||
|
||
```
|
||
radar hardware
|
||
│
|
||
▼
|
||
Acquisition ──[raw ring]──▶ Preprocessing ──[preprocessed ring]──▶ Processing ──[result ring]──▶ GUI / Web
|
||
│
|
||
└──▶ TCP locator (detected objects)
|
||
```
|
||
|
||
## Supported hardware
|
||
|
||
- **librevna** — single LibreVNA.
|
||
- **librevna_multi** — synchronized multi-device LibreVNA.
|
||
- **kamil_adc** — external Kamil ADC acquisition process, with optional laser control.
|
||
- **mock / simulator** — driver-less mode for development without hardware.
|
||
|
||
## Processing modes
|
||
|
||
- **pass_through** — magnitude and phase of the selected channel for each combo.
|
||
- **bscan** — depth-versus-sweep B-scan heatmap.
|
||
- **gpr** — coherent back-projection GPR imaging with object detection.
|
||
- **legacy_gpr** — ellipse-intersection MIMO localization (point and extended/region modes).
|
||
- **sar3d** — 3D SAR of the reference notebook `Multi_antenna_3D_clean_v1.ipynb`
|
||
on an NVIDIA GPU through CUDA: front (X-Y), side (Z-Y), top (X-Z)
|
||
and oblique maximum projections of the imaged
|
||
volume. Its settings carry the notebook's parameter names; the scene window is
|
||
fixed (X −2…2 m, height −0.5…2 m above the ground), its range extent follows
|
||
`MIN_DEPTH`/`MAX_DEPTH`, and the voxels are cubes with edge `SAR_VOXEL_M`
|
||
(default 2 cm). The radar motion is corrected from each trace's capture
|
||
timestamps, so the sweep timing needs no parameters.
|
||
|
||
For matrix-radar B-scans, **Next combination** advances output first, then input,
|
||
and wraps to the first pair. The desktop fields and browser channel selector
|
||
redraw the selected cached B-scan immediately without restarting acquisition.
|
||
**Save Last 1000** and **Save S21 JSON** use a fixed limit of 1000 recent
|
||
measurements from each available stage; older GUI profiles may still contain
|
||
`save_count`, which is ignored. **Start + Record** retains its separate count for
|
||
recording future measurements to disk.
|
||
|
||
Tx/Rx antenna positions are edited in tables with switch position and X/Y/Z in
|
||
metres, on both the desktop and web interface. Coordinate and legacy-GPR medium
|
||
changes apply to the running processor and recompute its retained result. Profiles
|
||
preserve all three coordinates. GPR/SAR views wait for a result marked with the
|
||
current settings revision, so an older in-flight result cannot overwrite new geometry.
|
||
|
||
## Repository layout
|
||
|
||
| Path | Contents |
|
||
| --- | --- |
|
||
| `data_acq_and_processing/` | C++ pipeline: `sweep_orchestrator/`, `preprocessing/`, `processing/` (including `locator/`), shared `common_cpp/` (config, IPC rings), and vendored `third_party/` (Eigen, nlohmann/json). |
|
||
| `python_app/` | Python application: `gui/`, `orchestration/` (process supervisor, shared-memory readers, config), `hardware_full/` (device drivers and services), `webui/`, `storage/`, `models/`, `workflows/`, `scripts/` (producers and tools), and `tests/`. |
|
||
| `run_config_examples/` | Ready-made configuration profiles, one per hardware setup. |
|
||
| `deploy/` | systemd daemon installer, and the USB update kit for the Pi and Jetson (`pack-usb.sh`, `update.sh`). |
|
||
| `build/bin/` | Compiled C++ binaries. |
|
||
| `device_firmware/` | Device firmware and protocol documentation. |
|
||
|
||
## Requirements
|
||
|
||
- Linux (POSIX shared memory; GPIO on Raspberry Pi). The deployment target is a
|
||
**Raspberry Pi 5**; hardware PWM gating for the generator sweep is Pi 5
|
||
specific (GPIO12/13/18/19, `dtoverlay=pwm-2chan`).
|
||
- A C++20 compiler and `make`; Eigen and nlohmann/json are vendored.
|
||
- `libusb-1.0` development headers and `libatomic.so.1` (`libatomic1` on Debian/Ubuntu).
|
||
Python ring readers/writers use lock-free atomic loads, stores and cursor CAS
|
||
through libatomic, including memory barriers for ARM. Slot sizes must be multiples
|
||
of 8 bytes, and the complete ring mapping must not exceed 1 TiB.
|
||
- **Python 3.10 or newer**, in a virtual environment.
|
||
- For `sar3d`: an NVIDIA GPU with its driver (`libcuda`). The
|
||
kernels are compiled at run time by NVRTC, so there is no nvcc step; `make`
|
||
needs only `nvrtc.h`, `cuda.h` and `libnvrtc`, taken from the pip
|
||
`nvidia-cuda-*` packages in `.venv` (`requirements-cuda.txt`, x86_64) or from
|
||
a system toolkit in `/usr/local/cuda` (Jetson).
|
||
Without these dependencies or a usable GPU, SAR3D shows a warning above the
|
||
plots and does not calculate an image. Other processing modes remain available.
|
||
|
||
## Build and run
|
||
|
||
From the repository root:
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/python -m pip install --upgrade pip
|
||
.venv/bin/python -m pip install -r requirements.txt
|
||
make # build the C++ binaries into build/bin/
|
||
```
|
||
|
||
`make` builds the sar3d GPU code when it finds CUDA (see Requirements) and
|
||
prints a note when it does not. After installing the CUDA requirements into an
|
||
existing venv, run `make` again: it notices the change and rebuilds that part.
|
||
For SAR3D on x86_64, install the optional packages with
|
||
`.venv/bin/python -m pip install -r requirements-cuda.txt`, then run `make`.
|
||
|
||
Run the desktop GUI:
|
||
|
||
```bash
|
||
.venv/bin/python -m python_app.gui.main
|
||
# or, with the build step and single-instance handling:
|
||
./start.sh
|
||
```
|
||
|
||
`start.sh` supports `--headless` (offscreen, auto-start — for unattended Pi use),
|
||
`--profile PATH`, `--producer-only`, `--skip-build`, and `--clean-shm`; see
|
||
`./start.sh --help`. The acquisition device (LibreVNA, Kamil ADC, …) is detected
|
||
automatically from the active config's `radar.model` — there is no per-device flag.
|
||
|
||
## Jetson (aarch64) bring-up
|
||
|
||
Ubuntu 22.04 on Jetson has glibc 2.35, and PyQt6 6.8+ ships only
|
||
`manylinux_2_39` aarch64 wheels — `requirements.txt` therefore pins PyQt6 to
|
||
`<6.8` on aarch64 (6.7.1 is the last release with a compatible wheel). With
|
||
that pin the stock `./start.sh` provisioning works; the extra steps are only
|
||
for the CUDA-backed `sar3d` processor:
|
||
|
||
```bash
|
||
# 1. copy the replay dataset (for dataset-replay profiles) and point
|
||
# radar.replay.dataset_dir in the profile at the local copy — the path in
|
||
# run_config_examples/*.json is machine-specific.
|
||
|
||
# 2. normal launch; make takes the JetPack CUDA from /usr/local/cuda
|
||
# (requirements.txt installs no pip nvidia-* packages on aarch64) and the
|
||
# driver's /usr/lib/aarch64-linux-gnu/nvidia/libcuda.so.1:
|
||
./start.sh --profile run_config_examples/run_config_replay_vna.example.json
|
||
```
|
||
|
||
Ubuntu 22.04 ships Python 3.10, and the host parses the radar stream in Python:
|
||
the same parsing code ran 1.6 times slower on 3.10 than on 3.13 (the Pi's version)
|
||
in a benchmark. Use Python 3.13 for the venv, installed with uv so the system
|
||
`python3` stays untouched (apt depends on it):
|
||
|
||
```bash
|
||
curl -LsSf https://astral.sh/uv/install.sh | sh # then open a new terminal
|
||
uv python install 3.13
|
||
bash radar_update/update.sh --python 3.13 # see Deployment
|
||
```
|
||
|
||
## Running without hardware (emulation)
|
||
|
||
Two ways to run the full pipeline with no radar attached:
|
||
|
||
**Mock producer (synthetic data).** The default `run_config.json` already sets
|
||
`radar.driver_mode: "mock"`, so the producer generates synthetic S-parameters.
|
||
Mock acquisition supports the LibreVNA models; Kamil ADC uses native acquisition
|
||
or recorded-dataset replay without devices.
|
||
Both LibreVNA modes share one simulator: a point reflector moves back and forth
|
||
along the antenna array and between depths 3.25–4.25 m over an eight-second cycle.
|
||
Each S21 uses the actual transmitter–target–receiver distance from the configured
|
||
XYZ coordinates; coincident antennas produce identical signals. All combinations
|
||
share the same target position and measured capture window, with collections paced
|
||
at 50 ms intervals. B-scan's default range is 0–8 m; stored profiles retain their
|
||
chosen range. The simulator models a stationary radar, so radar speed should be zero.
|
||
The scene uses the antenna positions from its startup profile; live geometry edits
|
||
change the reconstruction of captured sweeps, whose samples remain unchanged.
|
||
The preprocessor still requires S21 calibration/reference sets. Start the GUI:
|
||
|
||
```bash
|
||
./start.sh
|
||
```
|
||
|
||
In **Preprocessing Panel**, use **Create neutral sets**, then **Start**. This makes
|
||
identity calibration + zero reference, preserving the simulated S21 samples.
|
||
|
||
**Dataset replay (recorded scene).** When
|
||
`radar.replay.dataset_dir` is set in the profile, the supervisor launches
|
||
`python_app.scripts.replay_raw_producer` instead of the normal producer: it
|
||
replays recorded snapshots into the raw ring at the recorded cadence, looping
|
||
forever with a monotonic timeline. Pair it with NEUTRAL preprocess sets so the
|
||
already-preprocessed recording passes through the preprocessor bit-exactly.
|
||
A ready profile for the VNA_AlGo tank005 dataset is provided:
|
||
|
||
```bash
|
||
# one-time: neutral sets for this profile (or use the GUI button)
|
||
.venv/bin/python -m python_app.scripts.create_neutral_preprocess_sets \
|
||
--config run_config_examples/run_config_replay_vna.example.json
|
||
# interactive GUI (mode sar3d is pre-selected by the profile's gui section):
|
||
./start.sh --profile run_config_examples/run_config_replay_vna.example.json
|
||
```
|
||
|
||
Finer voxels increase SAR3D work as 1/`SAR_VOXEL_M`³. Increase the voxel size when
|
||
a faster preview matters more than spatial resolution. Processing failures appear
|
||
above the plots and a failed single capture
|
||
finishes with its explanation while retaining the acquired data.
|
||
|
||
The web interface is served in-process at `http://<host>:8080`. It mirrors the live
|
||
plot and offers Start / Stop / Capture controls and the active processor settings.
|
||
|
||
Manual dataset and JSON export keep the last 1000 available collections from each
|
||
stage independently, including raw or preprocessed sweeps without a result.
|
||
Streaming recording keeps every collection received after arming until the requested
|
||
result count or Stop; Stop and closing the window flush accepted data. Acquisition
|
||
rings use latest-wins delivery, so disk recording cannot recover sweeps overwritten
|
||
before the GUI receives them. NumPy datasets preserve both S-parameters, capture
|
||
windows, every supported result payload, and processing time. Failed exports remove
|
||
their staging files, leaving existing datasets intact. Companion profiles retain
|
||
the radar, antenna layout and preprocessing used for acquisition, including when
|
||
another profile is loaded after Stop.
|
||
|
||
Calibration and reference sets keep their trace arrays and metadata together in one
|
||
atomically replaced NPZ archive, with a readable JSON sidecar. Earlier NPZ/JSON pairs
|
||
remain supported. Invalid trace shapes, duplicate combinations and non-finite
|
||
calibration/reference values are rejected before a saved set is replaced.
|
||
Median captures also require identical frequency axes and channel coverage.
|
||
In **Preprocessing Panel**, create or select the ordinary S21 calibration and
|
||
empty-scene reference first. **Create…** in the **Object Calibration** row opens a separate step:
|
||
take fresh sweeps, then enter the measured reflector centre X/Y/Z in metres.
|
||
Height above ground, radar pitch and sphere radius follow `mimo_calibration.py`;
|
||
several reflector positions may contribute to one calibration. The calculation
|
||
shows each pair's phase at the target before/after correction and its coherence.
|
||
The selected reference is already subtracted, and the previous object correction
|
||
is disabled while these sweeps are acquired. Saved object corrections apply on the
|
||
next **Start**, after notch filtering and before any processor; **Off** disables
|
||
them. Changing antenna geometry disables an incompatible correction and reloads
|
||
acquisition without restarting the application. **Import…** in that row
|
||
accepts the example's `cal_*.npy` / `cal_meta.json` format; the example and GUI share
|
||
one numerical implementation.
|
||
|
||
Dataset replay restores both recorded S-parameters and orders long recordings by
|
||
their numeric index; older S21-only datasets still replay with zero S11. Offline
|
||
JSON conversion and the GUI share the same sweep-matching code.
|
||
|
||
## Configuration
|
||
|
||
The active profile is `run_config.json`. It selects the radar model and sweep, the
|
||
switch matrix and acquisition combos, the preprocessing calibration/reference
|
||
assets, the GPR geometry, and the shared-memory ring sizes. Profiles for each
|
||
hardware setup are provided in `run_config_examples/` — copy one to
|
||
`run_config.json`, or pass it with `--profile`, to use it.
|
||
|
||
## Deployment
|
||
|
||
On a Raspberry Pi, `deploy/install-daemon.sh` installs a systemd service that runs
|
||
the application headless and restarts it on failure. A single-instance lock ensures
|
||
the interactive GUI and the daemon never run at the same time, since they share the
|
||
radar, the shared-memory rings, and the locator port.
|
||
|
||
**Updating a device from a USB flash drive.** On the development machine
|
||
`deploy/pack-usb.sh` writes the host part of the last commit (no venv, build, data
|
||
or local configs) and the update script into `radar_update/` on the flash drive.
|
||
On the Pi or Jetson, close the program and run, from the drive's folder:
|
||
|
||
```bash
|
||
bash radar_update/update.sh # or: --python 3.13 for a fresh venv
|
||
```
|
||
|
||
It installs the newest archive into `~/Documents/radar_system` (or `RADAR_DIR`);
|
||
the current version is renamed to `radar_system<N>`, the next free number. Kept from
|
||
it: `run_config.json`, profiles in `run_config_examples/` changed or added on the
|
||
device (the release version of a changed one lands next to it as `.new`),
|
||
`python_app/runtime/` without logs, `python_app/data*` and any other top-level
|
||
file or folder that is not part of the release, as hard links. The venv is copied
|
||
independently so package updates cannot modify the rollback environment (or a new
|
||
one is built with `--python`). It builds the C++ pipeline and ADC collector and restarts
|
||
`radar.service` if it was running. To go back:
|
||
`rm -rf radar_system && mv radar_system<N> radar_system`.
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
|
||
make check-native
|
||
make check-sar3d PYTHON=.venv/bin/python
|
||
ASAN_OPTIONS=detect_leaks=0 make check-native NATIVE_SANITIZERS=address,undefined
|
||
```
|
||
|
||
The tests cover failed writes and settings commits, recordings during manual Save,
|
||
repeated Start/Stop/Close, web-action cancellation, malformed IPC frames, hardware
|
||
resource cleanup with mock failures, and an isolated deployment update. Native tests
|
||
also exercise mutated frames, ring concurrency, locator input and ADC spool recovery.
|
||
Firmware host tests check cart timer rollover and SBUS packing; they do not validate
|
||
physical timing. Real devices, GPIO and the CUDA backend need their own hardware run.
|
||
|
||
Invalid device/switch settings and undersized raw/preprocessed slots fail before
|
||
acquisition starts. Oversized processed results report the required slot size in
|
||
the log; switching to a mode that fits resumes processing without a restart.
|
||
|
||
GUI profiles, run configs, session state and live processing settings use shared
|
||
atomic writers with unique temporary files. Profiles and run/session settings sync
|
||
their file and directory; frequent live settings avoid extra disk syncs. A failed
|
||
write before replacement leaves the previous file intact. The launcher and supervisor
|
||
share acquisition producer selection, with replay taking precedence over the model.
|