2026-10-05 17:39:40 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 17:39:40 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 16:46:28 +03:00
2026-06-11 19:51:30 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 17:39:40 +03:00
2026-10-05 17:39:40 +03:00
2026-10-05 17:39:40 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 17:39:40 +03:00
2026-10-05 16:46:28 +03:00
2026-10-05 16:46:28 +03:00

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:

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:

.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:

# 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):

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:

./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:

# 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 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

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.

S
Description
No description provided
Readme
50 MiB
Languages
C++ 47.3%
C 41.4%
Python 8.7%
VHDL 2%
QMake 0.2%
Other 0.1%