569 lines
34 KiB
Markdown
569 lines
34 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 S21 traces, 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 uses **Python acquisition producers**, **C++ preprocessing and processing**,
|
||
and a **Python application** for supervision, 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
|
||
skips overwritten frames and continues from the retained tail:
|
||
|
||
1. **Acquisition** — Python producers configure the radar, acquire frequency sweeps,
|
||
and publish raw S21 traces. Both single and multi-device LibreVNA use
|
||
`matrix_raw_producer`; RadioPhotonic radar (RPH) uses `rph_radar_raw_producer` with its C++
|
||
collector (E-502) or the RFG board's UDP stream.
|
||
2. **Preprocessing** — `data_preprocessor` applies through 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 producer and C++ processes,
|
||
reads the result rings, renders the live view with pyqtgraph, and exposes acquisition
|
||
controls, processing settings and a copy of the 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.
|
||
- **rph_radar** — [RadioPhotonic radar (RPH) collector](data_acq_and_processing/rph_radar_collector/README.md),
|
||
with manual laser setpoints or LD1 current variation via **Apply Radar**.
|
||
`radar.rph_radar.source` selects the ADC:
|
||
- `e502` (default) — the application owns the collector and exchanges complete
|
||
sweeps over a PTY. Switching to a different antenna pair starts a fresh ADC session
|
||
after settling, discarding queued samples and an initial partial sweep.
|
||
- `rfg_udp` — the RFG board sends averaged sweep points to `udp_bind`
|
||
(`run_config_rph_radar_rfg.example.json`); main = ch0 − ch1, reference = ch2. It
|
||
needs `laser_control.mode: variation` (even when the laser is started elsewhere):
|
||
a sweep must have exactly the task's step count, `ceil((max − min) / step)` in DAC
|
||
codes, with no lost points; others are dropped (the board loses a few percent).
|
||
Capture windows come from kernel receive timestamps. Only one antenna pair (both
|
||
switches with one position): without a source-side marker a sweep after a switch
|
||
cannot be proven fresh.
|
||
- **mock / simulator** — driver-less mode for development without hardware.
|
||
|
||
## Processing modes
|
||
|
||
- **pass_through** — magnitude and phase of S21 for each combo.
|
||
- **bscan** — depth-versus-sweep B-scan heatmap.
|
||
- **gpr** — coherent back-projection on the ground plane, as in `Horns_motion_4x4_ground.py`,
|
||
with object detection. Radar height and look angle define the slice in radar X/Z axes;
|
||
the wave speed is 3×10⁸ m/s (air).
|
||
- **sar3d** — 3D SAR of the reference notebook `Multi_antenna_demonstrate_depthnorm.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
|
||
set by the selected antennas with a 2 m X margin and 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.
|
||
|
||
Both coherent GPR and SAR apply `PAIR_DEPTH_EQUALIZE` (on by default) before the
|
||
per-pair median normalization. The notebook's 1.5 m sliding median estimates each
|
||
pair's depth-dependent level; positive gains bring the pairs to their geometric
|
||
mean while preserving phase and the common depth envelope. **Subtract mean of previous
|
||
collections** subtracts up to **Mean count** preceding spectra for each pair before
|
||
motion correction and the IFFT; zero count disables it. SAR uses a causal aperture:
|
||
`SAR_FRAME_RADIUS` previous frames plus the current frame, with the current scene origin.
|
||
The processor retains enough history for the aperture and each frame's preceding
|
||
mean; transport ring capacity does not shorten this background window.
|
||
Scene width `SAR_X_SIZE_M` and length `SAR_Z_SIZE_M` use 0 for the automatic notebook
|
||
bounds; positive values resize the axes about their automatic centres. `SAR_Y_RANGE_M`
|
||
sets the height limits. The depth gate remains independent of these scene bounds.
|
||
|
||
SAR **Zoom (%)** shows the full scene at 100%; increasing it shrinks all four
|
||
views uniformly towards the brightest response. **SAR_CLOUD_THRESHOLD** hides
|
||
values below the selected fraction of the normalization maximum. Both controls
|
||
redraw the existing result without recalculating it. Tx/Rx markers appear on the
|
||
front, side and top projections in ground coordinates. A hollow square marks the
|
||
peak; its size follows the contiguous half-maximum width along the volume axes
|
||
(FWHM, at least one voxel). This measures the imaged response rather than the
|
||
reflector's physical dimensions.
|
||
|
||
The desktop uses a light theme with dark plot canvases. **Mode** stays below the
|
||
**Actions** buttons while the settings sections scroll; the settings panel and
|
||
runtime log can be collapsed with their arrow buttons. SAR3D status, peak
|
||
coordinates and level appear above the projections.
|
||
|
||
For B-scans on every radar model, **B-scan pair** selects a pair from the displayed
|
||
history independently of the run combinations. **Next Combination** advances output
|
||
first, then input, and wraps to the first recorded pair; it is enabled with at least
|
||
two pairs. Desktop and browser selectors redraw the cached B-scan immediately.
|
||
|
||
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 changes apply to the
|
||
running processor and recompute its retained result. Profiles
|
||
preserve all three coordinates. A new table row becomes active after X is entered;
|
||
desktop, web and profile geometry use one Python validator; C++ validates its own
|
||
input. 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: `preprocessing/`, `processing/` (including `locator/`), `rph_radar_collector/`, shared `common_cpp/` (config, IPC rings, H7992 driver), 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**.
|
||
- A C++20 compiler and `make`; Eigen and nlohmann/json are vendored.
|
||
- `libusb-1.0` runtime 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.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 # data_preprocessor and data_processor in build/bin/
|
||
make rph_radar_collector # RadioPhotonic radar (RPH) with radar.rph_radar.source: e502
|
||
```
|
||
|
||
`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.
|
||
CUDA 13 is installed from `requirements.txt` on x86_64. For GPUs older than
|
||
sm_75, install NVRTC 12 outside `.venv` and build with
|
||
`make BUILD_DIR=/tmp/build-nvrtc12 CUDA_ROOTS="<dir>/nvidia/cuda_nvrtc <dir>/nvidia/cuda_runtime"`.
|
||
A separate build directory keeps the two CUDA builds independent.
|
||
|
||
Run the desktop GUI through the launcher for build and single-instance handling:
|
||
|
||
```bash
|
||
./start.sh --profile run_config.json # explicit local mock profile
|
||
# or choose a hardware profile from run_config_examples/
|
||
```
|
||
|
||
`start.sh` also supports `--headless` (offscreen, auto-start), `--auto-start`,
|
||
`--producer-only`, `--skip-build`, `--build-only`, and `--clean-shm`; see
|
||
`./start.sh --help`. Pass `--profile PATH` so launcher provisioning and the GUI
|
||
use the same profile. The producer is selected from `radar.model`; there is no
|
||
per-device flag. Provision dependencies
|
||
before unattended use: headless startup does not install missing packages.
|
||
|
||
For development, `.venv/bin/python -m python_app.gui.main` starts the GUI directly,
|
||
without the launcher's build step or single-instance lock.
|
||
|
||
## Jetson (aarch64) bring-up
|
||
|
||
`requirements.txt` installs PyQt6 from wheels only, so pip picks the newest release
|
||
the system glibc can run: 6.7 on Jetson's glibc 2.35, current releases on Debian 13. CUDA comes from JetPack in `/usr/local/cuda`; pip CUDA packages
|
||
are installed only on x86_64. Use the same build commands and launcher as above.
|
||
|
||
The USB updater can create a fresh venv with an already installed interpreter,
|
||
for example `bash radar_update/update.sh --python 3.13`. The system Python does
|
||
not need to be replaced.
|
||
|
||
## Running without hardware (emulation)
|
||
|
||
The default `run_config.json` already sets `radar.driver_mode: "mock"`, so the
|
||
producer generates synthetic S-parameters and no radar is needed. Mock acquisition
|
||
supports the LibreVNA models; RadioPhotonic radar (RPH) needs native acquisition.
|
||
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. The root mock profile explicitly selects a 0–8 m B-scan
|
||
range. Both shipped mock profiles use a 0.445 m vertical antenna baseline
|
||
(Rx1/Rx3 and, for the four-output matrix, Tx2/Tx3), avoiding coincident pairs.
|
||
The general defaults are 0.824–1.0 m; stored profiles retain their 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 --profile run_config.json
|
||
```
|
||
|
||
Open **Preprocessing…**, use **Create Neutral S21 Sets**, then **Start**. This makes
|
||
identity calibration + zero reference, preserving the simulated S21 samples.
|
||
|
||
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.
|
||
|
||
## Web interface
|
||
|
||
The embedded server listens at `http://<host>:8080`; set
|
||
`RADAR_SYSTEM_WEBUI_PORT` to use another port. It is available in desktop and
|
||
headless mode. Browser actions are:
|
||
|
||
- **Start**, **Single Capture**, **Stop**, **Capture Temporary Reference** and **Remove Last Measurement**;
|
||
- load a profile from `run_configs/*.json` using **Load**;
|
||
- **Save Last 1000** and **Start + Record to Disk** with a path, optional name and recording count;
|
||
- edit processor settings and antenna tables with **Apply**, or clear history with **Clear Runtime History**;
|
||
- select a **B-scan pair** or advance with **Next Combination**.
|
||
|
||
Copy profiles from `run_config_examples/` into `run_configs/` to offer them in the
|
||
browser. JSON export and calibration dialogs are desktop actions. PNG capture
|
||
runs only while a browser is connected and the plot has changed, including zoom
|
||
and resize, at most five times per second. Each client retains the latest frame,
|
||
status and settings independently; reconnecting refreshes settings, and **Apply**
|
||
sends only edited fields. Field labels, units and checkboxes match the desktop form.
|
||
|
||
## Data and preprocessing
|
||
|
||
### Snapshots and recording
|
||
|
||
**Save Last 1000** and **Save S21 JSON** keep up to 1000 available collections from
|
||
each stage independently, including raw or preprocessed sweeps without a result.
|
||
The limit is fixed; legacy profile `save_count` values are ignored. Saving
|
||
keeps an active disk recording running.
|
||
Without raw/preprocessed taps, result snapshots remain available, but S21 JSON
|
||
requires retained traces.
|
||
**Start + Record to Disk** has a separate count
|
||
for future results. It keeps collections received after arming until that count,
|
||
Stop, or exhaustion of the bounded writer queue. If storage falls behind,
|
||
recording stops accepting data, drains the queue and final chunk, and reports the limit.
|
||
Stop and closing the window also flush accepted data without blocking the GUI.
|
||
Pausing acquisition for a capture or history action ends an active recording:
|
||
the log reports the accepted count, requested count and stop reason before draining
|
||
the writer. Latest-wins rings cannot recover sweeps overwritten before the GUI
|
||
receives them. NumPy datasets preserve S21, 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. Dataset name prefixes also describe the acquisition;
|
||
a manual save is cancelled if acquisition is restarted during its final history drain.
|
||
|
||
Each NumPy dataset includes `processing_settings.json`, mapping result revisions to
|
||
the live settings used for processing. Results retain their revision, capture window,
|
||
and actual `speed_m_s` (including speed supplied by the locator). Settings
|
||
changed while acquisition is stopped are recorded for the reprocessed results as well.
|
||
**Remove Last Measurement** uses capture order and removes matching collections
|
||
from every stage; the processor suppresses that sweep if it is still pending.
|
||
**Clear Runtime History** clears all retained
|
||
stages and rejects late results from before the cutoff. These actions leave saved
|
||
files intact. A sweep is identified by `(collection_id, capture_start_ns)`; all
|
||
stages preserve the producer's capture window. `monotonic_ns` is diagnostic only.
|
||
|
||
**Save S21 JSON** can add exports to an existing NumPy dataset. Existing JSON files
|
||
are never overwritten; missing raw/preprocessed counterparts produce empty arrays,
|
||
and stages pair only by the exact acquisition key. Its `vna_config_profile.json` companion
|
||
leaves the NumPy dataset's `config_profile.json` intact. A streaming write failure
|
||
keeps all collections already published; only the incomplete collection is discarded.
|
||
Legacy snapshots without capture windows retain repeated IDs through one-to-one
|
||
time/index matching; processing completion timestamps are not compared with
|
||
acquisition timestamps.
|
||
|
||
### Calibration and reference sets
|
||
|
||
Calibration and reference sets keep their trace arrays and metadata together in one
|
||
atomically replaced NPZ archive, with a readable JSON sidecar. Both files and their
|
||
directory entries are synced before successful completion. Existing file permissions
|
||
are retained; new files and directories respect the process umask. 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.
|
||
Open **Preprocessing…** to create or select the ordinary S21 calibration and
|
||
empty-scene reference. The processing order is through calibration → calibrated
|
||
reference subtraction → notch → object correction → the selected processor.
|
||
|
||
### Object calibration
|
||
|
||
**Create…** in the **Object calibration** row opens a separate dialog. Use
|
||
**Capture Data** to take fresh preprocessed sweeps, then enter the measured
|
||
reflector centre X/Y/Z in metres and use **Calculate and Save**. The optional
|
||
**Calculation Settings** section expands inside the scrollable dialog body;
|
||
the calculation, cancellation and close buttons remain visible below it.
|
||
Coordinates follow the notebook: X across the array, Y above the ground, Z along
|
||
the ground.
|
||
Height above ground, radar pitch and sphere radius follow `MIMO_calibration.ipynb`;
|
||
several reflector positions may contribute to one calibration. The only diagnostic
|
||
plot shows the phase of each antenna pair's contribution at the target point,
|
||
before and after correction, as in cell 5 of `MIMO_calibration.ipynb`: Tx rows,
|
||
Rx columns, phase relative to the complex sum of each map, with cell values and
|
||
standard deviation in degrees on a cyclic ±180° scale.
|
||
With partial antenna geometry, only the specified Tx × Rx pairs contribute to
|
||
the calculation and phase maps. Acquisition still retains every physical stage;
|
||
pairs outside that geometry receive unit correction and appear in `unchanged_combos`.
|
||
The selected reference is already subtracted, and the previous object correction
|
||
is disabled while these sweeps are acquired. A capture owns an exclusive pipeline
|
||
pause until it finishes or is cancelled, including during automatic-restart backoff.
|
||
An explicit **Stop** cancels automatic resumption.
|
||
|
||
Object corrections use the same NPZ collection format as other preprocessing sets:
|
||
S21 holds each pair's multiplier on the captured frequency grid. Metadata retain
|
||
the capture radar key, antenna geometry, through-set name and content hash, target
|
||
positions, estimation settings and phase diagnostics. Saving uses the capture's
|
||
configuration even if the current profile changes during calculation. The next
|
||
**Start** checks the selected through set's S21 and frequency arrays, geometry and
|
||
coverage of the requested combinations before applying the correction after notch
|
||
filtering and before any processor. **Off** disables it. Changing antenna geometry
|
||
disables an incompatible correction and reloads acquisition without restarting
|
||
the application. Changing relative permittivity only updates processing settings.
|
||
|
||
Legacy `cal_*.npy` / `cal_meta.json` folders are not application calibration sets.
|
||
Capture a set through **Create…** to use object correction; the legacy files on
|
||
disk are left intact. Ordinary through/reference NPZ sets need no conversion.
|
||
|
||
Older NPZ sets and snapshots remain readable: extra S11 arrays are ignored;
|
||
new trace files contain only frequency and S21.
|
||
|
||
## Runtime interfaces
|
||
|
||
Tap rings are optional: an empty `rings.raw_tap.name` or
|
||
`rings.preprocessed_tap.name` disables that copy and its GUI history. Core processing
|
||
and single capture still work without taps; object calibration needs the preprocessed
|
||
tap. All producers share raw publication and hardware retry handling.
|
||
|
||
Raw and preprocessed IPC use **RAW3** and **PRP3** respectively: each trace contains
|
||
float32 frequencies followed by complex64 S21 (12 bytes per point). The complete
|
||
frame requires collection and per-trace capture windows; its size is `44 + combos * (28 + 12 * points)` bytes.
|
||
Older RAW2/PPR2 bundles are incompatible and are rebuilt from NPZ sets at Start;
|
||
update Python and C++ together. Results use RSL3; older RSL2 frames are rejected.
|
||
|
||
RF switches use H7992 with 1–4 positions and two GPIO control lines. Profiles use
|
||
`driver_mode`, GPIO settings and the position count. An old `driver: "h7992"` is accepted;
|
||
other explicit driver values or enabled inversion are rejected before GPIO opens.
|
||
Two positions use the existing H7992 mapping `(A,B) = (0,0), (0,1)`; physical
|
||
wiring and channel numbers follow the profile.
|
||
|
||
Single-device LibreVNA acquires the configured `run.combos` in order. Multi-device
|
||
acquisition keeps the full matrix: SAR3D derives intra-sweep stage timing from
|
||
the complete list of outputs sharing a capture window.
|
||
|
||
## Configuration
|
||
|
||
A profile selects the radar model and sweep, switches and acquisition combos,
|
||
preprocessing sets, antenna geometry, ring sizes and GUI settings. The GUI loads
|
||
`RADAR_SYSTEM_PROFILE` (set by `start.sh --profile`), otherwise the last profile in
|
||
`python_app/runtime/gui_session_state.json`, otherwise root `run_config.json`.
|
||
If a selected external profile cannot be loaded, it reports the error and falls
|
||
back to the root profile. **Load Config** and **Save Config** manage desktop profiles.
|
||
|
||
Hardware examples are in `run_config_examples/`. The root `run_config.json` is
|
||
intentionally mock; select a hardware profile explicitly on a real device. A
|
||
change of model, serials, sweep parameters or multi-device switch positions changes
|
||
the `radar_key` used to find calibration/reference sets; select or capture matching
|
||
sets before **Start**. Changing the multi-device reference-clock source also resets
|
||
display history. For RadioPhotonic radar (RPH), identity follows the phase anchors, output
|
||
band and acquisition/laser settings; executable/TTY paths and LibreVNA-only sweep
|
||
fields do not affect it. RPH processing frequency controls use its output band.
|
||
Phase, band and timeout errors are rejected before a running pipeline is stopped.
|
||
|
||
Profiles with model `kamil_adc` are rejected with an explicit error: use
|
||
`rph_radar` and the `radar.rph_radar` section. An inactive `radar.kamil_adc`
|
||
section left in older saved profiles is ignored; there are no aliases or automatic
|
||
data migrations. Old RPH calibration/reference sets use a different
|
||
measurement key and are not selected by the renamed mode.
|
||
|
||
### RPH phase calibration
|
||
|
||
Use `run_config_examples/run_config_rph_radar.example.json` (E-502) or
|
||
`run_config_rph_radar_rfg.example.json` (RFG board) for **RadioPhotonic radar (RPH)**.
|
||
Apply the desired laser settings with **Apply Radar**, then stop acquisition before
|
||
running the calibration tool:
|
||
|
||
```bash
|
||
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200
|
||
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200 --apply
|
||
```
|
||
|
||
Pass the path to your active RPH profile. The first command reports measured phase
|
||
anchors; `--apply` captures again and writes only `phase0_rad` and `phase1_rad` in
|
||
`radar.rph_radar.phase_calibration`. Frequency anchors must already describe the
|
||
known sweep endpoints. The tool uses current acquisition settings, sends no laser
|
||
commands, preserves every other JSON byte, and refuses to overwrite a profile
|
||
edited during capture. Band and point count stay unchanged. `--warmup` controls
|
||
how many initial sweeps to discard (default 10).
|
||
|
||
## Deployment
|
||
|
||
On a Raspberry Pi, `deploy/install-daemon.sh` installs a systemd service that runs
|
||
the application through `start.sh --headless --skip-build` and restarts it on
|
||
failure. The launcher holds a single-instance lock for the radar, rings and locator
|
||
port; an interactive launch first asks systemd to stop an active daemon.
|
||
|
||
**Updating a device from a USB flash drive.** On the development machine
|
||
`deploy/pack-usb.sh /path/to/drive` writes the host part of the **last commit**
|
||
and its update script into `radar_update/` on the flash drive. Uncommitted changes,
|
||
venv, build outputs, recorded data and local configs are excluded.
|
||
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
|
||
```
|
||
|
||
Run the updater as the normal device user. It installs the newest archive into
|
||
`~/Documents/radar_system` (or `RADAR_DIR`), renaming the current version to the
|
||
next free `radar_system<N>`. It preserves:
|
||
|
||
- `run_config.json` and changed/added `run_config_examples/*.json` by independent
|
||
copy; a conflicting release profile is placed beside the device profile as `.new`;
|
||
- `python_app/runtime/` by copy, excluding logs and `*.pids`;
|
||
- `python_app/data*` and extra non-hidden top-level entries absent from the release
|
||
by hard links, excluding `build`, `graphify-out`, `device_firmware`, `docs` and `RELEASE`;
|
||
- the venv by independent copy, or a fresh one when `--python` is supplied.
|
||
|
||
Saved data is written through replacement or new files, preserving the backup's
|
||
hard links. `RELEASE` gets a new inode. The updater builds the C++ pipeline and ADC
|
||
collector, then restarts `radar.service` only if it was running before the update.
|
||
Keep the device's real hardware profile: the release's root profile is mock.
|
||
|
||
For rollback, stop `radar.service` and close the GUI, rename the new installation
|
||
aside to preserve measurements taken since the update, then rename the chosen
|
||
backup to `radar_system`. Restart the service if it is normally used.
|
||
|
||
## 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
|
||
make check-gpr
|
||
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 whole-sweep PTY overflow 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.
|
||
|
||
`check-sar3d` runs the real CUDA processor (`data_acq_and_processing/tests/test_backprojection.py`).
|
||
It compares it with `sar3d_reference.npz`: compact float32/complex64 inputs, the three
|
||
maximum projections of the notebook's volume, its joint peak/level and response
|
||
widths for six compensation/normalization, equalization, background, scene and
|
||
timing cases. The expected values come from unchanged functions in cells 1 and 14
|
||
of the original notebook, with each frame's previous-collection mean subtracted
|
||
from its input spectra by NumPy. The same test checks the published
|
||
status for invalid input, a result too large for the ring slot, an empty and a very weak
|
||
signal. Without a usable GPU it checks only the CUDA warning and skips the rest; the
|
||
notebook itself is not needed to run it. To regenerate the reference, pass the
|
||
original notebook (it is kept outside the repository):
|
||
|
||
```bash
|
||
.venv/bin/python data_acq_and_processing/tests/generate_backprojection_reference.py \
|
||
--notebook /path/to/Multi_antenna_demonstrate_depthnorm.ipynb \
|
||
--gpr-script /path/to/Horns_motion_4x4_ground.py
|
||
```
|
||
|
||
The generator also writes `gpr_reference.npz` from the unchanged Horns coherent BP
|
||
functions, with the notebook equalizer inserted before scalar normalization.
|
||
`make check-gpr` compares the CPU processor with those images, including motion and
|
||
previous-collection subtraction. Neither source file is needed to run the tests.
|
||
|
||
The archives record source SHA256 hashes and NumPy/SciPy versions. Another build
|
||
can be checked with `make check-sar3d BUILD_DIR=/path/to/build` or by running
|
||
`test_backprojection.py Sar3dTest` with
|
||
`BACKPROJECTION_FIXTURE=/path/to/build/native_tests/backprojection_fixture`.
|
||
For older GPUs, pass the same `BUILD_DIR` and NVRTC 12 `CUDA_ROOTS` used to build.
|
||
|
||
For development, install `pyright` into the venv, then run
|
||
`.venv/bin/pyright python_app` to check all Python modules and tests.
|
||
|
||
## Application architecture
|
||
|
||
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.
|
||
|
||
`AppWindow` assembles controllers and shuts them down. `WindowView` owns layout
|
||
and typed sections; controllers receive concrete dependencies, without GUI mixins
|
||
or duplicate window state. `ProfileController` owns startup selection, session
|
||
state and profile persistence. `RadarSettingsController` applies device limits,
|
||
while `LiveSettingsController` publishes revisions and updates plots.
|
||
|
||
GUI scalar settings use dataclass metadata in `models/gui_profile_schema.py`:
|
||
profile validation, desktop values, live processing fields, processing log summaries
|
||
and web forms share those bindings. Hardware fields and geometry have their own
|
||
typed mappings and validators. Section builders return typed widget groups; their
|
||
controllers connect signals directly, with no string table of action callbacks.
|
||
`gui/controllers/config_controller.py` reads and applies scalar metadata without owning
|
||
the window or acquisition processes. Web batches roll back together when validation
|
||
or live-file publication fails; desktop settings also revert to the last publication.
|
||
Profile loading blocks editor signals until all settings are populated.
|
||
|
||
`HistoryController` owns retained acquisitions; `PlotController` selects the view
|
||
and supplies independent B-scan, GPR, trace and SAR renderers with typed settings.
|
||
Renderers own their plots and display caches, without access to the acquisition
|
||
processes. Stopped B-scan replay uses the last run's ring, matches exact acquisition
|
||
keys and the requested settings revision, and cancels when that run changes. With
|
||
no retained preprocessed sweeps (including disabled taps), the processor recomputes
|
||
its own retained history. Capture previews preserve the selected processing view.
|
||
`PipelineController` owns processes, ring readers, polling/recovery, Single Capture
|
||
and replay. Its pause prevents recovery from starting a producer over a hardware
|
||
capture; Stop or Close cancels any pending resumption, including during a drain.
|
||
Validation rejects Start before stopping existing processes. Pauses resume only
|
||
continuous acquisition, retry failed resumption with the usual backoff, and release
|
||
ownership before presenting capture errors. GPIO errors are logged without dialogs.
|
||
Rendering failures leave acquisition and the locator running and log each cause once;
|
||
repeated ring-read failures retain their separate recovery and stop policy.
|
||
`PreprocessController` owns calibration/reference sessions and asset selection.
|
||
One `SequentialCaptureSession` handles both current settings and a list of sweep
|
||
variants; a single variant adds no extra acquisition or reconfiguration. Multi-device
|
||
collection always captures one complete synchronized cycle. The native
|
||
`DataPreprocessor` owns all three bundles in `preprocessing/bundles`: through,
|
||
through-calibrated reference and object correction, applied in that order with notch
|
||
filtering before object correction.
|
||
Object capture checks both acquisition processes and releases its pause only after
|
||
completion or cancellation; losing the producer reports a failure immediately.
|
||
`SnapshotController` coordinates manual exports, paused Clear/Remove operations
|
||
and Start + Record to Disk.
|
||
History deletion is retained before publishing it to the processor, so a failed
|
||
settings write can be retried without restoring deleted sweeps. `RecordingController`
|
||
owns a bounded writer session; its disk thread receives data and storage only.
|
||
A processor restart during Start + Record to Disk does not finish the newly armed session.
|
||
Temporary references and neutral sets clear retained processing history without
|
||
restarting the locator while the pipeline is paused.
|
||
|
||
`WebBridge` queues browser actions onto Qt and exposes immutable status/frame
|
||
snapshots. Clear and Remove use separate action endpoints; live settings only edit
|
||
settings. Trace display options (including its Y range) do not publish processing
|
||
revisions. A timeout cancels queued actions; an action already running reports that
|
||
state without inviting a duplicate request. The GPIO controller invokes the same
|
||
reference-capture action as desktop and web, and ignores queued presses after Close.
|
||
`LogController` batches worker logs and returns web-action errors to the browser
|
||
without opening a desktop modal. Stop/Close also cancels delayed launcher startup.
|
||
|
||
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.
|