added docs

This commit is contained in:
Ayzen
2026-10-16 18:35:02 +03:00
parent b88d704126
commit ad2b3e555a
19 changed files with 3719 additions and 663 deletions
-1
View File
@@ -252,7 +252,6 @@ AGENTS.md
.claude/ .claude/
.graphifyignore .graphifyignore
pyrightconfig.json pyrightconfig.json
/docs/
test_end_2/ test_end_2/
# --- device_firmware: PlatformIO / STM32G431 cart remote --- # --- device_firmware: PlatformIO / STM32G431 cart remote ---
+66 -581
View File
@@ -1,599 +1,84 @@
# Radar System # 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 коэффициента передачи S21 по всем парам антенн, калибрует их, строит изображения сцены
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 программам по TCP. Программа работает на рабочей станции с экраном и на Raspberry Pi 5
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: a full ring overwrites its oldest
unread frame, a reader that falls behind continues with the frames still retained, and
one overtaken during a read resumes at the newest frame:
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. A band selecting fewer than two
sweep points gives no range profile, and such sweeps are left out of the image.
- **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_depthnorm_clean.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 voxel steps `SAR_VOXEL_M` X, Y and Z default to the
notebook's 2.5, 2 and 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 square is left out when CLEAN objects are shown
or when the background gate empties the frame.
**BG_GATE_ENABLE** shows a frame relative to its background, the median of the volume
over its working voxels: below 6 backgrounds the frame is empty, at 6 its maximum
appears at the cloud threshold, and from 8.5 the scale is full. **SAR_CLEAN_ENABLE**
runs the notebook's CLEAN on a one-frame aperture: always with `SAR_FRAME_RADIUS` 0,
otherwise while no previous frame is available, as after Start. It subtracts the strongest point response
from the A-scans of every pair, step by step on a 4 × 10 × 4 cm grid over the whole scene
region, until the residual is background. The search ignores `SAR_X_SIZE_M` and
`SAR_Z_SIZE_M`, so reflectors just outside a narrowed scene are subtracted too. Each object is then imaged apart and drawn in its own 0–1
scale over the residual, so a weak object is not hidden by the sidelobes of a strong
one. **SAR_MARK_ENABLE** draws boxes around groups of CLEAN objects: red for very low
objects (like a mine), orange on the ground, blue above it, dashed grey at the scene
boundary, where the response may lie partly outside. The legend is in the title of the
3D view. Without boxes the objects are numbered with their level in backgrounds. All
three switches are on by default, like in the notebook, and the status line reports the
background ratio and the CLEAN result. The gate and CLEAN are processing settings; boxes
only redraw. Their thresholds and rules are the notebook's fixed values. Both cost time:
on a GTX 1060 with the default scene of 10 million voxels, the gate adds about 0.1 s per
frame and CLEAN about 0.4 s more.
The desktop uses a light theme with white 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). | | одиночный LibreVNA | один векторный анализатор цепей и ВЧ-ключи на антеннах |
| `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/`. | | многоприборная схема | три синхронных LibreVNA; с контроллером ключей — матрица 4 × 4 пар за одну развёртку |
| `run_config_examples/` | Ready-made configuration profiles, one per hardware setup. | | радиофотонный радар | СВЧ-сигнал формируют лазеры, данные оцифровывает АЦП L-Card E-502 или плата RFG |
| `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 Режимы обработки: pass-through (S21 по частоте), B-scan (дальность во времени), GPR
(карта земли перед радаром с поиском объектов), 3D SAR (трёхмерное изображение на
видеокарте NVIDIA).
- 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 ```sh
./start.sh
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 `start.sh` создаёт окружение Python, ставит пакеты и собирает C++. Корневой профиль
prints a note when it does not. After installing the CUDA requirements into an `run_config.json` — симулятор. В окне нажмите **Preprocessing…** →
existing venv, run `make` again: it notices the change and rebuilds that part. **Create Neutral S21 Sets**, затем **Start** и выберите режим в списке **Mode**.
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: Установка, запуск с железом и служба для работы без экрана — в главе
[Установка и запуск](docs/installation.md).
```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, 1. [Как работает радар](docs/principles.md) — физика измерения и обработки для тех,
without the launcher's build step or single-instance lock. кто не работал с векторными анализаторами и радарами.
2. [Оборудование](docs/hardware.md) — приборы, подключение, прошивки.
3. [Установка и запуск](docs/installation.md) — установка, `start.sh`, служба,
обновление с флешки.
4. [Типовые сценарии](docs/workflow.md) — порядок действий от подготовки до
сохранения данных.
5. [Окно программы](docs/desktop.md) — каждая кнопка, поле и сообщение окна.
6. [Режимы обработки](docs/processing.md) — что показывает каждый режим и что делает
каждая его настройка.
7. [Калибровка и предобработка](docs/preprocessing.md) — наборы, съём, калибровка по
объекту.
8. [Веб-интерфейс, кнопка и локатор](docs/web.md) — управление по сети, GPIO-кнопка,
протокол локатора, работа без экрана.
9. [Файл конфигурации](docs/configuration.md) — каждый ключ профиля.
10. [Данные на диске](docs/data.md) — форматы наборов, снимков, журналов.
11. [Неполадки](docs/troubleshooting.md) — признак, причина, что делать.
12. [Словарь](docs/glossary.md).
## Jetson (aarch64) bring-up Для разработки:
`requirements.txt` installs PyQt6 from wheels only, so pip picks the newest release - [Устройство программы](docs/architecture.md) — процессы, кольца, кадры, состав кода,
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. - [Разработка](docs/development.md) — сборка, тесты, эталоны, правила.
- [Протокол LibreVNA и кадры колец](device_firmware/librevna_protocol.md),
[переключение стимула](device_firmware/per_point_switching.md),
[контроллер ключей](device_firmware/switch_controller/README.md),
[сборка прошивок](device_firmware/multi_libreVNA_firmware/reproduce/README.md),
[протокол пульта тележки](device_firmware/cart_firmware/docs/protocol.md),
[коллектор E-502](data_acq_and_processing/rph_radar_collector/README.md).
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 | `python_app/` | окно программы, драйверы приборов, веб-интерфейс, хранение данных, тесты |
producer generates synthetic S-parameters and no radar is needed. Mock acquisition | `data_acq_and_processing/` | C++: препроцессор, процессор с режимами обработки и локатором, коллектор E-502, тесты |
supports the LibreVNA models; RadioPhotonic radar (RPH) needs native acquisition. | `run_config_examples/` | профили под каждое железо |
Both LibreVNA modes share one simulator: a point reflector moves back and forth | `run_configs/` | профили для загрузки из веб-интерфейса |
along the antenna array and between depths 3.25–4.25 m over an eight-second cycle. | `deploy/` | служба systemd и обновление с флешки |
Each S21 uses the actual transmitter–target–receiver distance from the configured | `device_firmware/` | прошивки LibreVNA, контроллера ключей и пульта тележки |
XYZ coordinates; coincident antennas produce identical signals. All combinations | `docs/` | документация |
share the same target position and measured capture window, with collections paced | `start.sh` | запуск: окружение Python, сборка, программа |
at 50 ms intervals. The root mock profile explicitly selects a 0–8 m B-scan | `run_config.json` | профиль по умолчанию — симулятор |
range. Both shipped mock profiles use a 0.445 m vertical antenna baseline | `Makefile` | сборка и тесты C++ |
(Rx1/Rx3 and, for the four-output matrix, Tx2/Tx3), avoiding coincident pairs. | `requirements.txt` | Python-пакеты |
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.
SAR3D work grows inversely with the product of the three `SAR_VOXEL_M` steps. Increase them 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 displayed volume, its peak/level, response
widths and background ratio for ten cases. Six cover compensation/normalization,
equalization, background, scene and timing. Four are frames of point reflectors in noise
for CLEAN, which also compare the objects and their boxes: with and without the background
gate, with a scene where the stop at each range's background adds steps, and with a
narrowed scene. The box rules are also checked
one by one on synthetic objects. The expected values come from unchanged functions in cells 1 and 5
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 volume beyond float32, a result too large for the ring slot,
an empty and a very weak signal, and that the views are normalized at their peak on
finer grids. Without a usable GPU it checks only the CUDA warning and the box rules and
skips the rest; the notebook itself is not needed to run it. To regenerate the references,
pass the original notebook and the Horns script (both are kept outside the repository):
```bash
.venv/bin/python data_acq_and_processing/tests/generate_backprojection_reference.py \
--notebook /path/to/Multi_depthnorm_clean.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. Every plot change passes through `PlotController`, which counts changes
of the shown image for the browser stream. The display shows the newest acquisition,
including results read while stopping. 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; sweeps the processor does not return are left out
rather than mixing settings in one image. 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.
@@ -1,101 +1,110 @@
# RadioPhotonic radar (RPH) collector # Коллектор радиофотонного радара (RPH)
Application-owned acquisition binary for the **L-Card E-502** ADC. It captures the Программа сбора для АЦП L-Card E-502, которой владеет приложение. Она принимает
dual-channel main + reference IQ stream and publishes complete `RPH1` sweeps over двухканальный поток IQ (основной и опорный сигналы) и передаёт целые развёртки в
a PTY. `RphRadarService` in `python_app/hardware_full/rph_radar` reads and пакетах `RPH1` через псевдотерминал. Поток читает и обрабатывает `RphRadarService` из
processes that stream for `python_app.scripts.rph_radar_raw_producer`. `python_app/hardware_full/rph_radar` для процесса сбора
`python_app.scripts.rph_radar_raw_producer`.
This is the data *source* for `radar.model == "rph_radar"` with Это источник данных для `radar.model: "rph_radar"` с `radar.rph_radar.source: "e502"`.
`radar.rph_radar.source: "e502"`, paired with the Python `rph_radar_raw_producer`. Источник `rfg_udp` читает UDP-поток платы RFG напрямую и эту программу не использует;
The `rfg_udp` source reads the RFG board's UDP stream directly and does not use this LibreVNA работает через драйверы на Python. Как радар устроен целиком — в главе
binary. LibreVNA acquisition uses Python drivers. [Оборудование](../../docs/hardware.md).
## Layout ## Состав
| Path | Contents | | Путь | Что |
|------|----------| | --- | --- |
| `src/` | Collector implementation (`main.cpp`) and the PTY writer. | | `src/` | коллектор (`main.cpp`) и запись в псевдотерминал |
| `include/` | PTY header and `switch_sequencer.h`. | | `include/` | заголовок псевдотерминала и `switch_sequencer.h` |
| `vendor/lcard/` | L-Card X502/E502 SDK headers (`x502api.h`, `e502api.h`, `lcard_pstdint.h`), vendored verbatim so the collector builds with no external include path. | | `vendor/lcard/` | заголовки SDK L-Card X502/E502 (`x502api.h`, `e502api.h`, `lcard_pstdint.h`) без изменений, чтобы коллектор собирался без внешних путей |
In switch-aware mode, `SwitchSequencer` drives the H7992 switches at actual Коллектор использует общий драйвер ВЧ-ключей H7992 со своим набором заголовков и
DI_SYN2 sweep ends. Duration limits and Stop never advance the switches. Every каталогом объектов `build/rph/`.
physical combination change discards the remaining decoded ADC/DIN block and
recreates the source session after `max(1 ms, run.settling_ms)` has elapsed.
Timestamps never decide whether a sweep belongs to the new combination. The
collector completes a cycle only after a full, clean sweep for each combination.
An unchanged combination keeps the existing session.
A restart stops cyclic DOUT with `WAIT_DONE`, stops and disables the streams, ## Переключение пар
restores DO2 high / DO1 and DO8 low, then closes and frees the SDK descriptor.
After settling, a new descriptor is created, opened and configured. The collector
reloads the DO1/DO8 pattern, waits for `OutCycleSetup(WAIT_DONE)`, and starts
ADC/DIN/DOUT together. Pending samples and phase accumulators are cleared;
source sweep and cycle IDs remain continuous. The initial high DI_SYN2 portion
is dirty: acquisition waits for a complete rising-to-falling sweep. This can
skip a variable number of physical sweeps while reopening; no fixed skip count
or firmware-pause estimate is used. Hardware validation must measure reopen
latency, USB resource release and multi-combination throughput.
The application uses the internal ADC clock. ADC and DIN start together, Если ключами управляет коллектор, `SwitchSequencer` переключает ключи H7992 по
continuously; DI_SYN2 gates sweeps in software. Source times are the monotonic настоящим концам развёрток (спаду DI_SYN2). Ограничение длительности и остановка
clock immediately before `StreamsStart` plus the ADC sample index divided by the ключи не переключают. При каждой смене пары коллектор отбрасывает оставшийся
configured sample rate. These are source-clock estimates, not hardware timestamps; декодированный блок АЦП и цифрового входа и после `max(1 ms, run.settling_ms)`
startup latency shifts the initial epoch earlier. USB and PTY queueing do not создаёт сеанс источника заново. Принадлежность развёртки новой паре по времени не
shift timestamps to the time Python reads a sweep. An estimated source timestamp определяется. Цикл считается законченным, только когда для каждой пары есть целая
ahead of receipt stops acquisition instead of publishing a future capture window. чистая развёртка. Если пара не меняется, сеанс сохраняется.
ADC/host drift can trigger this guard during long single-combination runs; its
bound and the accuracy of capture windows need measurement on hardware. Drift
cannot make data from an old SDK session clean after a physical switch.
The collector uses the common H7992 driver, with its own include set and Перезапуск сеанса: остановка циклического вывода DOUT с `WAIT_DONE`, остановка и
`build/rph/` object tree. отключение потоков, возврат DO2 в высокий уровень, DO1 и DO8 — в низкий, закрытие и
освобождение
дескриптора SDK. После паузы установления создаётся, открывается и настраивается новый
дескриптор, загружается шаблон DO1/DO8, коллектор ждёт `OutCycleSetup(WAIT_DONE)` и
запускает АЦП, цифровой вход и вывод вместе. Накопленные отсчёты и фазовые
накопители сбрасываются; номера развёрток и циклов идут без разрыва. Начальный
высокий участок DI_SYN2 считается испорченным: сбор ждёт целую развёртку от фронта до
спада. Поэтому при переоткрытии пропускается переменное число развёрток; постоянного
числа пропусков или оценки паузы прошивки нет. Время переоткрытия, освобождение
ресурсов USB и скорость при многих парах нужно измерить на железе.
## PTY format ## Время съёма
Each packet is a 48-byte little-endian header (`<4sIQQQQHHI`) followed by the Коллектор работает от внутренних часов АЦП. АЦП и цифровой вход запускаются вместе и
payload: `RPH1`, payload bytes (u32), source sweep ID, cycle ID, start/end ns работают непрерывно; развёртки выделяет программа по линии DI_SYN2. Время отсчёта —
(four u64), input/output positions (two u16), flags (u32; bit 0 means dirty). монотонные часы компьютера непосредственно перед `StreamsStart` плюс номер отсчёта,
Without collector switching both positions are `0xffff`, and cycle equals sweep делённый на частоту выборки. Это оценка по часам источника, а не аппаратная метка:
ID. Payload points remain four u16 words: marker (`0x000a` main, `0x00a8` задержка запуска сдвигает начало отсчёта раньше. Очереди USB и псевдотерминала время
reference), step index, signed I, signed Q. There are no boundary or combo frames. не сдвигают. Если оценка времени отсчёта оказалась позже момента его приёма, сбор
The queue drops complete sweeps on overflow; a packet already being written is останавливается, чтобы не публиковать окно съёма из будущего. Расхождение часов АЦП и
never truncated. Maximum payload is 1,048,544 bytes. Python matches complete компьютера может сработать на этой проверке при долгой работе на одной паре; её
cycles. Python-controlled switching closes the previous collector and PTY reader, предел и точность окон съёма нужно измерить на железе. Данные старого сеанса SDK
then starts a new collector after settling; phase-processing state is preserved. после переключения ключа чистыми не становятся ни при каком расхождении часов.
## Build ## Формат псевдотерминала
Пакет — 48-байтный заголовок в порядке байтов little-endian (`<4sIQQQQHHI`) и данные:
`RPH1`, длина данных (u32), номер развёртки источника, номер цикла, начало и конец
съёма в нс (четыре u64), положения входа и выхода (два u16), флаги (u32; бит 0 —
развёртка испорчена). Без переключения коллектором оба положения равны `0xffff`, а
номер цикла совпадает с номером развёртки. Точка данных — четыре слова u16: метка
(`0x000a` основной сигнал, `0x00a8` опорный), номер ступени, I и Q со знаком. Кадров
границ и пар нет. При переполнении очередь выбрасывает целые развёртки; пакет, который
уже пишется, не обрезается. Наибольший размер данных — 1 048 544 байта. Целые циклы
собирает Python. При переключении ключей из Python прежний коллектор и чтение
псевдотерминала закрываются, а после паузы установления запускается новый коллектор;
состояние фазовой обработки сохраняется.
## Сборка
```bash ```bash
make rph_radar_collector # -> build/bin/rph_radar_collector make rph_radar_collector # -> build/bin/rph_radar_collector
``` ```
It is **not** part of `make all`; `start.sh` builds it automatically when the В `make all` коллектор не входит; `start.sh` собирает его сам, если в профиле
active config selects `radar.model == "rph_radar"`, unless `--skip-build` is set. `radar.model: "rph_radar"`, кроме запуска с `--skip-build`. Сборка в Linux берёт общие
The Linux build uses the repository's common sources and vendored headers and исходники и заголовки из репозитория и линкуется с `-pthread -ldl -lutil`;
links with `-pthread -ldl -lutil`; no installed L-Card SDK is needed to build it. установленный SDK L-Card для сборки не нужен.
## Runtime ## Работа
The collector loads the proprietary `libx502api.so` / `libe502api.so` at run time Закрытые библиотеки `libx502api.so` и `libe502api.so` коллектор загружает при запуске
via `dlopen`, so those libraries must be discoverable by the dynamic loader. через `dlopen`, поэтому их должен находить загрузчик. Сервис RPH ставит
The RPH radar service prepends `~/.local/lib` (their default install location) `~/.local/lib` (их обычное место) в начало пути поиска, если профиль не задаёт
unless the config pins `LD_LIBRARY_PATH` in `radar.rph_radar.env`. `LD_LIBRARY_PATH` в `radar.rph_radar.env`.
There is one acquisition path: USB, differential ADC channels 2/3, maximum Путь съёма один: USB, дифференциальные каналы АЦП 2 и 3, наибольшая частота от
internal-clock rate, rising/falling DI_SYN2 gating, DO1 pair subtraction and DO8 внутренних часов, выделение развёрток по фронту и спаду DI_SYN2, вычитание соседних
reference overlay. `radar.rph_radar.args` sets ADC range, internal reference, отсчётов с разными уровнями DO1 с усреднением и наложение опорного сигнала по DO8.
maximum sweep duration and DO8 period. The example's fixed-path options remain `radar.rph_radar.args` задаёт диапазон АЦП, внутреннюю опорную
accepted as guards; incompatible modes and unused diagnostic options are rejected. частоту, наибольшую длительность развёртки и период DO8. Остальные параметры из
The service appends `tty:<path>` from `radar.rph_radar.tty_path`. примера принимаются только как подтверждение этого пути; несовместимые режимы и
неиспользуемые отладочные параметры отвергаются. Параметр `tty:<путь>` из
`radar.rph_radar.tty_path` добавляет сервис.
When both RF switches use `driver_mode: "native"`, Python passes Если оба ВЧ-ключа в `driver_mode: "native"`, Python передаёт коллектору
`switching:<JSON>` with the already decoded input/output switch settings, combos `switching:<JSON>` с уже прочитанными настройками ключей, парами и паузой
and settling time. The collector reads no profile file. Otherwise, Python selects установления; профиль коллектор не читает. Иначе ключи переключает Python и при смене
the switches and starts a new collector after settling when the combination пары после паузы запускает новый коллектор. Съём наборов калибровки тоже идёт этим
changes. Calibration captures also use this Python-controlled path. путём.
The launching service owns the collector process and forwards its output to the Процессом коллектора владеет запустивший его сервис; вывод коллектора уходит в журнал
application log. On Linux, parent death sends SIGKILL to the collector; normal программы. В Linux смерть родителя завершает коллектор сигналом SIGKILL; обычный
Stop drains and closes the service's own child. No global process-name kill is **Stop** дочитывает и закрывает собственный дочерний процесс сервиса. Завершения
used. процессов по имени нет.
+203
View File
@@ -0,0 +1,203 @@
# Устройство программы
## Процессы и кольца
Программа — четыре процесса (с АЦП E-502 — пять: добавляется коллектор), связанные
кольцевыми буферами в разделяемой памяти POSIX:
```text
raw preprocessed results
железо ─► процесс сбора ─────► data_preprocessor ─────────► data_processor ─────► окно
│ │ │
│ raw_tap │ preprocessed_tap └─► TCP-локатор
└─────► окно └─────► окно
```
| Процесс | Что делает | Код |
| --- | --- | --- |
| окно программы (Python, PyQt6) | запускает и перезапускает остальные процессы, читает результаты и копии, рисует, обслуживает веб-интерфейс и GPIO-кнопку | `python_app/gui/` |
| процесс сбора (Python) | управляет железом, публикует коллекции в `raw` | `python_app/scripts/matrix_raw_producer.py`, `rph_radar_raw_producer.py` |
| препроцессор (C++) | калибровка, вычитание референса, режекторный фильтр, калибровка по объекту | `data_acq_and_processing/preprocessing/` |
| процессор (C++) | режимы обработки, локатор | `data_acq_and_processing/processing/` |
Процесс сбора выбирается по `radar.model` в
`python_app/orchestration/producer_selection.py`; той же функцией пользуются окно и
`start.sh --producer-only`. У радиофотонного радара с АЦП E-502 процесс сбора
запускает ещё и C++-коллектор `rph_radar_collector`, который передаёт развёртки через
псевдотерминал.
Кольца работают по правилу «побеждает новое»: писатель никогда не ждёт читателя, при
переполнении затирается самый старый непрочитанный кадр, а читатель, которого
обогнали во время чтения, продолжает с самого свежего. Окно читает только копии
`raw_tap` и `preprocessed_tap`: если бы оно читало основные кольца, оно отбирало бы
кадры у препроцессора и процессора. Пустое имя копии отключает её.
Раскладка кольца — `RDRRING2`, версия 1: заголовок 64 байта, у каждого слота заголовок
16 байт. Код: Python — `python_app/orchestration/shm/ring_layout.py`, C++ —
`common_cpp/ipc/` (`ShmRing`). Владелец заменяет несовместимый сегмент через unlink, не
обрезая отображения читателей.
## Форматы кадров
| Кадр | Где | Содержимое |
| --- | --- | --- |
| RAW3 | `raw`, `raw_tap`, файлы наборов `python_app/runtime/*_bundle.bin` | коллекция: окно съёма, затем для каждой трассы пара, окно съёма, частоты float32 и S21 complex64. Размер — 44 + пары × (28 + 12 × точки) байт |
| PRP3 | `preprocessed`, `preprocessed_tap` | то же после предобработки |
| RSL3 | `results` | заголовок 64 байта (номер коллекции, метка стадии, время обработки, `settings_revision`, окно съёма, использованная скорость, число результатов) и результаты: трасса, число, изображение или таблица, для всей коллекции и для пар |
Окно съёма обязательно; старые форматы RAW2, PRP2 и RSL2 отвергаются. Python и C++
обновляются только вместе. Подробное описание полей — §6
[librevna_protocol.md](../device_firmware/librevna_protocol.md).
## Идентичность и порядок коллекций
- Коллекция определяется парой `(collection_id, capture_start_ns)`. Номер после
**Stop** и **Start** снова начинается с 1, поэтому одного номера мало.
- Окно съёма ставит процесс сбора по часам CLOCK_MONOTONIC; дальше оно не меняется.
У каждой трассы своё окно: при переключении пар окно коллекции про отдельную пару
ничего не говорит.
- История упорядочена по `capture_start_ns`. `monotonic_ns` — диагностическая метка
стадии.
- Сопоставление стадий, **Single Capture**, **Remove Last Measurement**, кэш 3D SAR и
фильтр приближения GPR используют точный ключ (`python_app/models/collection_identity.py`),
без эвристик по близости времён.
Результаты штатно приходят не по порядку: процессор пересчитывает сохранённые
коллекции при смене настроек, а остановленное окно в режиме B-scan отправляет свои
коллекции обратно в `preprocessed`. Поэтому:
- коллекция не новее последней принятой обрабатывается и публикуется, но не
попадает в историю процессора и не уходит локатору;
- при новой ревизии процессор пересчитывает новейшую коллекцию, в B-scan — всю
историю; локатору уходит только новейшая;
- на время такого пересчёта окно пишет `reprocess_current_result: false`, чтобы
процессор не пересчитывал параллельно;
- история окна вставляет результат по времени съёма; на диск пишутся только новые
коллекции;
- удалённые коллекции (`history_clear_before_ns`, `history_removed`) отбрасываются и
процессором, и окном, даже если их результаты придут позже.
## Окно программы
`gui/app_window.py` только создаёт контроллеры, связывает их и закрывает окно.
Раскладку строит `gui/window_view.py`; секции `gui/controllers/sections/` — наборы
виджетов, сигналы подключают контроллеры. Миксинов и фасадов нет.
| Контроллер (`gui/controllers/`) | Отвечает за |
| --- | --- |
| `profile_controller` | профиль: выбор при запуске, загрузка, сохранение, профиль рядом с данными |
| `config_controller` | перевод полей окна в модели и обратно по метаданным; состояния не хранит |
| `radar_settings_controller` | развёртка, АЦП и лазер, пары сбора, пределы прибора |
| `live_settings_controller` | режим обработки, ревизия, live-файл, пакет настроек из веба |
| `pipeline_controller` | процессы, кольца, опрос и восстановление, **Single Capture**, приостановка, пересчёт B-scan |
| `history_controller` | истории стадий, удаления |
| `plot_controller` и `renderers/` | графики, выбор пары B-scan, проверка ревизии, счётчик изменений картинки для веба |
| `snapshot_controller`, `recording_controller` | сохранение данных, очистка и удаление, запись на диск |
| `preprocess_controller` | наборы, окно предобработки, съём, нейтральные наборы, временный референс |
| `object_calibration_controller`, `object_calibration_capture` | калибровка по объекту |
| `web_controller` | веб-сервер, очередь действий, снимки состояния и картинки |
| `control_button_controller`, `log_controller` | GPIO-кнопка; журнал и окна ошибок |
Поля настроек описаны один раз — метаданными dataclass-моделей в
`python_app/models/gui_profile_schema.py` (их разворачивает `setting_fields.py`).
Из этого описания берутся ключ профиля, виджет, ключ live-файла, поле веб-формы и
проверка при чтении. Подписи веб-формы берутся из подписей полей окна. Qt в моделях и
кодеках нет. Оформление окна — `gui/theme.py`, цвета серий и маркеров —
`gui/controllers/renderers/common.py`, оформление веба — `webui/static/style.css`.
### Правила жизненного цикла
- Приостановка (`gui/runtime/pipeline_pause.py`). Конвейер берёт владение до
остановки и дочитывания колец, потому что дочитывание прокачивает события Qt.
Асинхронный съём держит приостановку до конца. Автоперезапуск при удержанной
приостановке запрещён; продолжается только то, что шло до неё.
- **Stop** и закрытие окна. Пользовательский **Stop** сразу увеличивает счётчик остановок
и отменяет ожидающие продолжения; техническая остановка приостановку не отменяет.
Закрытие сразу запрещает новые запуски; если идёт дочитывание, его владелец
заканчивает и вызывает отложенное закрытие.
- **Start** проверяет всё до того, как что-то остановить. Отказ не трогает работающий
процессор, локатор и историю.
- Ошибка отрисовки не останавливает сбор: она пишется один раз и видна в строке
состояния. Сбор останавливают только ошибки чтения колец.
- Показывается новейшая коллекция: опрос рисует последний результат истории, даже
если последним пришёл пересчёт более старой.
- Графики меняются только через `PlotController`. Он считает изменения картинки, и
веб снимает кадр по этому счётчику. Сигнал `QGraphicsScene.changed` не подключать:
подписка переводит Qt в старый режим обновления, при котором на экране остаются
следы прежних кривых (это проверяет тест
`test_redrawn_views_leave_no_stale_pixels_on_screen`).
- Без экрана и при нажатии GPIO-кнопки окна ошибок не показываются; приостановка
освобождается до показа ошибки.
- Одно действие — один код для кнопки окна, веба и GPIO, с общей проверкой
занятости. Веб-поток трогает Qt только через сигналы с очередью и снимки состояния.
- Синхронный съём (калибровка, референс, нейтральные наборы) идёт в потоке окна
при приостановленном сборе; лишняя развёртка внутри такого нажатия — это зависшее
окно. Съём для калибровки по объекту асинхронный, через обычный конвейер, а расчёт
— в рабочем потоке без Qt.
## Состав кода
| Каталог | Что |
| --- | --- |
| `python_app/gui/` | окно, диалоги, контроллеры, отрисовка |
| `python_app/hardware_full/` | сервисы приборов: `librevna_service.py`, `multi_device_service.py`, `matrix_radar_service.py`, `switched_matrix_radar_service.py`, `point_target_mock_service.py` (симулятор), `switch_service.py` |
| `python_app/hardware_full/librevna_driver/` | USB-драйвер одиночного LibreVNA, протокол версии 14 |
| `python_app/hardware_full/librevna_multi_device_driver/` | синхронная многоприборная развёртка (`cycle_collection.py` — один цикл) |
| `python_app/hardware_full/rph_radar/`, `laser_control/` | радиофотонный радар: коллектор и плата RFG, плата лазеров |
| `python_app/hardware_full/switch_drivers/` | ВЧ-ключи H7992 через GPIO (`gpio_uapi.py` — ioctl ядра без внешних пакетов) |
| `python_app/models/` | профиль: схема, чтение и запись, проверки; модель данных (`dataset_model.py`) |
| `python_app/orchestration/` | запуск процессов (`process_supervisor.py`, `restart_policy.py`), рабочие файлы, кольца (`shm/`) |
| `python_app/storage/npz/` | наборы и наборы данных (`store.py`, `serialize.py`, `snapshot_numpy.py`, `vna_history_json.py`) |
| `python_app/webui/` | веб-сервер FastAPI в процессе окна, страница `static/` |
| `python_app/workflows/` | сценарии съёма и калибровки по объекту |
| `python_app/scripts/` | процессы сбора и утилиты |
| `python_app/tests/` | тесты Python |
| `data_acq_and_processing/common_cpp/` | общий C++: конфигурация (`config/`), кадры и кольца (`ipc/`), драйвер H7992 (`hardware/`) |
| `data_acq_and_processing/preprocessing/` | препроцессор и загрузка наборов |
| `data_acq_and_processing/processing/` | процессор, режимы (`processors/src/`), локатор (`locator/`) |
| `data_acq_and_processing/rph_radar_collector/` | коллектор L-Card E-502 |
| `data_acq_and_processing/tests/` | тесты C++ и эталоны 3D SAR и GPR |
| `data_acq_and_processing/third_party/` | nlohmann/json и Eigen |
| `deploy/` | служба и обновление с флешки |
| `device_firmware/` | прошивки и описания протоколов |
## Где что править
| Задача | Куда смотреть |
| --- | --- |
| ключи профиля и их проверка | `python_app/models/run_config_schema.py`, `run_config_codec.py`, `run_config_validation.py`; то же на стороне C++ — `data_acq_and_processing/common_cpp/config/` |
| поля окна, live-файла и веб-формы | `python_app/models/gui_profile_schema.py`, секции `gui/controllers/sections/`, C++ — `processing/data_processor/src/processing_live_config.cpp` |
| формат кадров | синхронно: C++ `common_cpp/ipc/` (`shared_types.hpp/.cpp`), Python-писатель `storage/npz/serialize.py`, Python-читатель `orchestration/shm/decoder.py`, описание — §6 `librevna_protocol.md` |
| модель данных | `python_app/models/dataset_model.py` |
| драйверы приборов | `python_app/hardware_full/` |
| запуск процессов | `orchestration/process_supervisor.py`, `restart_policy.py`; из окна — `pipeline_controller.py` |
| хранение данных | `python_app/storage/npz/` |
| режимы обработки | `data_acq_and_processing/processing/processors/src/`: `bscan`, `gpr`, `passthrough`, `sar3d`; общий код GPR и 3D SAR — `backprojection.hpp`; отрисовка — `gui/controllers/renderers/` |
| 3D SAR | `sar3d_processor.cpp` (алгоритм), `sar3d_clean.{hpp,cpp}` (фон кадра, CLEAN, рамки), `sar3d_gpu.{hpp,cpp}` (CUDA), `renderers/sar3d.py` |
| сценарии съёма | `gui/controllers/preprocess_controller.py`, `python_app/workflows/` |
| веб-интерфейс | `python_app/webui/`, мост к окну — `gui/controllers/web_controller.py` |
Изменив поведение, поправьте главу документации, где оно описано: ключи профиля —
[configuration.md](configuration.md), поля окна — [desktop.md](desktop.md) и
[processing.md](processing.md), форматы данных — [data.md](data.md), кадры колец и
USB-протокол — `librevna_protocol.md`, переключение стимула —
`per_point_switching.md`.
## 3D SAR и эталон
Алгоритм 3D SAR — порт исследовательского ноутбука `Multi_depthnorm_clean.ipynb`
(ячейки 1 и 5; хранится вне репозитория). Комментарии в `sar3d_processor.cpp` и
`sar3d_clean.cpp` называют функции и константы ноутбука, поля окна — ровно его имена.
Величины ноутбука, не вынесенные в окно, — константы в начале этих файлов.
Решения CLEAN и рамок зависят от точных равенств (углы сетки лежат на границе окна
дальности, полуразмеры окон кратны шагам), поэтому граница на видеокарте считается в
double без FMA (`__dmul_rn`, `__dadd_rn`), а хост-код 3D SAR собирается с
`-ffp-contract=off` (`SAR3D_HOST_FLAGS` в `Makefile`). Иначе на машинах с FMA (aarch64,
x86 с `-mfma`) результат расходился бы с ноутбуком.
Сверка с ноутбуком автоматизирована (глава [Разработка](development.md)): генератор
выполняет неизменённые функции ноутбука и пишет эталон `sar3d_reference.npz`, тест
гоняет настоящий CUDA-бэкенд и сравнивает с ним. Без видеокарты проверяются только
предупреждение и правила рамок, поэтому после правки 3D SAR сверку нужно прогнать на
настоящей карте. GPR сверяется с `gpr_reference.npz` на CPU, без видеокарты.
+428
View File
@@ -0,0 +1,428 @@
# Файл конфигурации
Все настройки программы хранятся в профиле — файле JSON. В профиле две части:
конфигурация сбора и обработки (разделы `radar`, `switches`, `control_button`,
`logging`, `run`, `preprocess`, `gpr`, `rings`) и состояние окна (раздел `gui`).
Незнакомые термины (полоса ПЧ, дБм, пара, коллекция) — в [словаре](glossary.md).
| Где | Что |
| --- | --- |
| `run_config.json` в корне | профиль для разработки: симулятор многоприборной схемы, `driver_mode: "mock"`, уровень журнала `debug` |
| `run_config_examples/` | готовые профили под каждое железо (таблица ниже) |
| `run_configs/` | профили, которые можно загрузить из веб-интерфейса |
| `python_app/runtime/run_config.json` | рабочая копия, которую программа пишет при каждом **Start**; по ней работают процессы конвейера |
> **Внимание.** Корневой `run_config.json` — профиль симулятора: с ним программа не
> обращается к железу. На машине с радаром работайте с профилем из
> `run_config_examples/`, скопированным и настроенным под установку.
## Как профиль загружается и сохраняется
При запуске программа берёт профиль по порядку:
1. путь из переменной окружения `RADAR_SYSTEM_PROFILE` (её задаёт
`./start.sh --profile ПУТЬ`);
2. последний использованный профиль из `python_app/runtime/gui_session_state.json`;
3. корневой `run_config.json`.
Относительный путь отсчитывается от каталога проекта. Если выбранный профиль не
читается, программа пишет
`Failed to load the last selected config profile; falling back to root run_config.json.`
и загружает корневой; если не читается файл сеанса —
`Failed to read GUI session-state; using root run_config.json.` Если не читается и
корневой профиль, программа не запускается. Профиль без раздела `gui` загружается со
значениями окна по умолчанию; при запуске (кроме корневого профиля) об этом пишется
`Loaded legacy run config without GUI defaults: <путь>`, при **Load Config…** —
`kind=legacy run config`
в строке `Config profile loaded: …`.
**Load Config…** загружает другой профиль; это возможно только при остановленном
сборе (`Stop all pipeline processes before loading a config profile`) и не во время
съёма наборов.
**Save Config…** записывает текущие значения всех полей окна, выбранные наборы и
геометрию антенн. Загруженный или сохранённый профиль становится стартовым для
следующего запуска.
> **Внимание.** Изменения в окне и в веб-интерфейсе, в том числе выбор наборов и
> временный референс, в профиль сами не записываются. После перезапуска программы они
> пропадут, если не нажать **Save Config…**.
Правила чтения:
- нет раздела или ключа — берётся значение по умолчанию. Умолчания схемы — минимальные
заглушки (`points: 1`, `if_bandwidth_hz: 1.0`, пустые имена колец), а не рабочие
настройки: начинайте с профиля-примера;
- типы строгие: целое число нельзя записать как `5.0`, логическое — как `1`; ошибка
называет ключ (`points must be a JSON integer`);
- неизвестные ключи пропускаются и при сохранении исчезают;
- раздел `gui` с неизвестным номером версии отвергается (`Unsupported gui.version: N`).
## Когда изменение вступает в силу
| Обозначение | Когда |
| --- | --- |
| сразу | сразу, в том числе во время сбора |
| Apply Radar | при нажатии **Apply Radar** (без экрана — автоматически при запуске) |
| Start | при следующем **Start** или **Single Capture**: программа заново пишет рабочую копию профиля и перезапускает процессы сбора и предобработки |
| Start, процессор | как Start, но перезапускается и процессор, а с ним TCP-локатор: его клиенты отключаются, история обработки очищается |
| профиль | ключа нет в окне: правьте файл и загрузите его через **Load Config…** при остановленном сборе |
Процессор перезапускается, если изменились: ключ радара (`radar_key`), опорная частота
многоприборной схемы, `driver_mode` радара и ключей, число положений ключей, список
пар, любой ключ раздела `rings`, любой ключ `run.locator_server`, путь к live-файлу
или `run.idle_sleep_ms`. Смена наборов предобработки процессор не перезапускает
(исключение — нейтральные наборы).
## Ключ радара
`radar_key` — строка, по которой программа находит наборы калибровки и референса:
они лежат в каталогах `python_app/data/<вид набора>/<radar_key>/`. Смена любой
величины, из которой строится ключ, скрывает наборы, снятые при старых значениях.
| Радар | Из чего строится | Пример (из профилей-примеров) |
| --- | --- | --- |
| `librevna` | модель, серийный номер (`no_serial`, если пуст), начало и конец развёртки, число точек, полоса ПЧ, мощность | `librevna_no_serial_st1000000_sp6000000000_p201_if50000_pw-10` |
| `librevna_multi` | то же для мастера и обоих ведомых, плюс положения ключей `_out`, `_in`, `_stim` | `librevna_multi_209E307A5532_20A1307D5532_2072306C5532_st500000000_sp6000000000_p1031_if50000_pw-3_out1_in1_stim2` |
| `rph_radar` | первые 16 знаков хеша SHA-256 от выходной полосы, фазовой калибровки, параметров коллектора (`args`) или источника `rfg_udp`, числа положений ключей и настроек лазера (если плата лазеров включена или источник — RFG) | `rph_radar_2afdaa32e154b122` |
`dwell_us`, путь к программам и тайм-ауты в ключ не входят.
## Профили-примеры
| Файл | Радар | Что в нём |
| --- | --- | --- |
| `run_config.json` (корневой) | `librevna_multi`, `mock` | симулятор схемы с переключением стимула: 16 пар, 1 МГц–6 ГГц, 201 точка, вертикальная база антенн 0,445 м, журнал `debug` |
| `run_config_librevna.example.json` | `librevna`, `native` | одиночный LibreVNA, выходной ключ на 2 положения (линии 17, 27), входной на 4 (линии 22, 23), 8 пар, 1 МГц–6 ГГц, 201 точка |
| `run_config_librevna_multi.example.json` | `librevna_multi`, `native` | приборы `209E307A5532` (мастер), `20A1307D5532`, `2072306C5532`; 8 пар, 1 МГц–6 ГГц, 201 точка |
| `run_config_librevna_multi.example2.json` | `librevna_multi`, `native` | то же для второго комплекта: `206830985532`, `206930965532`, `206930A15532` |
| `run_config_librevna_multi_per_point.example.json` | `librevna_multi`, `native` | переключение стимула, комплект `209E307A5532`; 16 пар, 0,5–6 ГГц, 1031 точка, −3 дБм, `dwell_us: 2`, измеренная геометрия антенн; режим `sar3d` |
| `run_config_librevna_multi_per_point_lowband.example.json` | `librevna_multi`, `native` | переключение стимула, комплект `206830985532`; 30–200 МГц, 201 точка — проверка ключей на низких частотах; у антенн y = z = 0, Tx2 и Tx3 совпадают с Tx0 и Tx1 |
| `run_config_rph_radar.example.json` | `rph_radar`, `e502` | АЦП E-502, плата лазеров на `/dev/ttyUSB0` в режиме `variation`, ключи на 2 и 4 положения, 8 пар |
| `run_config_rph_radar_rfg.example.json` | `rph_radar`, `rfg_udp` | плата RFG на `192.168.2.1:5000`, плата лазеров на `/dev/serial/by-id/usb-Silicon_Labs_CP2102_…`, одна пара, измеренные фазовые якоря |
| `run_config_simulator.example.json` | `librevna`, `mock` | симулятор одиночного LibreVNA: 8 пар, 1 МГц–6 ГГц, 201 точка; режим `gpr` |
`run_configs/run_config.json` — профиль симулятора одиночного LibreVNA для
веб-интерфейса, близкий к `run_config_simulator.example.json`.
## radar
| Ключ | Тип, по умолчанию | Что делает | Когда |
| --- | --- | --- | --- |
| `radar.model` | строка, `"librevna"` | тип радара: `"librevna"`, `"librevna_multi"` или `"rph_radar"`. Старое имя `"kamil_adc"` отвергается с подсказкой перейти на `rph_radar` | профиль; Start, процессор |
| `radar.serial` | строка, `""` | серийный номер прибора; у многоприборной схемы — мастера (обязателен). Пустой у одиночного — первый найденный прибор | профиль; Start, процессор |
| `radar.driver_mode` | строка, `"mock"` | `"native"` — настоящее железо, `"mock"` — симулятор. Радиофотонному радару нужен `"native"` | профиль; Start, процессор |
### radar.sweep
Развёртка LibreVNA. Радиофотонный радар эти ключи не использует (у него
`radar.rph_radar.band`), а `points` у него в профиль не записывается. Поля редактируются в секции
**Radar**; с подключённым прибором они подрезаются под его пределы.
| Ключ | Тип, по умолчанию | Поле | Что делает | Ключ радара |
| --- | --- | --- | --- | --- |
| `start_hz` | число, `1000000.0` | **Start (Hz)** | начало развёртки, Гц, не меньше 0 | да |
| `stop_hz` | число, `6000000000.0` | **Stop (Hz)** | конец развёртки, Гц, больше начала | да |
| `points` | целое, `1` | **Points** | число частотных точек | да |
| `if_bandwidth_hz` | число, `1.0` | **IF bandwidth (Hz)** | полоса ПЧ, Гц, больше 0 | да |
| `stimulus_power_dbm` | число, `-30.0` | **Stimulus power (dBm)** | мощность излучения, дБм | да |
| `dwell_us` | целое, `0` | — | дополнительная задержка на каждой стадии прибора, мкс; при отправке значение молча ограничивается диапазоном 0–65535, а прибор — 10 239 мкс. В режиме переключения стимула в это время контроллер переключает ключ; в примерах `2` | нет |
Изменения применяются при **Start**; у одиночного LibreVNA — ещё и при
**Apply Radar**.
### radar.multi_device
Только для `librevna_multi`; другие радары эти ключи игнорируют. В окне не
редактируются.
| Ключ | Тип, по умолчанию | Что делает |
| --- | --- | --- |
| `slave_serials` | список строк, `[]` | серийные номера двух ведомых, ровно два. Первый даёт входы 0 и 1, второй — 2 и 3; порядок важен. Можно записать строкой через запятую; старое имя `slave_serial_numbers` тоже читается |
| `force_external_reference` | логическое, `false` | внешняя опорная частота 10 МГц на всех трёх приборах. Включайте, только если опора подана на все приборы (глава [Оборудование](hardware.md)) |
| `recovery_attempts` | целое, `3` | сколько раз переподключать приборы после сбоя, прежде чем передать ошибку процессу сбора |
| `output_switch_positions` | целое, `1` | положения настоящего ВЧ-ключа на выходах мастера, которым управляет компьютер (линии из `switches.port1`). 1 — ключа нет. Ни один пример его не использует |
| `input_switch_positions` | целое, `1` | то же для входов ведомых (линии из `switches.port2`) |
| `stimulus_switch_positions` | целое, `1` | положения ключа переключения стимула внутри частотной точки; `2` — режим переключения стимула. Прошивка мастера поддерживает 1, 2 и 4, контроллер STM32 — 2 |
Все ключи, кроме `recovery_attempts`, входят в ключ радара или в условие перезапуска
процессора.
### radar.rph_radar
Только для `rph_radar`. Поля без пометки «—» редактируются в секции **Radar**.
| Ключ | Тип, по умолчанию | Поле | Что делает |
| --- | --- | --- | --- |
| `source` | строка, `"e502"` | — | источник: `"e502"` — АЦП L-Card через `rph_radar_collector`, `"rfg_udp"` — плата RFG по UDP |
| `udp_bind` | строка, `"0.0.0.0:5000"` | — | только RFG: адрес `IPv4:порт`, на котором принимаются пакеты |
| `project_dir` | строка, `""` | **Project dir** | рабочий каталог коллектора; пусто — каталог проекта |
| `executable_path` | строка, `""` | **Executable path** | путь к коллектору, обычно `build/bin/rph_radar_collector` |
| `tty_path` | строка, `""` | **TTY path** | путь псевдотерминала, через который коллектор передаёт развёртки; старый файл по этому пути удаляется перед запуском |
| `args` | список строк, `[]` | **Arguments** (по строке на параметр) | параметры коллектора (глава [Оборудование](hardware.md)); `tty:` здесь задавать нельзя |
| `env` | объект строк, `{}` | **Environment JSON** | переменные окружения коллектора; `~` и `$ПЕРЕМЕННАЯ` раскрываются. Если `env` не задаёт `LD_LIBRARY_PATH`, в начало унаследованного `LD_LIBRARY_PATH` добавляется `~/.local/lib` |
| `startup_timeout_s` | число, `5.0` | **Startup timeout (s)** | сколько ждать появления псевдотерминала после запуска коллектора |
| `sweep_timeout_s` | число, `5.0` | **Read timeout (s)** | сколько ждать пригодной развёртки, покрывающей полосу, прежде чем переподключиться |
| `stop_timeout_s` | число, `2.0` | **Stop timeout (s)** | ожидание остановки коллектора; на деле не больше 0,5 с, затем он завершается принудительно |
| `phase_calibration.phase0_rad`, `freq0_hz` | `0.0`, `2046000000.0` | — | первая опорная точка «фаза опорного сигнала — частота» |
| `phase_calibration.phase1_rad`, `freq1_hz` | `300.0`, `5612000000.0` | — | вторая опорная точка; фазы и частоты точек должны различаться |
| `band.start_hz`, `band.stop_hz` | `2100000000.0`, `5500000000.0` | — | выходная полоса: развёртка пересчитывается на её сетку; развёртка, не покрывающая полосу, отбрасывается |
| `band.points` | `2048` | — | число точек выходной сетки, не меньше 2 |
Для `source: "e502"` поля `executable_path` и `tty_path` обязательны, а `project_dir`, если
задан, должен быть каталогом. Все три тайм-аута должны быть конечными положительными
числами (`radar.rph_radar.<ключ> must be finite and positive`).
Фазы `phase0_rad` и `phase1_rad` записывает программа фазовой калибровки
(`rph_radar_calibrate --apply`). Полоса и якоря входят в ключ радара, поэтому после
новой фазовой калибровки старые наборы не видны.
### radar.laser_control
Плата лазеров радиофотонного радара. Поля — в секции **Radar**, применяются кнопкой
**Apply Radar**.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `enabled` | `false` | программировать плату при **Apply Radar** |
| `port` | `""` | последовательный порт платы, 115 200 бод |
| `mode` | `"manual"` | `"manual"` — постоянные токи и температуры, `"variation"` — развёртка тока LD1 (плате RFG нужен этот режим) |
| `pi_coeff1_p`, `pi_coeff1_i` | `1280`, `25` | коэффициенты ПИ-регулятора первого канала |
| `pi_coeff2_p`, `pi_coeff2_i` | `1280`, `179` | коэффициенты ПИ-регулятора второго канала |
| `manual.temp1`, `manual.temp2` | `24.0`, `24.9` | температуры лазеров, °C, 15–40 |
| `manual.current1`, `manual.current2` | `97.3`, `102.0` | токи лазеров, мА, 15–110 |
| `variation.variation_type` | `"CHANGE_CURRENT_LD1"` | что меняется; другого варианта нет |
| `variation.static_temp1`, `static_temp2` | `24.0`, `24.9` | температуры во время развёртки |
| `variation.static_current1`, `static_current2` | `97.3`, `102.0` | постоянные токи во время развёртки |
| `variation.min_value`, `max_value` | `97.3`, `107.0` | пределы развёртки тока LD1, мА, начало меньше конца |
| `variation.step` | `0.05` | шаг тока, мА, 0,002–0,5 |
| `variation.time_step` | `50` | длительность ступени, мкс, 20–100 |
| `variation.delay_time` | `10` | пауза между развёртками, мс, 3–10 |
## switches
Два ВЧ-ключа: `port1` — выходной (передающие антенны), `port2` — входной (приёмные).
Таблица положений — в главе [Оборудование](hardware.md).
| Ключ | Тип, по умолчанию | Что делает |
| --- | --- | --- |
| `name` | строка, `""` | имя ключа в сообщениях и в GPIO (`radar_<имя>`) |
| `driver_mode` | `"mock"` | `"native"` — управлять линиями GPIO, `"mock"` — только помнить положение |
| `radar_port` | целое, `0` | порт прибора, к которому подключён ключ: 1 или 2. Задайте его явно: `0` не проходит проверку при **Start** (`switches.<имя>.radar_port must be 1 or 2`) |
| `positions` | `1` | число положений, у настоящего ключа 1–4. Задаёт число выходов или входов матрицы |
| `default_position` | `0` | положение после открытия ключа, меньше `positions` |
| `gpio_chip` | `""` | GPIO-чип, например `/dev/gpiochip0`; для `native` обязателен |
| `pin_a`, `pin_b` | `-1` | номера линий A и B на чипе; для `native` — разные и неотрицательные |
Поле `driver` допускается только со значением `"h7992"`, `invert_logic` — только
`false`; оба при сохранении убираются. Другие значения отвергаются ещё до обращения к
GPIO. У многоприборной схемы программа сама переписывает в этих разделах `driver_mode`
(`"mock"`), `radar_port`, `positions` (положения настоящего ключа × выходы или входы
приборов) и `default_position` (`0`): значения из файла не используются. Читаются
только `name`, `gpio_chip`, `pin_a` и `pin_b` — для настоящих ключей на выходах мастера
или входах ведомых, если `multi_device.output_switch_positions` или
`input_switch_positions` больше 1; их режим берётся из `radar.driver_mode`. Изменения
применяются при **Start**;
число положений и режим перезапускают процессор.
## control_button
GPIO-кнопка, которая снимает временный референс. Настройки читаются один раз при
запуске программы.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `enabled` | `false` | включить кнопку |
| `gpio_chip` | `"/dev/gpiochip0"` | GPIO-чип |
| `pin` | `-1` | номер линии на чипе (не номер контакта разъёма); отрицательный — кнопка не включается |
| `active_low` | `true` | `true` — нажатие опускает линию в 0 (кнопка на землю), `false` — поднимает в 1 |
| `bias` | `""` | подтяжка: `"pull_up"`, `"pull_down"`, `"disabled"`; пусто — к питанию при `active_low`, к земле иначе |
| `debounce_ms` | `50` | подавление дребезга средствами ядра, мс; 0 — без подавления |
| `action` | `"capture_tmp_reference"` | действие по нажатию; другого нет |
## logging
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `level` | `"info"` | наименьший уровень записей журнала: `debug`, `info`, `warning` или `error`. Меняется списком **Level** над журналом окна; записывается при **Save Config…** |
На уровне `debug` в журнал попадают трассы тайминга переключений; корневой профиль
работает с ним.
## run
| Ключ | Тип, по умолчанию | Что делает | Когда |
| --- | --- | --- | --- |
| `settling_ms` | целое, `0` | пауза после переключения ВЧ-ключей перед развёрткой, мс. Поле **Settling (ms)** | Start |
| `idle_sleep_ms` | целое, `2` | сколько процессы предобработки и обработки спят, когда нет входных данных, мс | Start, процессор |
| `continuous` | логическое, `false` | непрерывный сбор или одна коллекция. Программа сама ставит `true` при **Start** и `false` при **Single Capture**; значение из профиля важно только для `./start.sh --producer-only` | — |
| `processing_live_config_path` | строка, `""` | путь к live-файлу. Программа всегда подставляет `python_app/runtime/processing_live.json` своего каталога | — |
| `combos` | список `{"input", "output"}`, `[]` | пары сбора в порядке обхода (ниже) | Start, процессор |
| `locator_server` | объект | TCP-локатор (ниже) | Start, процессор |
### run.combos
Список пар `{"input": вход, "output": выход}`; пустой список — полная матрица:
выходы по порядку, внутри каждого — все входы. Пар не больше 4096, повторы и номера
вне матрицы отвергаются.
- Одиночный LibreVNA и радиофотонный радар: при **Start** список берётся из окна —
**Run combos** (`вход:выход` через запятую, пусто — полная матрица) или
**Single combo**. В режимах pass-through и B-scan, если оба ключа `mock` или у обоих
не больше одного положения, программа снимает только пару `0:0`.
- Многоприборная схема: список всегда равен полной матрице; в окне он показан, но не
редактируется.
### run.locator_server
TCP-сервер, который отдаёт объекты GPR внешним программам и принимает от них
скорость (протокол — в главе [Веб-интерфейс, кнопка и локатор](web.md)). В окне не
редактируется. Старое имя раздела `run.locator` тоже читается.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `host` | `"0.0.0.0"` | адрес, на котором сервер ждёт клиентов; `0.0.0.0` — все интерфейсы |
| `port` | `8888` | TCP-порт, 1–65535 |
| `device_id` | `3` | номер устройства в заголовке каждого пакета |
| `protocol_version` | `1` | поле `ver` в пакетах |
| `max_payload_bytes` | `65536` | наибольший размер входящего пакета; клиент с большим пакетом отключается |
| `client_queue_size` | `32` | очередь пакетов на клиента; при переполнении выбрасываются старые |
| `logger_name` | `"locator_runtime"` | не используется |
Ключ `enabled` в этом разделе программа не сохраняет: локатор включён всегда.
## preprocess
| Ключ | Что делает |
| --- | --- |
| `s21.calibration.set_name` | имя выбранного набора калибровки (through) |
| `s21.reference.set_name` | имя выбранного набора референса |
| `s21.object_calibration.set_name` | имя выбранной калибровки по объекту; пусто — не применять |
| `s21.*.bundle_path` | путь к файлу набора для препроцессора; программа заполняет его сама при **Start**, в профиле — пусто |
| `notch.enabled` | включить режекторный фильтр, по умолчанию `false` |
| `notch.bands_hz` | полосы подавления `[нижняя, верхняя]`, Гц, по умолчанию `[]` |
| `notch.taper_width_hz` | ширина плавного края, Гц, не меньше 0, по умолчанию `40000000.0` |
| `notch.taper_type` | форма края: `"cosine"` (по умолчанию) или `"hard"`; как работает фильтр — в главе [Калибровка и предобработка](preprocessing.md) |
Наборы выбираются в окне **Preprocessing Setup** и записываются в профиль при
**Save Config…**. Изменения применяются при **Start**.
## gpr
Геометрия антенн для GPR и 3D SAR и для симулятора. Редактируется таблицами
**Tx geometry** и **Rx geometry** на страницах GPR и 3D SAR; правка таблицы сразу
уходит процессору.
| Ключ | Запись | Что делает |
| --- | --- | --- |
| `tx_geometry` | `{"output_pos", "x_m", "y_m", "z_m"}` | положения передающих антенн: номер выхода и координаты в осях радара, м |
| `rx_geometry` | `{"input_pos", "x_m", "y_m", "z_m"}` | положения приёмных антенн: номер входа и координаты, м |
Записей может быть меньше, чем выходов или входов: антенны без записи в GPR и 3D SAR
не участвуют. Номер должен быть меньше числа выходов (входов), без повторов, иначе
`gpr.tx_geometry output_pos is out of range [0, N)`. При сужении матрицы лишние
записи нужно удалить. Отсутствующие `y_m` и `z_m` считаются нулями. Оси — в главе
[Как работает радар](principles.md).
## rings
Пять колец разделяемой памяти, через которые процессы передают данные
(глава [Устройство программы](architecture.md)):
| Кольцо | Кто пишет | Кто читает |
| --- | --- | --- |
| `raw` | процесс сбора | препроцессор |
| `raw_tap` | процесс сбора, копия | окно программы |
| `preprocessed` | препроцессор | процессор |
| `preprocessed_tap` | препроцессор, копия | окно программы |
| `results` | процессор | окно программы |
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `name` | `""` | имя сегмента разделяемой памяти, например `/radar_raw`. Пустое имя у `raw_tap` или `preprocessed_tap` отключает копию и соответствующую историю окна |
| `capacity` | `1` | сколько кадров помещается в кольцо; при переполнении затирается самый старый |
| `slot_size_bytes` | `4096` | наибольший размер кадра, байт, кратный 8 |
Слоты `raw`, `preprocessed` и включённых копий должны вмещать коллекцию:
44 + пары × (28 + 12 × точки) байт. Например, 16 пар по 1031 точке требуют 198 444
байт. Программа проверяет это до запуска
(`rings.raw.slot_size_bytes must be at least N for this sweep`). Слот `results` должен
вмещать результат; изображения 3D SAR при настройках по умолчанию занимают около
1 МБ. Во всех примерах слоты по 2 МиБ (`2097152`) и ёмкость 50.
Ёмкость влияет и на историю: процессор хранит до 50 коллекций, но не больше
ёмкости `preprocessed` и `results`, а B-scan во время сбора показывает столько же. В
GPR и 3D SAR процессор при этом хранит не меньше, чем нужно обработке: 1 +
**Mean count** (если включено вычитание среднего) + **SAR_FRAME_RADIUS** (в 3D SAR),
независимо от ёмкости колец.
Калибровке по объекту нужно кольцо `preprocessed_tap`.
## gui
Состояние окна: значения всех полей, которые программа запоминает. Раздел
записывается целиком при **Save Config…**. Если его нет, программа берёт значения по
умолчанию, а часть вычисляет из профиля: номера антенн — из геометрии и списка пар,
окно видимости GPR — из положения антенн, каталог сохранения —
`python_app/data/snapshots`.
| Ключ | Поле в окне | Смысл |
| --- | --- | --- |
| `version` | — | версия раздела, всегда `1` |
| `switches.combo_mode` | кнопки **Select** у **Run combos** и **Single combo** | откуда брать пары сбора: `"text"` или `"single"` |
| `switches.combos_text` | **Run combos** | список пар `вход:выход` |
| `switches.single_input`, `single_output` | **Input**, **Output** в строке **Single combo** | одна пара |
| `processing.selected_mode` | **Mode** | режим обработки: `pass_through`, `bscan`, `gpr`, `sar3d` |
| `processing.pass_through.*` | страница pass-through | `show_magnitude`, `show_phase`, `unwrap_phase`, `combo_filter`, `fixed_y_enabled`, `y_min_db`, `y_max_db` |
| `processing.bscan.*` | страница B-scan | `axis`, `cut_m`, `max_depth_m`, `gain`, `start_freq_mhz`, `stop_freq_mhz`, `subtract_mean_ascan`, `history_window_scans` |
| `processing.gpr.*` | страница GPR | `input_positions`, `output_positions`, `min_depth_m`, `max_depth_m`, `range_comp_power`, `object_min_frac`, `score_mode`, `motion_mode`, `look_angle_deg`, `direction_sign`, `speed_m_s`, `ignore_socket_speed_enabled`, `max_detected_objects_to_draw`, `draw_top_m_objects`, `object_approach_min_frames`, `start_freq_mhz`, `stop_freq_mhz`, `background_subtract_enabled`, `background_mean_count`, `remove_sidelobe_objects_enabled`, `bp_ground_depth_m`, `pair_depth_equalize`, `render_mode`, `visible_x_min_m`, `visible_x_max_m`, `visible_z_min_m`, `visible_z_max_m` |
| `processing.sar3d.*` | страница 3D SAR | `input_positions`, `output_positions`, `min_depth_m`, `max_depth_m`, `start_freq_mhz`, `stop_freq_mhz`, `speed_m_s`, `ignore_socket_speed_enabled`, `look_angle_deg`, `frame_radius`, `voxel_x_m`, `voxel_y_m`, `voxel_z_m`, `bp_ground_depth_m`, `comp_range_power`, `use_compensation`, `normalize_each_frame`, `pair_depth_equalize`, `background_subtract_enabled`, `background_mean_count`, `bg_gate_enabled`, `clean_enabled`, `x_size_m`, `y_min_m`, `y_max_m`, `z_size_m`, `cloud_threshold`, `zoom_percent`, `mark_enabled` |
| `data_actions.save_path` | **Path** | каталог для сохранения данных; относительный путь — от рабочего каталога процесса |
| `data_actions.save_name` | **Name** | имя сохраняемого набора данных, по умолчанию `snapshot_manual` |
| `data_actions.record_count` | **Sweeps to record** | сколько коллекций записать на диск, по умолчанию 100 |
| `preprocess_dialog.set_name` | **Set name** | имя нового набора, по умолчанию `set_001` |
| `preprocess_dialog.radar_config_dir` | **Directory** | каталог вариантов радара |
| `preprocess_dialog.use_all_radar_configs` | **Capture current combo for all found radar configs** | съём по всем вариантам |
| `preprocess_dialog.median_sweep_count` | **Sweeps per combo (median)** | развёрток на пару, по умолчанию 5 |
Ключи страниц обработки названы по полям окна; что делает каждое поле, сколько оно по
умолчанию и в каких пределах — в главе [Режимы обработки](processing.md). Типы,
варианты выбора, объявленные в схеме границы и версия проверяются при чтении, и
нарушение отвергает весь профиль: `gui.processing.gpr.score_mode must be one of: peak, combined`,
`gui.processing.sar3d.zoom_percent must be <= 1000`. Значение в пределах схемы, но вне
пределов поля окна поле молча подрезает к своей границе (например, `frame_radius: 1000`
станет 24), и подрезанное значение запишется при следующем **Save Config…**.
`preprocess_dialog.median_sweep_count` меньше 1 поднимается до 1.
## processing_live.json
Живые настройки процессора. Файл `python_app/runtime/processing_live.json` пишет
программа при каждом изменении настройки обработки, смене режима, правке геометрии,
очистке истории и удалении последнего измерения; читает его только процессор. Править
его вручную не нужно: программа перезаписывает его целиком.
В файле 53 ключа. Ключи настроек обработки соответствуют полям окна:
| Ключ | Поле |
| --- | --- |
| `processor_mode` | **Mode** |
| `bscan_axis`, `bscan_cut_m`, `bscan_max_depth_m`, `bscan_gain`, `bscan_start_freq_mhz`, `bscan_stop_freq_mhz` | поля B-scan |
| `gpr_input_positions`, `gpr_output_positions`, `gpr_min_depth_m`, `gpr_max_depth_m`, `gpr_start_freq_mhz`, `gpr_stop_freq_mhz`, `gpr_speed_m_s`, `gpr_look_angle_deg`, `ignore_socket_speed`, `gpr_background_subtract_enabled`, `gpr_background_mean_count`, `pair_depth_equalize` | общие поля GPR и 3D SAR — со страницы выбранного режима |
| `gpr_range_comp_power`, `gpr_object_min_frac`, `gpr_score_mode`, `gpr_motion_mode`, `gpr_direction_sign`, `gpr_max_detected_objects_to_draw`, `gpr_draw_top_m_objects`, `gpr_object_approach_min_frames`, `gpr_remove_sidelobe_objects_enabled`, `gpr_bp_ground_depth_m`, `gpr_visible_x_min_m`, `gpr_visible_x_max_m`, `gpr_visible_z_min_m`, `gpr_visible_z_max_m` | поля GPR |
| `sar3d_frame_radius`, `sar3d_voxel_x_m`, `sar3d_voxel_y_m`, `sar3d_voxel_z_m`, `sar3d_bp_ground_depth_m`, `sar3d_comp_range_power`, `sar3d_use_compensation`, `sar3d_normalize_each_frame`, `sar3d_x_size_m`, `sar3d_y_min_m`, `sar3d_y_max_m`, `sar3d_z_size_m`, `sar3d_bg_gate_enabled`, `sar3d_clean_enabled` | поля 3D SAR |
| `tx_geometry`, `rx_geometry` | таблицы геометрии; заменяют `gpr` профиля в процессоре |
Служебные ключи:
| Ключ | Что делает |
| --- | --- |
| `settings_revision` | номер ревизии настроек (время записи по монотонным часам, нс). Процессор возвращает его в каждом результате, и окно рисует GPR и 3D SAR только текущей ревизии |
| `reprocess_current_result` | пересчитывать ли сохранённые коллекции при новой ревизии; `false` — пока окно само пересчитывает остановленный B-scan |
| `history_clear_before_ns` | граница **Clear Runtime History**: коллекции, начатые не позже этого момента, удаляются из истории процессора |
| `history_removed` | пары `{"collection_id", "capture_start_ns"}`, удалённые **Remove Last Measurement**; список очищается при **Clear Runtime History** |
Процессор отвергает файл целиком, если в нём ошибка JSON, число не того типа или вне
float32, неизвестное значение `processor_mode`, `gpr_score_mode` или
`gpr_motion_mode`, геометрия с номером вне матрицы или с повтором, отрицательные
`sar3d_x_size_m` или `sar3d_z_size_m`, `sar3d_y_max_m` не больше `sar3d_y_min_m`.
Тогда он работает с последними принятыми настройками и пишет
`data_processor warning: failed to refresh live processing config: …` в
`python_app/runtime/logs/data_processor.err.log`. Неизвестные ключи пропускаются.
+225
View File
@@ -0,0 +1,225 @@
# Данные на диске
Программа хранит данные в трёх местах: наборы предобработки и наборы данных — в
`python_app/data/` (или в каталоге, выбранном в **Path**), своё рабочее состояние — в
`python_app/runtime/`, обмен между процессами — в разделяемой памяти `/dev/shm`.
Каталоги `python_app/data*` и `python_app/runtime` в git не попадают.
> **Внимание.** Наборы калибровки и референса в `python_app/data/` — незаменимые
> измерения. При обновлении программы их переносит `update.sh`; при ручном переносе
> копируйте этот каталог целиком.
## Где что лежит
| Путь | Что | Кто пишет |
| --- | --- | --- |
| `python_app/data/s21_calibration/<radar_key>/<имя>.npz`, `.json` | наборы калибровки | окно предобработки |
| `python_app/data/s21_reference/<radar_key>/<имя>.npz`, `.json` | наборы референса, в том числе `tmp_reference` | окно предобработки, временный референс |
| `python_app/data/s21_object_calibration/<radar_key>/<имя>.npz`, `.json` | калибровки по объекту | окно калибровки по объекту |
| `python_app/data/preview_png/<вид>/<имя>/<radar_key>/i<вход>_o<выход>.png` | картинки развёрток при съёме по вариантам радара | окно предобработки |
| `<Path>/<набор данных>/` | снимки и записи на диск; по умолчанию `python_app/data/snapshots` | **Save Last 1000**, **Save S21 JSON**, **Start + Record to Disk** |
| `python_app/runtime/` | рабочее состояние программы (ниже) | программа |
| `/dev/shm/<имя кольца>` | кольца разделяемой памяти | процессы конвейера |
| `/tmp/radar_system.lock` | блокировка единственного экземпляра | `start.sh` |
| `radar.rph_radar.tty_path`, например `/tmp/ttyADC_data` | ссылка на псевдотерминал коллектора E-502 | `rph_radar_collector` |
| `/etc/udev/rules.d/99-radar-librevna.rules` | доступ к LibreVNA по USB | `start.sh` |
| `/etc/systemd/system/radar.service`, `/etc/sudoers.d/radar` | служба и право её перезапускать | `deploy/install-daemon.sh` |
Все файлы JSON и наборы программа записывает через временный файл и замену, а важные
ещё и сбрасывает на диск (fsync). Оборванная запись оставляет прежний файл целым;
от неё может остаться временный файл вида `.<имя>.<32 знака>.tmp`, а от оборванного
сохранения набора данных — каталог `.<имя набора>.<32 знака>`; их можно удалить.
## Наборы предобработки
Набор лежит в каталоге своего вида и ключа радара (глава
[Файл конфигурации](configuration.md)) и состоит из двух файлов:
| Файл | Содержимое |
| --- | --- |
| `<имя>.npz` | архив NumPy без сжатия: для каждой пары `freq_i<вход>_o<выход>` (частоты, float32) и `s21_i<вход>_o<выход>` (S21, complex64), плюс `metadata_json` — описание набора |
| `<имя>.json` | то же описание в читаемом виде |
Описание: `collection_id`, `monotonic_ns`, `capture_start_ns`, `capture_end_ns`,
список `combos` (вход, выход, окно съёма, имена массивов) и `metadata`. У калибровки
по объекту в `metadata` записано, при каких условиях она снята: ключ радара,
геометрия, имя и хеш набора калибровки, настройки расчёта, координаты отражателя,
параметры антенн, диагностика фаз и пары без поправки (`unchanged_combos`).
Старые наборы без `metadata_json` читаются по файлу `.json`; лишние массивы (например,
S11) пропускаются.
Имя набора — любая строка, кроме пустой, `.`, `..` и строк с `/` или `\`. Существующее
имя программа не перезаписывает; исключение — `tmp_reference`, который заменяется при
каждом съёме. Перед записью проверяется, что пары не повторяются, массивы одной длины
и значения конечны.
## Наборы данных
Набор данных — каталог со снимком истории (**Save Last 1000**) или записью на диск
(**Start + Record to Disk**). Имя каталога — `<префикс>_<Name>`, например
`librevna_multi_0.001-6GHz_201pts_-10dBm_run_A`; символы кроме латиницы, цифр, `.`,
`_` и `-` заменяются на `_`. Префикс: модель, полоса в единицах большей границы,
число точек (у радиофотонного радара — `adc`) и мощность. Пустое имя —
`snapshot_<ГГГГММДД>_<ЧЧММСС>` по UTC. Существующий каталог программа не трогает.
```text
<набор данных>/
config_profile.json профиль, с которым сняты данные
processing_settings.json ревизии настроек обработки
raw/ сырые коллекции
0000_id<номер>_ns<время>/
preprocessed/ предобработанные коллекции
results/ результаты обработки
```
Коллекции внутри стадии нумеруются с нуля. Время в имени каталога — метка своей
стадии, поэтому у одной коллекции оно разное в `raw`, `preprocessed` и `results`.
Сопоставляйте стадии по паре `collection_id` и `capture_start_ns` из `meta.json`.
Каталог коллекции в `raw/` и `preprocessed/`:
| Файл | Содержимое |
| --- | --- |
| `i<вход>_o<выход>_freq.npy` | частоты, float32 |
| `i<вход>_o<выход>_s21.npy` | S21, complex64 |
| `meta.json` | `collection_id`, `monotonic_ns`, `capture_start_ns`, `capture_end_ns`, `trace_count`, список трасс с их окнами съёма и именами файлов |
Каталог коллекции в `results/` содержит `meta.json` (номер, время обработки,
`settings_revision`, окно съёма, использованная скорость `speed_m_s`, список
результатов) и массивы результатов. Результаты всей коллекции — файлы
`collection_<NNN>_<имя>_kind<вид>_<поле>.npy`, результаты пары — в подкаталоге
`block_<NNN>_i<вход>_o<выход>/`.
| Вид | Поля |
| --- | --- |
| 1 — трасса | `freq` float32, `trace` complex64 |
| 2 — число | `scalar` float32 |
| 3 — изображение | `x_axis`, `y_axis` float32, `image` float32 размером (строки по y, столбцы по x) |
| 4 — таблица | `table` float32 (строки, столбцы) |
| Режим | Что сохраняется |
| --- | --- |
| pass-through | для каждой пары `pass_through` (вид 1): частота и S21 |
| B-scan | для каждой пары `bscan` (вид 1): в поле `freq` — дальность в метрах, в `trace` — значение (мнимая часть 0) |
| GPR | `gpr_accumulator` (вид 3): карта, оси X и Z; `gpr_points` (вид 4): строки `[x_m, z_m, score]` |
| 3D SAR | `sar3d_front`, `sar3d_side`, `sar3d_top`, `sar3d_oblique` (вид 3); `sar3d_oblique_basis`; `sar3d_peak` `[уровень, X, Y, Z]`; `sar3d_background` `[отношение, 6, 8.5]`; `sar3d_objects` `[X, Y, Z, уровень]` и `sar3d_marks` `[класс, X0, X1, Y0, Y1, Z0, Z1]` или `sar3d_target_width` `[X, Y, Z]`; `sar3d_status` `[код, байты текста UTF-8]` |
Классы рамок 3D SAR: 0 — очень низко, 1 — на земле, 2 — над землёй, 3 — у края. Коды
состояния 3D SAR: 0 — посчитано, 1 — нет сигнала, 2 — ошибка, 3 — нет CUDA.
`processing_settings.json` — словарь «номер ревизии → настройки обработки» в формате
`processing_live.json`: по `settings_revision` из `meta.json` результата видно, с
какими настройками он посчитан. При ручном сохранении туда попадают все ревизии
текущего сеанса, при записи — только использованные. `config_profile.json` — полный
профиль; разделы радара, ключей, пар, предобработки и геометрии взяты из конфигурации,
с которой шёл сбор, даже если потом загрузили другой профиль.
### Запись на диск
**Start + Record to Disk** создаёт каталог сразу и пишет в него коллекции, снятые
после нажатия; счётчик считает результаты. Данные копятся порциями по 200 коллекций и
уходят в очередь записи на 8 порций; каждая коллекция появляется на диске целиком.
Если диск не успевает и очередь заполнилась, запись останавливается с
`Disk recording stopped because the writer could not keep up`; записанное остаётся.
Если сбор так и не начался и ничего не пришло, каталог удаляется.
### Экспорт S21 JSON
**Save S21 JSON** создаёт в `<Path>/<набор данных>/` по файлу на пару
`<имя набора>_i<вход>_o<выход>_s21_vna_bscan_history.json` и профиль
`vna_config_profile.json`. Основные поля файла:
| Поле | Что |
| --- | --- |
| `format` | `"vna-system-history-v1"` |
| `input_index`, `output_index`, `channel` | пара и канал (`"s21"`) |
| `primary_stage` | `"preprocessed"` |
| `sweep_history` | список коллекций: `timestamp`, `sweep_points` (сырая развёртка, пары `[re, im]`), `calibrated_points` (предобработанная), `reference_points` (всегда пусто), `vna_config` (начало, конец, число точек) |
`timestamp` — время конца съёма развёртки этой пары (если его нет — коллекции) в
секундах монотонных часов компьютера (отсчёт от его загрузки), а не дата и время. Сырая и
предобработанная развёртки
сопоставляются по паре номер — начало съёма; если пары нет, массив пуст.
### Как открыть данные в Python
```python
import json
from pathlib import Path
import numpy as np
# Набор предобработки
with np.load("python_app/data/s21_reference/<radar_key>/<имя>.npz", allow_pickle=False) as archive:
meta = json.loads(str(archive["metadata_json"].item()))
for combo in meta["combos"]:
frequency, s21 = archive[combo["freq_key"]], archive[combo["s21_key"]]
# Предобработанные коллекции набора данных
from python_app.storage.npz.vna_history_json import collection_directories
for directory in collection_directories(Path("<набор данных>") / "preprocessed"):
meta = json.loads((directory / "meta.json").read_text())
for trace in meta["traces"]:
frequency = np.load(directory / trace["freq_file"])
s21 = np.load(directory / trace["s21_file"])
# Результаты (например, карта GPR)
for directory in collection_directories(Path("<набор данных>") / "results"):
meta = json.loads((directory / "meta.json").read_text())
for payload in meta["collection_payloads"]:
if payload["name"] == "gpr_accumulator":
image = np.load(directory / payload["image_file"])
```
`collection_directories` возвращает каталоги коллекций по порядку номеров. Загрузить
набор данных обратно в окно программы нельзя: он предназначен для обработки
отдельно.
## Рабочие файлы
Каталог `python_app/runtime/` программа создаёт и ведёт сама.
| Файл | Что | Можно удалить |
| --- | --- | --- |
| `gui_session_state.json` | путь последнего профиля: с него программа стартует | да; программа стартует с корневого профиля. Это и способ выйти из ситуации, когда запомненный профиль не даёт программе запуститься |
| `processing_live.json` | живые настройки процессора (глава [Файл конфигурации](configuration.md)) | да; программа запишет его заново |
| `run_config.json` | рабочая копия профиля последнего **Start** | при остановленном сборе |
| `s21_calibration_bundle.bin`, `s21_reference_bundle.bin`, `s21_object_calibration_bundle.bin` | выбранные наборы в формате для препроцессора, пересобираются при каждом **Start** | при остановленном сборе |
| `supervisor_children.pids` | номера процессов конвейера: если программа упала, следующий запуск завершает оставшиеся | когда процессы конвейера не работают |
| `logs/` | журналы (ниже) | при закрытой программе |
## Журналы
| Где | Что |
| --- | --- |
| `python_app/runtime/logs/radar.log` и `radar.log.1` … `radar.log.5` | журнал программы: то же, что в окне журнала, строки `дата время \| уровень \| источник \| сообщение`. Файл до 2 МиБ, хранятся пять предыдущих |
| `python_app/runtime/logs/acquisition.out.log`, `acquisition.err.log` | вывод процесса сбора, в том числе ожидание приборов и сообщения коллектора E-502 (`Collector: …`) |
| `python_app/runtime/logs/data_preprocessor.*.log`, `data_processor.*.log` | вывод препроцессора и процессора: отброшенные коллекции, отклонённый live-файл, сообщения 3D SAR (`sar3d: …`) и локатора (`locator: …`) |
| `*.prev` рядом | вывод прошлого запуска процесса или начало текущего, если файл превысил 8 МиБ |
| `journalctl -u radar.service` | при работе службой: вывод программы, включая строки `[start.sh] …` |
Уровень записей задаёт `logging.level` профиля или список **Level** в окне.
Подробности ошибки, показанной окном (например, трассировка вызовов), видны в окне
ошибки и в окне журнала, а в `radar.log` попадает само сообщение. Если процесс конвейера
завершился сам,
отчёт с последними 16 КиБ его вывода попадает в журнал программы. Раз в 50 замеров
программа пишет строку `metrics: <стадия> n=… avg=…ms min=…ms max=…ms` со временем
сбора, обработки и отрисовки.
## Разделяемая память
Кольца лежат в `/dev/shm` под именами из раздела `rings` профиля (без начальной `/`).
При ёмкости 50 и слоте 2 МиБ кольцо занимает около 100 МиБ виртуальной памяти, но
страницы выделяются только по мере записи. Кольца создаёт первый запустивший их
процесс с правами 0660 и при выходе не удаляет. `./start.sh --clean-shm` удаляет
кольца `/radar_raw`, `/radar_raw_tap`, `/radar_preprocessed`, `/radar_preprocessed_tap`,
`/radar_results` и те же имена с окончанием `_rph_radar`. Кольца с окончанием
`_simulator` он не трогает, а их используют корневой профиль, оба профиля симулятора и
оба примера с переключением стимула (`run_config_librevna_multi_per_point*.example.json`).
Их удаляйте вручную при закрытой программе: `rm /dev/shm/radar_*_simulator`.
> **Внимание.** Если программу хоть раз запустили от root (например, тесты через
> `sudo`), кольца станут принадлежать root, и обычный пользователь получит отказ в
> доступе. Удалите их при закрытой программе: `sudo rm -f /dev/shm/radar_*`.
+341
View File
@@ -0,0 +1,341 @@
# Окно программы
Окно называется **Radar System Control** и открывается развёрнутым на весь экран.
Слева — графики выбранного режима обработки, справа — панель управления. Работу ведут
несколько процессов — конвейер: процесс сбора управляет железом, препроцессор
калибрует данные, процессор их обрабатывает и держит TCP-локатор для внешних программ;
между собой они передают данные через кольца в разделяемой памяти (главы
[Устройство программы](architecture.md) и [Словарь](glossary.md)).
```text
┌──────────────────────────────────────┬─┬──────────────────────────────────┐
│ │ │ Actions (не прокручивается)│
│ │ │ ┌──────────────────────────────┐ │
│ графики │›│ │ Data Actions │ │
│ (вид зависит от режима) │ │ │ Switches │ │
│ │ │ │ Selected Preprocess Sets │ │
│ │ │ │ Processing │ │
│ │ │ │ Radar │ │
│ │ │ └──────────────── прокрутка ───┘ │
│ │ │ Status: … History: … │
│ │ │ Runtime Log Level ▾ │
│ │ │ [журнал] │
└──────────────────────────────────────┴─┴──────────────────────────────────┘
```
Стрелка между графиками и панелью (подсказка **Hide settings panel** или
**Show settings panel**) прячет и показывает всю панель; графики тогда занимают всю ширину.
Положение
стрелки не запоминается. Секция **Actions** со списком **Mode** всегда на месте,
остальные секции прокручиваются. Меню, панели инструментов и сочетаний клавиш нет.
Многие действия приостанавливают сбор: останавливают процесс сбора и препроцессор, но
оставляют работать процессор и локатор, а по окончании продолжают сбор, если он шёл.
Так работают **Apply Radar**, **Clear Runtime History**, **Remove Last Measurement**,
**Capture Temporary Reference** и съём наборов. Приостановка завершает и запись на
диск. **Stop** отменяет ожидающее продолжение. Если в это время идёт другое такое
действие, программа отвечает `Wait for the current pipeline operation to finish`.
## Actions
| Кнопка | Что делает |
| --- | --- |
| **Start** | запускает непрерывный сбор и обработку |
| **Single Capture** | снимает одну коллекцию, показывает результат и останавливает сбор |
| **Stop** | останавливает сбор |
| **Apply Radar** | отправляет настройки радара в железо, не запуская сбор |
| **Load Config…** | загружает профиль из файла |
| **Save Config…** | сохраняет текущие настройки в профиль |
| **Preprocessing…** | открывает окно **Preprocessing Setup**: наборы калибровки и референса, калибровка по объекту ([Калибровка и предобработка](preprocessing.md)) |
| **Clear Runtime History** | очищает историю в памяти |
| **Mode** | режим обработки: `pass_through`, `bscan`, `gpr`, `sar3d` ([Режимы обработки](processing.md)) |
### Start
Перед запуском программа проверяет всё и при любой ошибке отказывает, ничего не
остановив; ошибка показывается окном `Failed to start pipeline: <тип>: <текст>`.
Проверяются:
- значения полей (например, буквы в поле **Points**: `invalid literal for int()…`);
- профиль сбора: режим драйвера, модель, ключи, линии GPIO, размер слотов колец;
- требования режима обработки (B-scan, GPR, 3D SAR — в главе
[Режимы обработки](processing.md));
- выбранные наборы калибровки и референса (глава
[Калибровка и предобработка](preprocessing.md)).
Если проверка прошла, программа:
1. перезапускает процессор, если изменились настройки, от которых он зависит
(глава [Файл конфигурации](configuration.md));
2. записывает файлы наборов и рабочую копию профиля в `python_app/runtime/`;
3. очищает историю, если процессор запускается заново (первый **Start**, перезапуск на
шаге 1) или изменились профиль сбора, наборы или режекторный фильтр (тогда в
журнале `History reset because run settings changed`);
4. запускает процесс сбора и препроцессор и ждёт до 3 с, пока они поднимутся;
5. начинает читать кольца; строка состояния — **Status: running**.
Если во время работы процесс конвейера завершился, программа пишет отчёт с хвостом
его вывода и перезапускает конвейер: первый раз сразу, следующие подряд — не раньше
чем через 6, 12, 24, 48, затем 60 с после предыдущей попытки, пока не получится. Повторное
нажатие **Start** при работающем сборе отвечает
`Pipeline is already running`.
### Single Capture
Делает то же, что **Start**, но процесс сбора снимает одну коллекцию и завершается.
Если шёл непрерывный сбор, он сначала останавливается. Программа ждёт результат
коллекции, снятой после нажатия, рисует его и останавливает сбор
(`Single capture completed`). Строка состояния — **Status: single capture running**.
Если результата нет 300 с, сбор останавливается с
`Single capture timed out after 300s with no result; stopping.` В режиме 3D SAR, если
результат не посчитался, строка состояния показывает **Status: no signal**,
**Status: error** или **Status: warning**.
### Stop
Останавливает процесс сбора, дочитывает кольца (до 0,35 с), останавливает
препроцессор и дочитывает результаты (до 0,25 с). Процессор и локатор продолжают
работать: так остановленный B-scan можно пересчитать, а клиенты локатора остаются
подключёнными. Запись на диск завершается. **Stop** отменяет и ожидающие действия:
автозапуск, продолжение после приостановки, съём для калибровки по объекту,
пересчёт B-scan.
### Apply Radar
Приостанавливает сбор, проверяет профиль и:
- одиночный LibreVNA (`native`) — открывает прибор, отправляет настройки развёртки,
закрывает;
- многоприборная схема — приборы не трогает (их настраивает процесс сбора), только
перечитывает пределы мастера;
- радиофотонный радар — программирует плату лазеров, если она включена в профиле;
- режим `mock` у любой модели — ничего (`Radar pre-configuration skipped (mock mode)`).
Если работает только процессор, а настройки, от которых он зависит, изменились,
**Apply Radar** перезапускает процессор и очищает историю.
В журнал пишется `Radar settings applied: start=…, stop=…, points=…, ifbw=…, power=…`.
Профиль кнопка не меняет.
### Load Config… и Save Config…
**Load Config…** открывает выбор файла (**Load Config Profile**) и загружает профиль:
все поля, таблицы геометрии, выбранные наборы, режим. Сбор должен быть остановлен,
иначе — `Stop all pipeline processes before loading a config profile`. Ошибка чтения
показывается как `Failed to load config profile: …`, текущий профиль при этом не
меняется.
**Save Config…** (**Save Config Profile**) записывает профиль; имя без расширения
получает `.json`, существующий файл перезаписывается. Записываются все поля окна,
выбранные наборы и геометрия. Загруженный или сохранённый профиль становится
стартовым для следующего запуска.
### Clear Runtime History
Приостанавливает сбор, отбрасывает непрочитанные данные колец, очищает историю
окна и процессора, очищает графики и продолжает сбор. Файлы на диске не трогает;
подтверждения не спрашивает. Поздние результаты коллекций, снятых до очистки,
отбрасываются.
## Data Actions
| Элемент | Что делает |
| --- | --- |
| **Save Last 1000** | сохраняет до 1000 последних коллекций каждой стадии (сырые, предобработанные, результаты) в новый каталог набора данных |
| **Save S21 JSON** | сохраняет S21 предобработанной стадии в файлы JSON, по файлу на пару |
| **Remove Last Measurement** | удаляет новейшую коллекцию из истории всех стадий |
| **Capture Temporary Reference** | снимает временный референс и сразу его применяет ([Калибровка и предобработка](preprocessing.md)) |
| **Start + Record to Disk** | запускает сбор, если он остановлен, и записывает на диск следующие **Sweeps to record** коллекций |
| **Sweeps to record** | сколько коллекций записать, от 1 до 1 000 000 |
| **Path**, **Browse…** | каталог, в котором создаются наборы данных |
| **Name** | имя набора данных; пусто — `snapshot_<дата>_<время>` по UTC |
Каталог набора данных — `<Path>/<префикс>_<Name>`. Префикс описывает снятое:
модель, полосу, число точек (у радиофотонного радара — `adc`) и мощность. Каталог
не должен существовать (`Snapshot directory already exists`). Рядом с данными
кладутся профиль, с которым они сняты (`config_profile.json`), и все ревизии
настроек обработки (`processing_settings.json`). Состав файлов — в главе
[Данные на диске](data.md).
Перед сохранением программа дочитывает данные из колец; идущий сбор и запись при
этом не прерываются. Если в памяти нет ни одной коллекции, сохранение отказывает:
`No runtime data is available for save`. Окно хранит в памяти до 1000 коллекций каждой
стадии; что старее,
не сохранить. Без колец-копий (`raw_tap`, `preprocessed_tap`) сохраняются только
результаты.
**Save S21 JSON** пишет по файлу на пару: для каждой коллекции — сырая и
предобработанная развёртка, рядом профиль `vna_config_profile.json`. Его можно
направить в существующий каталог набора данных: существующие файлы данных JSON не
перезаписываются, а `vna_config_profile.json` заменяется.
**Remove Last Measurement** приостанавливает сбор и удаляет из истории окна и
процессора самую новую коллекцию; если её результат ещё в пути, процессор его не
выдаст. Повторное нажатие удаляет следующую.
**Start + Record to Disk** записывает коллекции, которые придут после нажатия. Запись
заканчивается, когда записано заданное число, по **Stop**, при приостановке сбора
(очистка истории, временный референс, съём) или если диск не успевает и очередь
записи переполнилась; причина пишется в журнал. Одновременно идёт только одна запись
(`A recording is already in progress…`), а каталог должен быть новым. Закрытие окна
дописывает принятые данные.
## Switches
| Элемент | Что делает |
| --- | --- |
| **Settling (ms)** | пауза после переключения ВЧ-ключей перед развёрткой, мс (`run.settling_ms`) |
| **Run combos** и **Select** | список пар сбора `вход:выход` через запятую; пусто — полная матрица. **Select** делает этот список источником пар |
| **Single combo**: **Input**, **Output** и **Select** | одна пара сбора; **Select** делает её источником пар |
| **B-scan pair** | какую пару показывать в B-scan; доступно в режиме B-scan |
| **Next Combination** | следующая пара B-scan; доступно, когда пар в истории хотя бы две |
Пары сбора применяются при **Start**. У многоприборной схемы список всегда равен
полной матрице и не редактируется. В режимах pass-through и B-scan, если оба ключа
`mock` или у обоих одно положение, снимается только пара `0:0`.
## Selected Preprocess Sets
Показывает выбранные наборы: **S21 calibration**, **S21 reference**,
**Object calibration**; `<not selected>` — набор не выбран. Выбираются наборы в окне
**Preprocessing Setup**.
## Processing
Страница настроек выбранного режима. Каждое поле описано в главе
[Режимы обработки](processing.md). В режимах GPR и 3D SAR над настройками — таблицы
геометрии антенн.
### Таблицы геометрии
**Tx geometry** — передающие антенны: **Output** (номер выхода), **X (m)**, **Y (m)**,
**Z (m)**. **Rx geometry** — приёмные: **Input**, **X (m)**, **Y (m)**, **Z (m)**.
Координаты — в осях радара (глава [Как работает радар](principles.md)).
- **Add Antenna** добавляет строку со следующим свободным номером; строка начинает
действовать после ввода X, и до этого кнопка недоступна. Недоступна она и тогда,
когда строк столько же, сколько выходов или входов.
- **Remove Selected** удаляет выделенные строки.
- Каждая правка проверяется: номер — целый, в пределах матрицы, без повторов;
координаты — числа; требования GPR и 3D SAR выполняются. Неверная правка
откатывается с сообщением **Invalid Antenna Geometry**.
- Принятая правка сразу уходит процессору и пересчитывает результат. Если она не
совпадает с геометрией выбранной калибровки по объекту, выбор этой калибровки
снимается.
## Radar
Для LibreVNA и симулятора секция показывает развёртку:
| Поле | Ключ профиля |
| --- | --- |
| **Start (Hz)** | `radar.sweep.start_hz` |
| **Stop (Hz)** | `radar.sweep.stop_hz` |
| **Points** | `radar.sweep.points` |
| **IF bandwidth (Hz)** | `radar.sweep.if_bandwidth_hz` |
| **Stimulus power (dBm)** | `radar.sweep.stimulus_power_dbm` |
Под полями — пределы прибора: `Limits: freq=…, points=1..N, ifbw=…, power=…`. В режиме
`mock` и у радиофотонного радара пределов нет: `Device limits are not available.` (с
`driver_mode: "native"` без подключённого прибора окно не открывается — глава
[Неполадки](troubleshooting.md)). С известными пределами подпись поля дополняется
диапазоном, а значения вне его подрезаются с предупреждением
`Radar fields were adjusted to satisfy device limits.` Поле применяется, когда из него
уходит фокус или нажат Enter. Если
изменение меняет ключ радара, выбор наборов сбрасывается, а границы частот обработки
подрезаются под новую развёртку.
Для радиофотонного радара вместо развёртки показаны поля коллектора и платы
лазеров:
| Поле | Ключ профиля |
| --- | --- |
| **Project dir**, **Executable path**, **TTY path** | `radar.rph_radar.project_dir`, `executable_path`, `tty_path` |
| **Arguments** (по строке на параметр) | `radar.rph_radar.args` |
| **Environment JSON** | `radar.rph_radar.env` |
| **Startup timeout (s)**, **Read timeout (s)**, **Stop timeout (s)** | `startup_timeout_s`, `sweep_timeout_s`, `stop_timeout_s` |
| **Board enabled** | `radar.laser_control.enabled` |
| **Board port**, **Board mode** | `port`, `mode` (`manual` или `variation`) |
| **PI 1 P**, **PI 1 I**, **PI 2 P**, **PI 2 I** | `pi_coeff1_p`, `pi_coeff1_i`, `pi_coeff2_p`, `pi_coeff2_i` |
| **Temperature 1**, **Temperature 2**, **Current 1**, **Current 2** (режим `manual`) | `manual.temp1`, `temp2`, `current1`, `current2` |
| **Static temp 1**, **Static temp 2**, **Static current 1**, **Static current 2**, **Min value**, **Max value**, **Step**, **Time step**, **Delay time** (режим `variation`) | `variation.static_temp1` … `delay_time` |
Поля платы доступны при включённой **Board enabled**; видны поля только выбранного
режима. Единицы и пределы — в главе [Файл конфигурации](configuration.md). Поля
коллектора применяются при **Start**, поля лазера — кнопкой **Apply Radar**.
## Строка состояния и журнал
Под секциями — строка состояния и счётчик истории
`History: raw=0, preprocessed=0, results=0` (сколько коллекций каждой стадии в памяти).
| Состояние | Что значит |
| --- | --- |
| **Status: idle** | сбор остановлен |
| **Status: running** | идёт непрерывный сбор |
| **Status: single capture running** | идёт однократный съём |
| **Status: error** | в журнал записана ошибка; состояние держится до следующего изменения |
| **Status: result display error (acquisition continues)** | не удалось нарисовать результат; сбор продолжается |
| **Status: runtime display/recording error (acquisition continues)** | ошибка отображения или записи на диск; сбор продолжается |
| **Status: no signal**, **Status: warning** | итог однократного съёма в режиме 3D SAR |
**Runtime Log** — журнал программы. Список **Level** (**Debug**, **Info**,
**Warning**, **Error**) задаёт наименьший уровень записей; он применяется сразу и
сохраняется в профиль при **Save Config…**. Стрелка справа прячет и показывает журнал.
В окне хранится до 2000 последних записей; при переполнении появляется
`Log panel overflow: N record(s) dropped (they are still in the log file).` Полный
журнал — в `python_app/runtime/logs/radar.log` (глава [Данные на диске](data.md)).
Повторяющиеся ошибки отрисовки показываются один раз.
## Графики
| Режим | Что на экране |
| --- | --- |
| pass-through | **Pass-Through S21**: модуль (**Magnitude (dB)**) и фаза (**Phase (deg)**) по частоте, кривая на пару, легенда `in<вход>/out<выход>` |
| B-scan | **B-scan S21 in<вход>/out<выход> \| sweeps=N**: номер развёртки — дальность |
| GPR | **GPR coherent BP — ground plane**: карта плоскости земли и объекты |
| 3D SAR | **Front (X-Y)**, **Side (Z-Y)**, **Top (X-Z)**, **3D Projection** и строка состояния 3D SAR над ними |
Подробно виды описаны в главе [Режимы обработки](processing.md). Приставку единиц на
осях (Гц, МГц, ГГц, м, мм) график подбирает сам.
Графики управляются мышью так, как принято в библиотеке pyqtgraph: колесо меняет
масштаб, перетаскивание сдвигает, правая кнопка открывает меню (**View All** —
вернуть весь график, экспорт картинки). Щелчок по строке легенды прячет и показывает
кривую. Графики окна видны и в веб-интерфейсе.
Последняя развёртка снятого набора (калибровка, референс, временный референс)
рисуется на графике pass-through с подписью вида `Tmp S21 Reference last trace`.
## Сообщения
Ошибка действия открывает окно с ошибкой и пишется в журнал. Без экрана, для
действий из веб-интерфейса и для GPIO-кнопки окон нет: ошибка уходит в журнал, а
веб-интерфейсу — в ответ на запрос. Причины частых ошибок — в главе
[Неполадки](troubleshooting.md).
Окно ошибки называется по действию и обычно показывает
`<заголовок>: <тип ошибки>: <текст>` (у записи на диск — `<заголовок>: <текст>`):
| Заголовок | Действие |
| --- | --- |
| `Failed to start pipeline` | **Start**, **Single Capture** |
| `Failed to stop pipeline` | **Stop** |
| `Failed to apply radar settings` | **Apply Radar** |
| `Failed to load config profile`, `Failed to save config profile` | **Load Config…**, **Save Config…** |
| `Failed to open preprocessing panel` | **Preprocessing…** |
| `Failed to clear runtime history`, `Failed to remove last runtime measurement` | **Clear Runtime History**, **Remove Last Measurement** |
| `Failed to save snapshot`, `Failed to save VNA history JSON` | **Save Last 1000**, **Save S21 JSON** |
| `Failed to start disk recording` | **Start + Record to Disk** |
| `Failed to capture tmp reference` | **Capture Temporary Reference** |
| `Cannot switch processing mode to <режим>` | **Mode** |
| `Failed to update live processing settings`, `Failed to update plot display settings`, `Failed to render processing settings` | правка настроек обработки и вида |
| `Failed to recompute the B-scan` | пересчёт остановленного B-scan |
| `Failed to start <вид> sequence`, `Failed to capture combo; previous captures kept, retry when ready`, `Failed to undo last preprocess capture`, `Failed to save preprocess set`, `Failed to abort capture sequence`, `Failed to refresh preprocessing sets`, `Failed to create neutral S21 sets` | окно **Preprocessing Setup** |
| `Failed to capture object-calibration data`, `Failed to calculate object calibration`, `Failed to save object calibration`, `Failed to update object calibration after geometry change` | калибровка по объекту |
| `Cannot perform data action`, `Cannot modify runtime history during active capture sequence` | действие с данными во время съёма или остановки |
| `Shutdown operation failed` | закрытие окна |
Пока идёт съём наборов, часть действий недоступна, и программа объясняет почему:
`Finish the active capture sequence first`, `Wait for preprocessing capture to finish`,
`Wait for object-calibration capture to finish`, `Wait for pipeline shutdown to finish`.
+160
View File
@@ -0,0 +1,160 @@
# Разработка
## Сборка
C++ собирается командой `make` из корня проекта; Eigen и nlohmann/json лежат в
`data_acq_and_processing/third_party/`, других внешних библиотек нет.
| Цель | Что собирает или делает |
| --- | --- |
| `all` (по умолчанию) | `build/bin/data_preprocessor`, `build/bin/data_processor` |
| `rph_radar_collector` | `build/bin/rph_radar_collector`; в `all` не входит |
| `check-native` | тесты C++: кадры, кольца, конфигурация, наборы, режекторный фильтр, локатор, псевдотерминал коллектора (на поддельной библиотеке L-Card) |
| `check-sar3d` | 3D SAR на настоящем CUDA-бэкенде против эталона |
| `check-gpr` | GPR на CPU, без видеокарты, против эталона |
| `clean` | удаляет каталог сборки |
| Переменная | По умолчанию | Что задаёт |
| --- | --- | --- |
| `BUILD_DIR` | `build` | каталог сборки. Программа запускает процессы только из `build/bin/`, другой каталог нужен для проверок |
| `CUDA_ROOTS` | пакеты `nvidia/*` в `.venv` и `/usr/local/cuda` | где искать `nvrtc.h`, `cuda.h`, `libnvrtc` |
| `PYTHON` | `.venv/bin/python` | интерпретатор для `check-sar3d` и `check-gpr` |
| `NATIVE_SANITIZERS` | не задана | санитайзеры для тестов C++, например `address,undefined` |
| `CXX`, `CXXFLAGS`, `LDFLAGS` | `g++`, `-std=c++20 -O2 …`, `-pthread -lrt` | компилятор и флаги |
`BUILD_DIR`, `CUDA_ROOTS`, `CXX`, `CXXFLAGS` и `LDFLAGS` задаются в командной строке
`make`, а не через окружение; `PYTHON` и `NATIVE_SANITIZERS` можно задать и переменной
окружения. `CXXFLAGS` в командной строке заменяет весь набор флагов, вместе с
`-std=c++20` и отслеживанием зависимостей `-MMD -MP`. `make` сам пересобирает нужное, если
изменились найденная CUDA или
санитайзеры. Сборка с CUDA и без неё описана в главе [Установка и запуск](installation.md).
## Тесты
```sh
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
make check-native
ASAN_OPTIONS=detect_leaks=0 make check-native NATIVE_SANITIZERS=address,undefined
make check-sar3d
make check-gpr
```
| Набор | Что нужно |
| --- | --- |
| Python, `python_app/tests` | `.venv` с пакетами, `QT_QPA_PLATFORM=offscreen` (дисплея у тестов нет), запись в `/dev/shm`, псевдотерминалы, UDP на loopback. Железо не нужно |
| C++, `make check-native` | g++ с C++20 |
| 3D SAR, `make check-sar3d` | сборка с CUDA и пригодная видеокарта; без неё проверяются только предупреждение и правила рамок, остальное пропускается |
| GPR, `make check-gpr` | numpy |
Особые случаи:
- `test_processor_history` запускает настоящий `build/bin/data_processor` и
пропускается, если он не собран.
- Тесты окна строят настоящее окно на временном профиле из
`run_config_examples/run_config_simulator.example.json` и не зависят от корневого
`run_config.json` (`python_app/tests/gui_test_support.py`).
- `test_deploy_scripts` гоняет `deploy/update.sh` во временном каталоге с заглушками
`systemctl` и `make`.
- `test_cart_firmware` компилирует прошивку тележки на компьютере и пропускается без
g++.
- `data_acq_and_processing/tests/test_backprojection.py` в общий прогон не входит:
его запускают `make check-sar3d` и `make check-gpr`.
- `test_documentation` проверяет документацию: `docs/configuration.md` называет каждый
ключ профиля и `processing_live.json`, главы называют каждую подпись окна и диалогов
(жирным, кодом или заголовком), а относительные ссылки ведут на существующие файлы.
Новый ключ или кнопка без описания роняет этот тест.
> **Внимание.** Не запускайте тесты от root: созданные ими кольца в `/dev/shm`
> станут принадлежать root, и программа от обычного пользователя получит отказ в
> доступе.
Проверка на старой видеокарте (NVRTC 13 не компилирует под карты старее sm_75):
```sh
make check-sar3d BUILD_DIR=/tmp/radar_build_nvrtc12 CUDA_ROOTS="$HOME/.cache/radar_nvrtc12/nvidia/cuda_nvrtc $HOME/.cache/radar_nvrtc12/nvidia/cuda_runtime"
```
## Эталоны 3D SAR и GPR
`data_acq_and_processing/tests/sar3d_reference.npz` и `gpr_reference.npz` строит
генератор, который выполняет неизменённые функции ноутбука
`Multi_depthnorm_clean.ipynb` (ячейки 1 и 5) и скрипта `Horns_motion_4x4_ground.py` с
заменёнными параметрами. В эталоне 3D SAR десять случаев: шесть объёмов (ширина
отклика, вычитание среднего предыдущих коллекций для каждого кадра апертуры,
отношение к фону) и четыре сцены точечных отражателей в шуме для CLEAN с объектами и
рамками. Правила рамок проверяются по одному на синтетических объектах. Архивы
записывают хеши SHA-256 исходников и версии NumPy и SciPy.
Перегенерировать эталоны нужно только при правке алгоритма; ноутбук и скрипт
хранятся вне репозитория:
```sh
.venv/bin/python data_acq_and_processing/tests/generate_backprojection_reference.py \
--notebook /путь/к/Multi_depthnorm_clean.ipynb \
--gpr-script /путь/к/Horns_motion_4x4_ground.py
```
Другую сборку можно проверить так: `make check-sar3d BUILD_DIR=/путь/к/сборке` или
`BACKPROJECTION_FIXTURE=/путь/к/сборке/native_tests/backprojection_fixture` при запуске
`test_backprojection.py Sar3dTest`.
## Утилиты
| Команда | Что делает |
| --- | --- |
| `.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config <профиль> --sweeps 200 [--warmup 10] [--apply]` | фазовая калибровка радиофотонного радара (глава [Оборудование](hardware.md)) |
| `.venv/bin/python -m python_app.scripts.per_point_switching_check --config <профиль> [--collections 3] [--switch-connected]` | проверка переключения стимула на железе; выход 0 — норма, 1 — сбой |
| `./start.sh --producer-only --profile <профиль>` | только процесс сбора с выводом в терминал — для отладки железа |
## Правила кода
- Обязателен только функциональный контракт: программа в правильном порядке
собирает коллекции, предобрабатывает, обрабатывает и показывает результат в окне и в
вебе, даёт выбрать настройки предобработки и выполнить действия пользователя.
Внутреннее устройство можно менять под этот контракт.
- Код — читаемый и без лишнего: понятные имена, без мёртвых опций и «умных» трюков.
- Наименьшая версия Python — 3.10 (код использует `dataclass(slots=True)` и `match`).
- `.venv/bin/pyright python_app` — 0 ошибок, без подавлений и расширения исключений
(режим `basic`).
- Нумерацию антенн и портов задаёт профиль: она уточняется по железу, менять её
самостоятельно нельзя.
- Корневой `run_config.json` остаётся профилем симулятора.
- Изменив поведение, исправьте документацию в том же изменении (таблица «Где что
править» в главе [Устройство программы](architecture.md)); полноту ключей и подписей
проверяет `test_documentation`. Документ описывает, как устроено сейчас; числа
перепроверяются по коду.
## Прошивки
> **Внимание.** `top_master.bin` и `combined.vnafw` — единственные копии рабочего
> битстрима мастера и рабочего образа; битстрим заново не воспроизводится. Не
> перезаписывайте их, новые сборки кладите под новыми именами.
- VHDL — только ASCII: любой другой символ, даже длинное тире в комментарии, ломает
GHDL (`invalid character, even in a comment`).
- В `VNA.xise` закреплено `Starting Placer Cost Table (1-100) Map spartan6 = 2`; не
сбрасывайте его в 1: ограничение `MCU_MOSI OFFSET hold` и в стоковой прошивке
проходит с запасом всего +0,458 нс.
- После правки FPGA прогоните тестбенч и мутационный тест — внесите ошибку и
убедитесь, что тестбенч падает:
```sh
ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep --assert-level=error
```
- Изменение объёма логики FPGA сравнивайте по Timing Score с контрольной сборкой
стоковой прошивки.
Порядок сборки прошивок —
[reproduce/README.md](../device_firmware/multi_libreVNA_firmware/reproduce/README.md).
## Git
- Основная ветка — `main`.
- Правила `build*` и `build/` в корневом `.gitignore` скрывают и файлы комплекта сборки
прошивок (в том числе `build.sh` и эталонные бинарники STM32); для них добавлены
исключения, а часть файлов скрывает ещё и вложенный `.gitignore` LibreVNA. Добавляя
файлы в `device_firmware/`, проверяйте `git status --ignored` и при необходимости
`git add -f`.
- В репозиторий не входят: `.venv`, `build/`, `python_app/data*`, `python_app/runtime`,
ноутбуки `*.ipynb`, локальные инструменты разработчиков.
+64
View File
@@ -0,0 +1,64 @@
# Словарь
Русские термины — по алфавиту, за ними термины, которые пишутся латиницей.
| Термин | Значение |
| --- | --- |
| А-скан | профиль дальности одной пары: амплитуда отражения в зависимости от расстояния; получается обратным преобразованием Фурье развёртки S21 |
| апертура | набор точек, из которых снята сцена. В 3D SAR — текущая коллекция и до `SAR_FRAME_RADIUS` предыдущих |
| боковой лепесток | ложный слабый пик рядом с настоящим на А-скане или изображении; появляется из-за того, что полоса частот ограничена |
| ВАЦ (векторный анализатор цепей, VNA) | прибор, который выдаёт синусоиду заданной частоты и измеряет амплитуду и фазу прошедшего сигнала; в системе — LibreVNA |
| ведомый прибор (slave) | LibreVNA многоприборной схемы, который только принимает |
| воксель | ячейка трёхмерной сетки 3D SAR |
| временный референс | референс, снятый одной кнопкой во время работы; хранится как набор `tmp_reference` |
| вход (input) | приёмная сторона пары: номер приёмной антенны |
| ВЧ-ключ | коммутатор H7992: подключает порт прибора к одной из антенн (их до четырёх) по двум логическим линиям |
| выход (output) | передающая сторона пары: номер передающей антенны; у LibreVNA совпадает с номером стадии прибора |
| геометрия антенн | координаты передающих и приёмных антенн в осях радара, таблицы **Tx geometry** и **Rx geometry** |
| дальность | половина пути волны от передающей антенны через отражатель к приёмной; в GPR и 3D SAR — расстояние вдоль оси обзора радара |
| дБ | логарифмическая мера отношения: для амплитуд 20·lg, для мощностей 10·lg |
| дБм | мощность в децибелах относительно 1 мВт |
| калибровка (through) | эталонное измерение тракта; каждая развёртка делится на него |
| калибровка по объекту | поправка пар по известному отражателю, выравнивающая их по фазе |
| ключ радара (`radar_key`) | строка из настроек железа; по ней находятся наборы, снятые при тех же настройках |
| когерентное суммирование | сложение комплексных значений с учётом фазы: вклады настоящего отражателя складываются, остальные гасятся |
| коллекция (collection) | все пары, снятые за один обход; единица данных в программе |
| кольцо (ring) | кольцевой буфер в разделяемой памяти, через который процессы передают кадры |
| копия кольца (tap) | кольца `raw_tap` и `preprocessed_tap`: копии для окна программы |
| локатор | TCP-сервер процессора: отдаёт объекты GPR и принимает скорость |
| мастер (master) | LibreVNA многоприборной схемы, который излучает и задаёт запуск |
| набор (предобработки) | сохранённое измерение для предобработки: калибровка, референс или калибровка по объекту |
| набор данных | каталог с сохранёнными коллекциями и результатами |
| нейтральные наборы | калибровка, равная 1, и референс, равный 0: данные проходят предобработку без изменений |
| обратная проекция | построение изображения: для каждой точки сумма значений А-сканов всех пар на задержке до этой точки |
| обратное преобразование Фурье | пересчёт значений по частотам в значения по времени (задержке); переводит развёртку S21 в А-скан |
| окно Ханна | плавное взвешивание частот перед преобразованием Фурье, снижающее боковые лепестки |
| окно съёма | время начала и конца измерения по монотонным часам компьютера; есть у коллекции и у каждой трассы |
| пара (combo) | сочетание передающей и приёмной антенны, записывается `вход:выход` |
| переключение стимула внутри частотной точки | режим многоприборной схемы, в котором каждый порт мастера на каждой частоте излучает через оба положения ключа; даёт матрицу 4 × 4 |
| полоса ПЧ (IF bandwidth) | полоса фильтра приёмника ВАЦ; чем уже, тем меньше шум и медленнее развёртка |
| предельная дальность | дальность, после которой А-скан повторяется: c / (2Δf) |
| препроцессор | процесс `data_preprocessor`: калибровка, вычитание референса, режекторный фильтр, калибровка по объекту |
| проекция максимума | плоская картинка объёма: в каждой её точке — наибольшее значение вдоль направления взгляда |
| профиль | файл JSON со всеми настройками программы |
| процесс сбора | процесс, который управляет железом и публикует коллекции |
| процессор | процесс `data_processor`: режимы обработки и локатор |
| развёртка (sweep) | один проход прибора по списку частот |
| разрешение по дальности | наименьшее расстояние между двумя отражателями, при котором они видны раздельно: c / (2B) |
| ревизия настроек | номер набора настроек обработки; процессор возвращает его в каждом результате |
| режекторный фильтр (notch) | обнуление заданных полос частот |
| режим обработки | что процессор делает с данными: pass-through, B-scan, GPR, 3D SAR |
| референс | измерение пустой сцены; вычитается как фон |
| симулятор | работа без железа (`driver_mode: "mock"`): программа моделирует один движущийся отражатель |
| стадия прибора | одно излучение на частоте: на каждой частоте прибор проходит все стадии по очереди |
| угол обзора | наклон радара вниз от горизонтали |
| фон кадра | медиана рабочей части объёма 3D SAR (ненулевые воксели опорных плоскостей); без объектов CLEAN кадр показывается, когда его максимум превышает фон в 6 раз |
| частотная точка | одна частота развёртки |
| ширина на половине максимума | ширина отклика на уровне половины его пика; её размер имеет квадрат, которым 3D SAR отмечает максимум |
| B-scan | изображение «номер развёртки — дальность» для одной пары |
| CLEAN | алгоритм, который по шагам вычитает самый сильный отклик, чтобы разделить объекты |
| GPR | режим обработки: изображение плоскости земли перед радаром и поиск объектов на нём |
| live-файл | `python_app/runtime/processing_live.json`: настройки обработки, которые программа передаёт процессору на ходу |
| RPH (радиофотонный радар) | радар, в котором СВЧ-сигнал формируется лазерами; данные оцифровывает АЦП E-502 или плата RFG |
| S21 | комплексный коэффициент передачи из порта 1 в порт 2: отношение принятой волны к поданной, то есть ослабление и сдвиг фазы |
| SAR (3D SAR) | радар с синтезированной апертурой; режим, строящий трёхмерное изображение сцены |
+382
View File
@@ -0,0 +1,382 @@
# Оборудование
Тип радара задаёт поле `radar.model` профиля, а `radar.driver_mode` — работать с
настоящим железом (`"native"`) или с симулятором (`"mock"`). Ключи профиля описаны
в главе [Файл конфигурации](configuration.md), принцип измерения — в главе
[Как работает радар](principles.md), термины — в [словаре](glossary.md).
| Радар | `radar.model` | Что подключается | Процесс сбора |
| --- | --- | --- | --- |
| Одиночный LibreVNA | `librevna` | один LibreVNA по USB, два ВЧ-ключа на GPIO | `python_app.scripts.matrix_raw_producer` |
| Многоприборная схема | `librevna_multi` | три LibreVNA по USB, петля триггера; в режиме переключения стимула — контроллер STM32 и ключи на портах мастера | `python_app.scripts.matrix_raw_producer` |
| Радиофотонный радар с АЦП | `rph_radar`, `radar.rph_radar.source: "e502"` | АЦП L-Card E-502 по USB, плата лазеров по последовательному порту, два ВЧ-ключа на GPIO | `python_app.scripts.rph_radar_raw_producer` и программа `rph_radar_collector` |
| Радиофотонный радар с платой RFG | `rph_radar`, `radar.rph_radar.source: "rfg_udp"` | плата RFG по Ethernet, плата лазеров по последовательному порту; одна пара антенн | `python_app.scripts.rph_radar_raw_producer` |
| Симулятор | `librevna` или `librevna_multi` с `driver_mode: "mock"` | ничего | `python_app.scripts.matrix_raw_producer` |
Для радиофотонного радара симулятора нет: он работает только с `driver_mode: "native"`.
Процесс сбора открывает приборы и при неудаче повторяет попытки бесконечно, с паузой
от 1 до 10 с: прибор можно подключить и после запуска. Его сообщения пишутся в
`python_app/runtime/logs/acquisition.err.log`, а в журнал окна попадают, только если
процесс завершился. Ошибки сбора и их признаки собраны в главе
[Неполадки](troubleshooting.md).
## LibreVNA
LibreVNA — двухпортовый векторный анализатор цепей (ВАЦ) с USB. В системе используются платы
ревизии B. Прошивки — изменённая версия 1.6.5, протокол версии 14 (программа
проверяет версию при подключении одиночного прибора).
| Предел | Значение |
| --- | --- |
| Частота | 0–6 ГГц |
| Полоса ПЧ | 6 Гц–50 кГц |
| Число точек | до 4501 |
| Мощность | от −40 до 0 дБм |
| Задержка на стадии `dwell_us` | до 10 239 мкс |
Ниже 25 МГц генератор прибора переключается на другую микросхему, и развёртка идёт
с неравномерными длинными паузами: если развёртка должна быть быстрой, начинайте её не
ниже 30 МГц (так сделано в `run_config_librevna_multi_per_point_lowband.example.json`).
С `driver_mode: "native"` программа при запуске окна, загрузке профиля и нажатии
**Apply Radar** читает пределы прибора и подрезает поля секции **Radar** под них
(`Radar fields were adjusted to satisfy device limits.` в журнале). У многоприборной
схемы пределы читаются с мастера. Если прибор с `driver_mode: "native"` не
подключён, окно программы не открывается: чтение пределов при старте завершается
ошибкой.
USB-доступ к прибору даёт правило udev `/etc/udev/rules.d/99-radar-librevna.rules`
(группа `plugdev`). Его ставит `./start.sh` при обычном запуске с профилем LibreVNA;
при запуске без экрана (`--headless`) правило не ставится.
## Одиночный LibreVNA
Прибор подключается по USB; если `radar.serial` пуст, берётся первый найденный.
Порт 1 излучает, порт 2 принимает. Выходной ВЧ-ключ (`switches.port1`) подключает
порт 1 к одной из передающих антенн, входной (`switches.port2`) — порт 2 к одной из
приёмных. Ключами управляет компьютер через GPIO.
Сбор одной коллекции:
1. Для каждой пары из списка сбора программа выставляет выходной ключ, затем входной,
и ждёт время установления `run.settling_ms`.
2. Запускает развёртку и принимает все точки. Если какая-то точка не пришла,
коллекция не публикуется, а прибор открывается заново.
3. Публикует коллекцию в кольцо `raw` и начинает следующую.
S21 одиночного прибора — отношение сигнала порта 2 к сигналу опорного приёмника.
**Apply Radar** открывает прибор, отправляет настройки развёртки и закрывает его.
## Многоприборная схема
Три LibreVNA: мастер (`radar.serial`) и два ведомых
(`radar.multi_device.slave_serials`, ровно два). Мастер на каждой частоте излучает по
очереди с порта 1 и порта 2, ведомые не излучают и принимают каждый на оба порта.
S21 — сигнал приёмника ведомого, делённый на сигнал опорного приёмника мастера на той
же стадии.
| Номер | Что это |
| --- | --- |
| вход 0, 1 | первый ведомый из `slave_serials`, порт 1 и порт 2 |
| вход 2, 3 | второй ведомый, порт 1 и порт 2 |
| выход 0, 1 (без переключения стимула) | мастер, порт 1 и порт 2 |
| выход 0, 1, 2, 3 (переключение стимула, два положения) | порт 1 положение A, порт 1 положение B, порт 2 положение A, порт 2 положение B |
Номер выхода совпадает с номером стадии прибора. Многоприборная схема всегда снимает
полную матрицу: список пар `run.combos` у неё заменяется всеми парами, и в окне он не
редактируется.
Приборы синхронизированы аппаратным сигналом запуска. Петля триггера:
`TRIGGER_OUT` мастера → первый ведомый → второй ведомый → `TRIGGER_IN` мастера.
Вернувшийся сигнал означает «все готовы». Мастер измеряет по вернувшемуся сигналу,
поэтому задержки в петле одинаковы для всех приборов и в отношении «приёмник/опорный»
сокращаются.
Опорная частота: поле `radar.multi_device.force_external_reference` включает внешнюю
опору 10 МГц сразу на всех трёх приборах (раздельно задать нельзя). Во всех профилях
оно выключено. Включайте его, только если опора 10 МГц действительно подана на все
приборы. Признак отсутствующей опоры при включённом поле — мастер не отдаёт точки:
`Packet not received from <серийный номер> within 1.0 s`.
Сбор: перед первой коллекцией программа настраивает опору на всех приборах,
затем отправляет настройки развёртки сначала ведомым, потом мастеру, чтобы приёмники
были готовы раньше, чем мастер выдаст запуск. Развёртка идёт непрерывно. Каждый
прибор передаёт свои точки по отдельному USB; программа ждёт начала развёртки на всех
трёх и принимает цикл, только если начала совпали по времени. Если пропало не больше
5 % точек трассы, они заменяются нулями (`Zero-filled dropped sweep point(s) to keep the cycle…`),
иначе цикл отбрасывается.
| Сбой | Предел |
| --- | --- |
| от прибора нет ни одной точки | 1,5 с |
| нет начала развёртки | 6 с |
| цикл не закончился | 8 с |
| повторные попытки без переподключения | 2 |
| переподключение всех приборов | `radar.multi_device.recovery_attempts` раз (по умолчанию 3), не дольше 10 с |
После этого процесс сбора пересоздаёт подключение и ждёт приборы бесконечно. Если
не пришло ни одной точки ни от одного прибора, в журнале процесса сбора будет
`No datapoints received from at least one device; hardware trigger sync did not start. Check Trigger Out/In loop and 10 MHz reference wiring.`
**Apply Radar** в этой схеме приборы не трогает: их настраивает процесс сбора
(`Multi-device raw producer will configure all LibreVNA devices`), кнопка только
перечитывает пределы мастера.
Прошивки: мастер работает с образом `combined.vnafw` (обычная раскладка стадий с
аппаратным триггером) или `combined_per_point_switching.vnafw` (переключение стимула),
ведомые — с `combined_external_lo_slave.vnafw`. Образы и правила их сборки — в
разделе «Прошивки» ниже.
## Переключение стимула внутри частотной точки
В этом режиме одна развёртка даёт матрицу 4 × 4 вместо 2 × 4: на каждом порту
мастера стоит ВЧ-ключ, и на каждой частоте каждый порт излучает дважды, через оба
положения ключа. Все 16 пар одной частоты измеряются за доли миллисекунды, поэтому
движение радара между ними пренебрежимо мало. Полное описание —
[per_point_switching.md](../device_firmware/per_point_switching.md).
Что нужно:
- прошить мастер образом `combined_per_point_switching.vnafw`; ведомые не трогать;
- поставить контроллер ключей (плата WeAct STM32G431CB с прошивкой
`device_firmware/switch_controller`) в разрыв петли триггера между
`TRIGGER_OUT` мастера и `TRIGGER_IN` первого ведомого;
- в профиле задать `radar.multi_device.stimulus_switch_positions: 2` и
`radar.sweep.dwell_us: 2`. Ключами на портах мастера управляет контроллер, а не
компьютер; разделы `switches` у многоприборной схемы программа сама ставит в `mock`.
| Вывод STM32 | Куда |
| --- | --- |
| PB0 (вход) | `TRIGGER_OUT` мастера |
| PB1 (выход, повторяет PB0) | `TRIGGER_IN` первого ведомого |
| PB2 | ключ порта 1 мастера |
| PB6 | ключ порта 2 мастера |
| PB5 | отладка: импульс 2 мкс на каждую поправку чётности |
Контроллер переключает оба ключа по каждому спаду сигнала запуска (положение A —
логический 0, B — 1). Если пауза между стадиями в 1,5 раза длиннее наименьшей из
последних 16 пауз, то есть на границе развёртки, он принудительно возвращает положение
A и считает это поправкой чётности. Задержка, которую он вносит в петлю (около 50 нс),
одинакова для всех
приборов и на фазу не влияет. Через 200 мс простоя триггера контроллер печатает
отчёт по USB (115 200 бод): `trigger cycles`, `resyncs (sweep ends)`, `parity corrections`
(должно быть `0 (ok)`), `stages per resync` (ожидается число точек × число стадий),
паузы и их гистограмма.
Проверка на железе — `python_app/scripts/per_point_switching_check.py --config <профиль>`:
она отказывает, если медиана модуля S21 какого-то выхода ниже −80 дБ (признак старой
прошивки мастера) или если два положения одного порта без подключённого ключа
различаются больше чем на 6 дБ. С ключом нужен флаг `--switch-connected`.
> **Внимание.** Полоса ПЧ 1 кГц в этом режиме воспроизводимо роняет приборы
> (`LIBUSB_ERROR_IO`). Работают 5, 10, 20 и 50 кГц; рабочее значение — 50 кГц.
## ВЧ-ключи H7992
Ключ подключает порт прибора к одной из до четырёх антенн. Положение задают две
линии GPIO, A (`pin_a`) и B (`pin_b`); линии нужны обе даже при двух положениях.
| Положение | A | B |
| --- | --- | --- |
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 2 | 1 | 0 |
| 3 | 1 | 1 |
Номера линий в профилях — номера линий GPIO-чипа (`gpio_chip`, обычно
`/dev/gpiochip0`), а не номера контактов разъёма. Во всех профилях-примерах, кроме
примера с платой RFG (там ключей нет), выходной ключ на линиях 17 (A) и 27 (B),
входной — на 22 (A) и 23 (B). При открытии программа
выставляет ключ в `default_position`. Ключ с `driver_mode: "mock"` никуда не
переключается, программа только помнит его положение.
Кто управляет ключами:
| Радар | Кто переключает |
| --- | --- |
| одиночный LibreVNA | процесс сбора на Python |
| многоприборная схема | процесс сбора, только если `multi_device.output_switch_positions` или `input_switch_positions` больше 1; в режиме переключения стимула — контроллер STM32 |
| радиофотонный с E-502 | программа `rph_radar_collector`, если оба ключа `native`; иначе процесс сбора |
> **Внимание.** На Raspberry Pi 5 номер GPIO-чипа 40-контактного разъёма менялся от
> версии ядра к версии. Перед работой проверьте его на самой машине: `gpiodetect` и
> `gpioinfo -c gpiochip0`. Неверный чип означает ключи, которые молча не
> переключаются.
## Радиофотонный радар
Плата лазеров меняет ток лазера LD1 ступенями, при этом ток второго лазера и
температуры обоих постоянны. На каждой ступени оцифровываются основной сигнал и
опорный. Опорное плечо имеет постоянную задержку, поэтому его фаза растёт с частотой
линейно; по двум опорным точкам `phase_calibration` (фаза — частота) программа
определяет частоту каждой ступени. S21 — основной сигнал, делённый на модуль
опорного. Развёртки, которые не покрывают выходную полосу `radar.rph_radar.band`,
отбрасываются, остальные пересчитываются на её равномерную сетку (по умолчанию
2,1–5,5 ГГц, 2048 точек). Поля `radar.sweep` для этого радара не используются, в
окне вместо секции LibreVNA показывается секция RPH.
### Плата лазеров
Плата подключается по последовательному порту (115 200 бод) и программируется
кнопкой **Apply Radar**, если в профиле `laser_control.enabled: true`. Программа
проверяет все значения до отправки первого байта, затем сбрасывает плату и отправляет
постоянные токи и температуры, а в режиме `variation` — ещё задачу развёртки тока
LD1. Каждая команда должна получить ответ «состояние 0»; иначе
`Device rejected command: STATE 0x…`. После закрытия порта развёртка лазера
продолжается.
| Величина | Пределы | По умолчанию |
| --- | --- | --- |
| температура лазеров | 15–40 °C | 24,0 и 24,9 °C |
| ток лазеров | 15–110 мА | 97,3 и 102,0 мА |
| развёртка тока LD1 | 15–110 мА, начало меньше конца | 97,3 → 107,0 мА |
| шаг тока | 0,002–0,5 мА | 0,05 мА |
| длительность ступени | 20–100 мкс | 50 мкс |
| пауза между развёртками | 3–10 мс | 10 мс |
При запуске без экрана **Apply Radar** выполняется автоматически через 0,5 с после
старта, а через 1 с после него — **Start**, даже если **Apply Radar** не удался.
В веб-интерфейсе **Apply Radar** нет.
### АЦП L-Card E-502
Данные с АЦП читает отдельная программа `build/bin/rph_radar_collector` (собирается
командой `make rph_radar_collector`, в обычный `make` не входит). Её запускает и
останавливает процесс сбора; программа живёт, только пока жив её запустивший
процесс. Библиотеки L-Card `libx502api.so` и `libe502api.so` она загружает при
запуске, по умолчанию из `~/.local/lib`. Развёртки она передаёт процессу сбора через
псевдотерминал `rph_radar.tty_path` (в примере `/tmp/ttyADC_data`). Устройство
выбирается первое найденное. Подробности — в
[README коллектора](../data_acq_and_processing/rph_radar_collector/README.md).
Параметры коллектора задаются списком `radar.rph_radar.args`:
| Параметр | Что задаёт | По умолчанию |
| --- | --- | --- |
| `range:<В>` | диапазон входа ±В: `10`, `5`, `2`, `1`, `0.5` или `0.2` | ±2 В |
| `internal_ref_hz:<Гц>` | внутренняя опорная частота, она же наибольшая частота АЦП: `2000000` или `1500000` | 2 МГц |
| `duration_ms:<мс>` | наибольшая длительность развёртки, больше 0 и не больше 60 000 мс; более длинная считается испорченной | 100 мс |
| `do8_cycle_period:<n>` | период выхода DO8 в словах цифрового вывода, 4–262144, причём НОК(4, n) не больше 262144 (нечётные n больше 65 536 не подходят) | 8 |
Остальные слова в `args` (`profile:phase`, `clock:internal`, `start:di_syn2_rise`,
`stop:di_syn2_fall`, `sample_clock_hz:max`, `packet_limit:0`, `do1_toggle_per_frame`,
`do1_pair_subtract_avg`, `do8_freq_ref`) только подтверждают единственный
поддерживаемый режим; любое другое слово отвергается.
Как коллектор использует цифровые линии АЦП:
| Линия | Что делает коллектор |
| --- | --- |
| DI_SYN2 | фронт открывает развёртку, спад закрывает |
| DI1 | смена уровня отмечает новую ступень |
| DI2 | делит отсчёты ступени на две группы; значение ступени — среднее разности групп |
| DI8 | отсчёты с высоким DI8 идут в опорный канал |
| DO1 | меняется на каждом кадре |
| DO2 | высокий, пока идёт поток |
| DO8 | высокий на двух последних словах каждого периода `do8_cycle_period` |
Если ключами управляет коллектор, он переключает их сам между развёртками, ждёт
`run.settling_ms` (не меньше 1 мс) и открывает новый сеанс АЦП; первая развёртка
после переключения отбрасывается.
### Плата RFG
Плата RFG шлёт по UDP точки развёртки на адрес `radar.rph_radar.udp_bind`. В примере
это проводной адрес компьютера `192.168.2.1:5000`: привязка к проводному интерфейсу
отсекает чужие широковещательные пакеты (например, из Wi-Fi), а любой непонятный пакет
программа считает ошибкой. В каждой точке три канала; основной сигнал — разность
каналов 0 и 1 (синхронный детектор), опорный — канал 2.
Требования профиля:
- `laser_control.mode: "variation"` — даже если лазер запускают из другой программы:
по его настройкам вычисляется длина развёртки;
- у обоих ключей одно положение, то есть одна пара антенн: без метки от источника
нельзя доказать, что развёртка после переключения свежая.
Длина развёртки — ⌈(конец − начало) / шаг⌉ ступеней в кодах ЦАП тока; при настройках
по умолчанию 199 ступеней по 50 мкс. Развёртка принимается, только если пришли все
ступени подряд. Плата теряет несколько процентов точек, такие развёртки
отбрасываются. Время каждой точки — метка ядра о приёме пакета.
### Фазовая калибровка
Фазовые якоря `radar.rph_radar.phase_calibration` измеряются программой
`python_app/scripts/rph_radar_calibrate.py`. Перед ней примените настройки лазера
кнопкой **Apply Radar** и остановите сбор.
```sh
.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
```
Первая команда снимает развёртки и печатает измеренные фазы начала и конца, вторая
снимает заново и записывает в профиль только `phase0_rad` и `phase1_rad`, не трогая
остальные байты файла. Частотные якоря `freq0_hz` и `freq1_hz` должны заранее
описывать известные концы развёртки. `--warmup` задаёт, сколько первых развёрток
отбросить (по умолчанию 10). Команды лазеру программа не отправляет. Если профиль
изменился во время съёма, она его не перезаписывает.
## Симулятор
С `driver_mode: "mock"` процесс сбора не трогает железо и моделирует один точечный
отражатель. Отражатель движется по восьмисекундному циклу: вдоль ряда антенн — вокруг
его середины (размах — половина ширины ряда плюс 0,25 м, но не меньше 0,75 м), по
дальности — между 3,25 и 4,25 м, на высоте 0,25 м. S21 каждой пары
считается по настоящим расстояниям от антенн из профиля; коллекции идут не чаще чем
раз в 50 мс. Шума, помех, переключения ключей и движения радара симулятор не
моделирует; скорость радара при работе с ним должна быть нулевой. Предобработке
по-прежнему нужны наборы; для симулятора подходят нейтральные (глава
[Калибровка и предобработка](preprocessing.md)).
## GPIO-кнопка
К компьютеру можно подключить кнопку, которая снимает временный референс — то же,
что **Capture Temporary Reference**. По умолчанию она выключена (раздел
`control_button` профиля, ни в одном примере не включена). Схема, на которую
рассчитаны умолчания: кнопка между линией GPIO и землёй, внутренняя подтяжка к
питанию, нажатие — спад уровня. Дребезг подавляет ядро (`debounce_ms`, по умолчанию
50 мс). Настройки кнопки читаются при запуске окна; после загрузки другого профиля
нужно перезапустить программу.
## Тележка
`device_firmware/cart_firmware` — прошивка платы WeAct STM32G431CB, которая заменяет
пульт тележки: читает джойстик и выдаёт команды движения в формате SBUS. С
программой радара она не связана. Скорость для поправки на движение радар получает
от клиента TCP-локатора (глава [Веб-интерфейс, кнопка и локатор](web.md)).
Протокол пульта — [protocol.md](../device_firmware/cart_firmware/docs/protocol.md).
## Прошивки
| Где | Что | На чём работает |
| --- | --- | --- |
| `device_firmware/multi_libreVNA_firmware/combined.vnafw` | образ мастера: обычные стадии и аппаратный триггер | LibreVNA-мастер |
| `device_firmware/multi_libreVNA_firmware/combined_per_point_switching.vnafw` | образ мастера с переключением стимула | LibreVNA-мастер |
| `device_firmware/multi_libreVNA_firmware/combined_external_lo_slave.vnafw` | образ ведомого | LibreVNA-ведомые |
| `device_firmware/multi_libreVNA_firmware/reproduce/` | полное дерево LibreVNA 1.6.5 с доработками и комплект воспроизводимой сборки | ПК разработчика |
| `device_firmware/switch_controller/` | контроллер ключей для переключения стимула | WeAct STM32G431CB |
| `device_firmware/cart_firmware/` | пульт тележки | WeAct STM32G431CB |
Образ `.vnafw` заливается в LibreVNA программой LibreVNA-GUI (Device → Firmware
Update). Платы STM32 прошиваются через PlatformIO и ST-Link: `pio run -t upload`.
> **Внимание.** `top_master.bin` и `combined.vnafw` — единственные копии рабочего
> битстрима мастера и рабочего образа прошивки, битстрим заново не воспроизводится.
> Никогда их не перезаписывайте; новые сборки сохраняйте под новыми именами.
Подробно:
- [reproduce/README.md](../device_firmware/multi_libreVNA_firmware/reproduce/README.md) — как
устроен комплект сборки, отличия от LibreVNA 1.6.5, что воспроизводится побайтово;
- [install_windows/README.txt](../device_firmware/multi_libreVNA_firmware/reproduce/install_windows/README.txt)
—
сборка мастера с переключением стимула на Windows;
- [per_point_switching.md](../device_firmware/per_point_switching.md) — переключение
стимула: раскладка стадий, изменения FPGA и микроконтроллера, порядок ввода в
строй;
- [switch_controller/README.md](../device_firmware/switch_controller/README.md) —
контроллер ключей: разводка, отчёт, отладка;
- [librevna_protocol.md](../device_firmware/librevna_protocol.md) — USB-протокол
LibreVNA и форматы кадров между процессами программы.
+238
View File
@@ -0,0 +1,238 @@
# Установка и запуск
Программа работает в Linux: на рабочей станции с экраном и на Raspberry Pi 5 без
оператора. Python-часть ставится в виртуальное окружение `.venv` внутри каталога
проекта, C++-часть собирается командой `make` в `build/bin/`.
## Что нужно
| Что | Зачем |
| --- | --- |
| Linux, bash, coreutils, `flock` | запуск через `start.sh` |
| Python 3.10 или новее с модулем `venv` | программа и окружение |
| компилятор C++20 (g++), `make` | процессы конвейера |
| библиотеки `libusb-1.0` и `libatomic.so.1` | USB-доступ к LibreVNA и кольца разделяемой памяти |
| `libxcb-cursor0`, `libxkbcommon-x11-0` (Debian/Ubuntu) | окно Qt на рабочем столе |
| видеокарта NVIDIA с драйвером, NVRTC и заголовки CUDA | только для режима 3D SAR |
| библиотеки L-Card `libx502api.so`, `libe502api.so` | только для радиофотонного радара с АЦП E-502 |
Python-пакеты перечислены в `requirements.txt`:
| Пакет | Для чего |
| --- | --- |
| `numpy` | массивы данных |
| `scipy` | оптимизация в калибровке по объекту |
| `libusb1` | USB-драйвер LibreVNA |
| `pyserial` | последовательный порт платы лазеров |
| `PyQt6` | окно программы (без экрана — тоже, в режиме `offscreen`) |
| `pyqtgraph` | графики |
| `fastapi`, `uvicorn[standard]` | веб-интерфейс |
| `nvidia-cuda-nvrtc`, `nvidia-cuda-runtime` | NVRTC и заголовки CUDA для 3D SAR; ставятся только на x86_64 |
PyQt6 ставится только из готовых сборок (wheel), поэтому pip выбирает самую новую
версию, которая работает с системной glibc.
## Установка
Из каталога проекта:
```sh
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements.txt
make # build/bin/data_preprocessor и build/bin/data_processor
make rph_radar_collector # только для радиофотонного радара с АЦП E-502
```
`./start.sh` при обычном запуске делает это сам: создаёт `.venv`, если его нет, ставит
недостающие пакеты, ставит системные библиотеки через менеджер пакетов (apt, dnf, yum,
pacman, zypper или brew, через `sudo`), правило udev для LibreVNA и собирает C++. Без
экрана (`--headless`) он ничего не ставит: всё должно быть установлено заранее.
## Запуск
```sh
./start.sh --profile run_config_examples/run_config_librevna_multi_per_point.example.json
```
Без `--profile` программа берёт последний использованный профиль, а если его нет —
корневой `run_config.json` (симулятор). Порядок выбора профиля — в главе
[Файл конфигурации](configuration.md). Но сам `start.sh` без `--profile` определяет
модель радара, решает, собирать ли коллектор E-502, и проверяет программы по
корневому `run_config.json`, а `--producer-only` берёт именно его. Поэтому с профилем
радиофотонного радара и для `--producer-only` всегда указывайте `--profile`.
| Ключ `start.sh` | Что делает |
| --- | --- |
| `--profile ПУТЬ` | профиль для запуска; файл должен существовать. Относительный путь — от каталога проекта |
| `--headless` | без окна: окно не показывается, через 0,5 с выполняется **Apply Radar**, ещё через 1 с — **Start**; если **Start** не удался, программа завершается с кодом 1 (`Headless auto-start did not bring the pipeline up`). Ничего не устанавливает |
| `--auto-start` | с окном, но **Start** выполняется сам через 0,5 с после запуска |
| `--producer-only` | запустить только процесс сбора для профиля, без окна; вывод — в терминал |
| `--skip-build` | не собирать C++ (наличие программ всё равно проверяется) |
| `--build-only` | подготовить окружение, собрать и выйти |
| `--clean-shm` | удалить известные кольца в `/dev/shm` перед запуском (глава [Данные на диске](data.md)) |
| `-h`, `--help` | справка |
Порядок работы `start.sh`: проверка окружения → определение модели радара по профилю
→ системные библиотеки и правило udev (не для `--headless`) → Python-пакеты → сборка
C++ (для радиофотонного радара — и коллектора) → проверка программ в `build/bin/` →
остановка службы `radar.service`, если она работает → блокировка
`/tmp/radar_system.lock` → запуск программы. Если блокировку держит другой
экземпляр, `start.sh` завершается с
`Another radar instance already holds /tmp/radar_system.lock; aborting.`
Для разработки окно можно запустить и напрямую:
`.venv/bin/python -m python_app.gui.main`. Тогда нет ни сборки, ни блокировки.
### Первый запуск без железа
1. Запустите `./start.sh` (корневой профиль — симулятор).
2. Нажмите **Preprocessing…**, затем **Create Neutral S21 Sets**: предобработке
нужны наборы калибровки и референса, нейтральные данные не меняют.
3. Нажмите **Start**. Симулятор показывает точечный отражатель, который движется
по сцене; режим обработки меняется списком **Mode**.
Режим 3D SAR без видеокарты NVIDIA показывает только предупреждение.
### Первый запуск с железом
1. Скопируйте нужный профиль из `run_config_examples/` и проверьте в нём серийные
номера, порты, линии GPIO и геометрию антенн (глава
[Файл конфигурации](configuration.md)).
2. Запустите `./start.sh --profile <профиль>`. При первом запуске он может спросить
пароль `sudo`, чтобы поставить системные библиотеки и правило udev для LibreVNA.
3. Нажмите **Apply Radar**, снимите калибровку и референс (глава
[Калибровка и предобработка](preprocessing.md)) и нажмите **Start**.
4. Нажмите **Save Config…**: выбор наборов и правки в окне сохраняются только так.
Порядок работы по шагам — в главе [Типовые сценарии](workflow.md).
## Видеокарта для 3D SAR
Ядра 3D SAR компилируются при запуске библиотекой NVRTC под установленную карту.
`make` ищет `nvrtc.h`, `cuda.h` и `libnvrtc` в пакетах `nvidia-cuda-*` из `.venv` и в
`/usr/local/cuda`, а `libcuda.so.1` (драйвер) — в системных каталогах. Если чего-то
нет, `make` пишет `note: CUDA not found, selecting sar3d will show a warning` и
собирает процессор без 3D SAR. После установки CUDA снова запустите `make`: он
заметит изменение и пересоберёт нужную часть.
- На x86_64 CUDA 13 ставится из `requirements.txt`. Целевая карта — RTX 5070.
- NVRTC 13 не компилирует под карты старее поколения sm_75 (например, GTX 1060,
sm_61). Для них поставьте NVRTC 12 отдельно от `.venv` и соберите в отдельный
каталог:
```sh
pip install --target ~/.cache/radar_nvrtc12 nvidia-cuda-nvrtc-cu12 nvidia-cuda-runtime-cu12
make BUILD_DIR=/tmp/build-nvrtc12 CUDA_ROOTS="$HOME/.cache/radar_nvrtc12/nvidia/cuda_nvrtc $HOME/.cache/radar_nvrtc12/nvidia/cuda_runtime"
```
Программа запускает процессор только из `build/bin/`, поэтому такая сборка нужна
для проверки на старой карте (глава [Разработка](development.md)).
- На Jetson CUDA берётся из JetPack (`/usr/local/cuda`); pip-пакеты CUDA там не
ставятся. PyQt6 на Jetson с glibc 2.35 — версии 6.7.
## Служба без экрана
На машине без оператора программу запускает служба systemd. Установка — один раз,
от обычного пользователя через `sudo`:
```sh
sudo bash deploy/install-daemon.sh
```
Скрипт создаёт `/etc/systemd/system/radar.service`, разрешает пользователю без пароля
`sudo systemctl stop|start|restart radar.service` (`/etc/sudoers.d/radar`) и включает
службу при загрузке, но не запускает её. Служба работает от имени этого пользователя
в каталоге проекта, запускает `start.sh --headless --skip-build`, перезапускается через
3 с после сбоя, а при остановке ждёт программу до 25 с.
```sh
sudo systemctl start radar.service # запустить
systemctl status radar.service # состояние
journalctl -u radar.service -f # журнал
sudo systemctl disable --now radar.service && sudo rm /etc/systemd/system/radar.service /etc/sudoers.d/radar && sudo systemctl daemon-reload # удалить
```
Служба берёт последний использованный профиль. Перед её запуском откройте окно
программы на этой машине, загрузите профиль с железом, выберите наборы и нажмите
**Save Config…**. Работа со службой — в разделе «Работа без экрана» главы
[Веб-интерфейс, кнопка и локатор](web.md).
## Машина развёртывания
Raspberry Pi 5 Model B (Cortex-A76 ×4, 4 ГиБ), Debian 13, Python 3.13. Проект лежит в
`~/Documents/radar_system`. На машине собираются `.venv` и C++; копировать их с
рабочей станции нельзя: там другая архитектура (x86_64).
```sh
python3 -m venv .venv && .venv/bin/python -m pip install -r requirements.txt
make -j4
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
```
На Pi нужна только часть проекта: `data_acq_and_processing`, `deploy`, `python_app`,
`run_config_examples`, `Makefile`, `requirements.txt`, `run_config.json`, `start.sh`.
Каталог `device_firmware/` и документация не нужны.
> **Внимание.** Номер GPIO-чипа 40-контактного разъёма на Pi 5 зависит от версии
> ядра. После установки и обновления ядра проверьте его: `gpiodetect` и
> `gpioinfo -c gpiochip0`. Неверный чип в профиле — ключи, которые молча не
> переключаются.
## Обновление с флешки
На рабочей станции:
```sh
deploy/pack-usb.sh /media/<пользователь>/<флешка>
```
Без аргумента скрипт берёт единственную подключённую съёмную флешку. В каталог
`radar_update/` на ней он кладёт архив `radar_system-<дата коммита>-<хеш>.tar.gz`
с частью проекта для Pi из **последнего коммита** (незакоммиченные изменения в него не
попадают) и скрипт `update.sh`.
На машине, от обычного пользователя (не через `sudo`), из каталога, где лежит
`radar_update`:
```sh
bash radar_update/update.sh # перенести текущий .venv
bash radar_update/update.sh --python 3.13 # собрать .venv заново этим Python
```
Скрипту можно передать путь к архиву: `bash radar_update/update.sh <архив>`; без него
берётся последний по имени `radar_system-*.tar.gz` рядом со скриптом.
Перед обновлением закройте окно программы, иначе скрипт откажется и покажет, какие
процессы работают в каталоге. Службу он остановит сам и запустит снова, если она
работала.
Что делает `update.sh`:
1. проверяет архив и переименовывает текущую версию в `radar_system<N>` — следующий
свободный номер; это копия для отката;
2. разворачивает новую версию в `~/Documents/radar_system` (другой каталог задаёт
переменная `RADAR_DIR`);
3. переносит из прежней версии: `run_config.json`; профили из
`run_config_examples/`, изменённые на машине (версия из архива ложится рядом с
окончанием `.new`); `python_app/runtime/` без журналов; данные
`python_app/data*` и чужие каталоги верхнего уровня — жёсткими ссылками, без
копирования и места на диске; `.venv` — копией;
4. ставит пакеты из `requirements.txt` и собирает C++ (`make all rph_radar_collector`);
5. записывает имя архива в файл `RELEASE` и запускает службу, если она работала.
Если что-то пошло не так, скрипт печатает команду отката. Вручную:
```sh
sudo systemctl stop radar.service
mv ~/Documents/radar_system ~/Documents/radar_system_failed
mv ~/Documents/radar_system<N> ~/Documents/radar_system
sudo systemctl start radar.service
```
Новую версию при откате лучше переименовать, а не удалять: в ней могут быть
измерения, снятые после обновления.
На свежую машину (без прежней версии) `update.sh` спрашивает
`Install a fresh one there? [y/N]` и ставит корневой `run_config.json` из архива, то есть
симулятор: профиль с железом нужно выбрать после установки.
+300
View File
@@ -0,0 +1,300 @@
# Калибровка и предобработка
Препроцессор (`data_preprocessor`) по очереди применяет к каждой коллекции четыре
шага и только потом отдаёт её на обработку. Зачем нужен каждый шаг, объяснено в
главе [Как работает радар](principles.md).
## Цепочка предобработки
Для каждой пары и каждой частотной точки:
| Шаг | Формула | Набор |
| --- | --- | --- |
| 1. Калибровка | S21 ← S21 / S21_through | набор калибровки (through) |
| 2. Вычитание референса | S21 ← S21 − S21_ref / S21_through | набор референса |
| 3. Режекторный фильтр | S21 ← S21 · m(f) | настройки `preprocess.notch` в профиле |
| 4. Калибровка по объекту | S21 ← S21 · F | набор калибровки по объекту, если выбран |
Референс делится на through один раз при загрузке, поэтому вычитается уже
откалиброванный фон. Точки, где |S21_through|² не больше 10⁻¹², на шаге 1 остаются как
есть, без деления. Окно съёма и номер коллекции не меняются.
У каждого набора должна быть каждая пара сбора, а сетка частот — совпадать с
развёрткой: то же число точек, а каждая частота отличается от своей не больше чем на
10⁻⁵ её значения, но допуск не меньше 1 мГц (на 6 ГГц — 60 кГц). Наборы проверяются при запуске
препроцессора, и неподходящий набор его останавливает. Если это случилось, пока
программа ждёт готовности процессов, **Start** отказывает с сообщением
``Failed to start pipeline: RuntimeError: Process `data_preprocessor` exited with code 1.``
и текстом ошибки. Если позже, в журнале окна появляется отчёт о завершении
процесса, а программа перезапускает конвейер с нарастающей паузой.
Коллекцию, которая не прошла проверку уже во время
работы, препроцессор отбрасывает и пишет об этом в свой журнал
`python_app/runtime/logs/data_preprocessor.err.log`. В окне программы это видно
только по тому, что изображение перестаёт обновляться.
## Наборы
| Набор | Каталог | Обязателен |
| --- | --- | --- |
| S21 Calibration — калибровка (through) | `python_app/data/s21_calibration/` | да |
| S21 Reference — референс | `python_app/data/s21_reference/` | да |
| Object Calibration — калибровка по объекту | `python_app/data/s21_object_calibration/` | нет |
Набор — архив NPZ с массивами частот и S21 по парам и JSON-файл рядом с описанием
(формат — в главе [Данные на диске](data.md)). Наборы лежат в подкаталогах с именем
`radar_key` — ключа конфигурации радара:
- у LibreVNA ключ складывается из модели, серийных номеров, начала и конца
развёртки, числа точек, полосы ПЧ и мощности, а у многоприборной схемы ещё из
положений ключей;
- у радиофотонного радара ключ — хеш выходной полосы, фазовой калибровки, параметров
источника, положений ключей и настроек лазера; лазер входит в ключ, если плата
лазеров включена в профиле или источник — плата RFG.
Программа показывает только наборы текущего ключа. Если изменить любую из этих
величин (например, число точек), старые наборы пропадают из списков, а выбор
сбрасывается с записью `Preprocess selection reset: <старый ключ> -> <новый ключ>; cleared {...}`
в журнале. Так программа не даёт применить калибровку, снятую при других
настройках. Если вернуть прежние настройки, наборы снова появятся в списках, но
выбрать их придётся заново.
Наборы применяются при запуске сбора: программа пересобирает из выбранных наборов
файлы `python_app/runtime/*_bundle.bin` и заново запускает препроцессор. Запуск — это
**Start**, а также продолжение сбора после приостановки: после съёма набора,
временного референса или нейтральных наборов сбор, если он шёл, продолжается уже с
новыми наборами. Смена выбора в списке во время сбора применится при следующем
запуске.
Перед запуском программа проверяет наборы и, если что-то не так, отказывается
запускаться, ничего не остановив. Окно ошибки показывает
`Failed to start pipeline: <тип ошибки>: <текст>`, где текст — один из этих:
| Условие | Сообщение |
| --- | --- |
| не выбраны калибровка или референс | `Select all required preprocess sets in Preprocessing Panel before Start: S21 Calibration, S21 Reference` |
| файла набора нет для текущего ключа | `Missing set files for <вид>/<radar_key>/<имя>` |
| в наборе нет какой-то пары сбора | `Selected <набор> set does not cover requested run combos` |
| архив повреждён или содержит не числа | `Corrupted preprocess set <вид>/<ключ>/<имя>: …` |
| калибровка по объекту не подходит | см. раздел «Калибровка по объекту» |
## Окно Preprocessing Setup
Открывается кнопкой **Preprocessing…**. Окно не модальное: пока оно открыто, главным
окном можно пользоваться.
Группа **Preprocess Sets**:
| Элемент | Что делает |
| --- | --- |
| **Set name** | имя нового набора; по умолчанию `set_001`. Для этого вида набора и этого ключа радара имя должно быть новым |
| **Create Neutral S21 Sets** | создаёт нейтральные наборы (см. ниже). Кнопка есть только у радиофотонного радара, у многоприборной схемы и у одиночного LibreVNA в режиме `mock` |
| **Refresh Sets** | перечитывает списки наборов с диска и каталог вариантов радара |
| **Sweeps per combo (median)** | сколько развёрток снимать на каждую пару, от 1 до 99, по умолчанию 5. В набор идёт медиана по каждой частоте, отдельно по вещественной и мнимой части: при трёх развёртках и больше одиночный сбой развёртки в набор не попадает. Это же число развёрток берут временный референс и калибровка по объекту |
| **Radar Config Variants** → **Directory**, **Browse…** | каталог с профилями JSON для съёма наборов сразу под несколько настроек развёртки (см. ниже). Под полем — итог просмотра: `Directory: <каталог> \| json=…, valid=…, skipped=…, duplicates=…` (файлов JSON, подходящих вариантов, пропущенных, повторов) или `No radar config directory selected.` |
| **Capture current combo for all found radar configs** | включает съём по всем вариантам из каталога |
| **S21 calibration**, **S21 reference** | выбор набора калибровки и референса для следующего **Start** |
| **Object calibration** | выбор калибровки по объекту; **Off** — не применять |
| **Create…** | открывает окно калибровки по объекту |
Группа **Capture All Antenna Pairs**:
| Элемент | Что делает |
| --- | --- |
| **Active type**, **Progress**, **Current combo** | вид снимаемого набора, сколько пар снято из скольких, какая пара следующая; без съёма — `<none>`, `0 / 0`, после последней пары — `<complete>` |
| **Start S21 Calibration**, **Start S21 Reference** | начинают съём набора |
| **Capture Current Combo** | снимает следующую пару |
| **Capture All Remaining** | снимает подряд все оставшиеся пары |
| **Undo Last Capture** | отменяет последний съём, чтобы переснять пару |
| **Save Captured Set** | сохраняет набор и выбирает его; доступна, когда сняты все пары |
| **Abort Sequence** | прерывает съём и отбрасывает снятое |
Ниже — журнал съёма (`S21 Calibration: 1/8 | input=0 output=0` на каждую пару),
строка состояния (сначала `Ready`) и графики модуля и фазы последней снятой развёртки
(до первого съёма — `Preview will appear after the first successful capture.`).
Последняя развёртка рисуется и в главном окне.
## Съём калибровки и референса
1. Выберите профиль и настройки радара. Наборы привязаны к ключу радара, поэтому
снимайте их при тех настройках развёртки, с которыми будете работать.
2. Подготовьте установку. Для калибровки — эталонный тракт для текущей пары; для
референса — пустую сцену: всё, что должно остаться в фоне, на месте, целей нет.
3. Нажмите **Preprocessing…**, задайте **Set name** и
**Sweeps per combo (median)**.
4. Нажмите **Start S21 Calibration** или **Start S21 Reference**. Сбор
приостанавливается, и на время съёма прибором управляет окно предобработки.
5. Снимайте пары кнопкой **Capture Current Combo** или все сразу кнопкой
**Capture All Remaining**. Проверяйте каждую развёртку по графикам; неудачную
переснимите через **Undo Last Capture**. При ошибке съёма уже снятые пары
сохраняются, и съём можно повторить.
6. Когда сняты все пары, нажмите **Save Captured Set**. Набор сохраняется и
выбирается, приостановленный сбор продолжается.
Съём всегда идёт по полной матрице положений ключей, а не только по парам сбора.
- Одиночный LibreVNA: для каждой пары программа выставляет ключи, ждёт время
установления (`run.settling_ms`, в окне — **Settling (ms)**) и снимает нужное
число развёрток этой пары.
- Многоприборная схема при калибровке: прибор снимает всю матрицу, а в набор идёт
только текущая пара, по одной за нажатие. **Capture All Remaining** здесь
недоступна.
- Многоприборная схема при съёме референса: одно нажатие снимает всю матрицу нужное
число раз, и набор сразу готов.
### Варианты радара
Иногда наборы нужны сразу под несколько развёрток — например, под разные полосы
частот. Для этого в **Directory** указывают каталог с профилями JSON. Профиль
считается вариантом, если в его `radar.sweep` есть хотя бы одно из полей
`start_hz`, `stop_hz`, `points`, `if_bandwidth_hz`, `stimulus_power_dbm`; эти поля
накладываются на текущий профиль, повторы одного ключа пропускаются. С включённой
**Capture current combo for all found radar configs** каждое нажатие снимает пару во
всех вариантах подряд, с перенастройкой прибора. **Save Captured Set** сохраняет
по набору на каждый вариант под одним именем, а картинки развёрток — в
`python_app/data/preview_png/<вид>/<имя>/<radar_key>/i<вход>_o<выход>.png`.
## Временный референс
Кнопка **Capture Temporary Reference** в секции **Data Actions** снимает референс
одним нажатием, не открывая окно предобработки. То же действие есть в
веб-интерфейсе и на GPIO-кнопке.
Программа приостанавливает сбор, снимает полную матрицу с текущими настройками
(медиана **Sweeps per combo (median)** развёрток) и сохраняет её как набор
`tmp_reference`, заменяя прежний. Затем она выбирает этот набор, очищает историю
обработки, рисует последнюю развёртку с подписью `Tmp S21 Reference last trace` и
продолжает сбор. Процессор и локатор при этом не перезапускаются, клиенты локатора
не отключаются.
Временный референс нужен, когда сцена или положение радара изменились: после
перестановки, на новом месте, после заметного изменения обстановки.
## Нейтральные наборы
**Create Neutral S21 Sets** сохраняет под именем из **Set name** два набора:
калибровку, равную 1, и референс, равный 0, для всех пар на текущей сетке частот.
С ними данные проходят предобработку без изменений. Они нужны, чтобы запустить
конвейер, пока настоящей калибровки нет: с симулятором, при первой проверке прибора.
Если набор с таким именем уже есть, программа откажется. После создания наборы
выбираются, история очищается, а процессор перезапускается: сразу, если сбор шёл,
иначе при следующем **Start**.
## Режекторный фильтр
Фильтр настраивается только в профиле, в разделе `preprocess.notch`; в окне
программы его нет.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `enabled` | `false` | включает фильтр |
| `bands_hz` | `[]` | полосы подавления: список пар `[нижняя, верхняя]` в герцах, верхняя не меньше нижней |
| `taper_width_hz` | `40e6` | ширина плавного края w, Гц, не меньше 0 |
| `taper_type` | `"cosine"` | форма края: `"cosine"` — косинусный спад, `"hard"` — без спада |
Внутри полосы [L, H] множитель m(f) равен нулю. При `"cosine"` по краям полосы он
плавно спадает от 1 до 0 по закону 0,5·(1 + cos(…)) на участках [L − w, L] и
[H, H + w]. При `"hard"` (или w = 0) зануляется вся полоса [L − w, H + w]. Края
соседних полос перемножаются.
Во всех профилях-примерах `enabled: true` и `bands_hz: []`, то есть фильтр ничего не
подавляет. Изменение фильтра применяется при следующем **Start** и сбрасывает
историю.
## Калибровка по объекту
Калибровка по объекту находит для каждой пары комплексную поправку по частоте так,
чтобы отклик известного отражателя у всех пар совпадал по фазе. Эталон — ноутбук
`MIMO_calibration.ipynb`, он хранится вне репозитория. Поправку можно строить по
нескольким положениям отражателя: они уточняют друг друга.
Что нужно заранее:
- кольцо `rings.preprocessed_tap` в профиле: калибровка снимает данные из него;
- таблицы геометрии антенн: в калибровку входят только пары, у которых есть
геометрия обеих антенн, остальные получают поправку 1;
- выбранный набор калибровки (through): поправка запоминает его имя и содержимое и
годится только вместе с ним;
- металлический шар известного радиуса (или другой точечный отражатель) и способ
измерить положение его центра.
Окно **MIMO Object Calibration** открывается кнопкой **Create…** в окне
предобработки. Вверху — подсказка о порядке работы
(`Capture data, then enter the measured reflector centre: …`), внизу — строка
состояния (сначала `Ready to capture`), а на месте карт фаз до расчёта —
`Pair phases at the target will appear after calculation.`
| Элемент | Что делает |
| --- | --- |
| **Set name** | имя набора поправки |
| таблица **Target**, **Sweeps**, **X (m)**, **Y (m)**, **Z (m)** | снятые положения отражателя: сколько коллекций снято и измеренный центр |
| **Capture Data** | снимает данные для нового положения отражателя |
| **Remove Selected Target** | удаляет выбранное положение |
| **Sphere radius (m, 0 = point)** | радиус шара, по умолчанию 0,16 м; 0 — точечный отражатель |
| **Antenna origin height above ground (m)** | высота начала осей антенн над землёй, по умолчанию 0,92 м |
| **Radar downward tilt (°)** | наклон радара вниз, по умолчанию 10° |
| **Calculation Settings** | раскрывает остальные параметры расчёта (таблица ниже) |
| **Calculate and Save** | считает поправку в фоне и сохраняет набор |
| **Cancel Operation** | прерывает съём или расчёт |
| **Close** | закрывает окно |
Параметры в **Calculation Settings**:
| Параметр | По умолчанию | Что делает |
| --- | --- | --- |
| **Estimation band start (MHz)**, **Estimation band stop (MHz)** | 2000 и 6000 | полоса, по которой оценивается поправка; в ней должно быть не меньше 4 точек |
| **Echo path gate half-width (m)** | 0,15 | полуширина окна вокруг ожидаемого пути до отражателя, в котором ищется его эхо |
| **Gate taper width (m)** | 0,03 | ширина плавного края этого окна |
| **Coarse path search (±m, 0 = off)** | 0,6 | предварительный поиск: на сколько можно сдвинуть отражатель по дальности до самого сильного эха |
| **Coordinate refinement (±m)** | 0,1 | в каких пределах уточнять координаты отражателя |
| **Refine coordinates** X, Y, Z | все три | по каким осям уточнять координаты |
| **Antenna phase model** | `linear` | модель фазы антенны по частоте: `const` — постоянная, `linear` — линейная (задержка), `poly3` — многочлен третьей степени, `raw` — без сглаживания |
| **Equalize amplitudes** | выключен | выравнивать и амплитуды, а не только фазы |
Порядок:
1. Поставьте отражатель в сцену и измерьте положение его центра в осях сцены: X —
вдоль ряда антенн, Y — высота над землёй, Z — вдоль земли от радара.
2. Нажмите **Capture Data**. Программа приостанавливает текущий сбор и запускает
конвейер с этим же профилем, но без калибровки по объекту и по всем парам из
таблиц геометрии (многоприборная схема — по полной матрице). Она собирает
столько новых коллекций, сколько задано в
**Sweeps per combo (median)**, и возвращает прежний режим работы. Предел ожидания —
300 с.
3. Введите X, Y, Z в строке нового положения.
4. Повторите для других положений, если нужно. Все положения должны быть сняты при
одних и тех же настройках радара, геометрии и предобработке.
5. Нажмите **Calculate and Save**. Расчёт идёт в фоне. После него окно показывает
карты фаз пар в точке отражателя до и после поправки (**Before**, **After**):
строки — передающие антенны, столбцы — приёмные, фаза — относительно суммы по
всем парам, по шкале ±180°, с разбросом в градусах.
**Capture Data** отказывает, если идёт однократный съём или запись на диск
(`Finish the single capture or disk recording before taking calibration data`), если
нет таблиц геометрии (`Configure Tx/Rx antenna positions before taking calibration data`)
или кольца копии
(`Object calibration requires rings.preprocessed_tap; enable it in the run profile`).
6. Если набор подходит к текущим настройкам, он выбирается и применяется при
следующем **Start** (`Applies on the next Start.`). Иначе он сохраняется
невыбранным с объяснением `Not selected for the current configuration: <причина>`.
Коллекции одного положения усредняются. По каждой частоте поправка раскладывается на
множители передающих и приёмных антенн; фазы сглаживаются выбранной моделью и
продолжаются на всю сетку частот. Поправка пары — сопряжённая фаза произведения
множителей, а с **Equalize amplitudes** — обратное произведение.
Перед каждым **Start** программа проверяет, что поправка снята при тех же настройках
радара, той же геометрии и с тем же набором калибровки (по имени и содержимому), что
она и калибровка покрывают все пары сбора, не содержит нулей и имеет ту же сетку
частот. Сообщения при отказе:
- `Object calibration has no capture provenance; capture it again`;
- `Object calibration radar configuration differs from the captured state` (а также
`antenna geometry`, `through calibration name`, `through calibration content`);
- `Object calibration and through must cover input=i, output=o`;
- `Object calibration corrections must be nonzero`;
- `Object calibration frequency grid differs for input=i, output=o`.
Если изменить таблицу геометрии так, что она перестала совпадать с геометрией
выбранной поправки, выбор поправки снимается, а работающий с ней сбор
приостанавливается и продолжается уже без неё.
+240
View File
@@ -0,0 +1,240 @@
# Как работает радар
Радар закреплён над землёй с наклоном вниз и смотрит вперёд, на участок земли в
нескольких метрах от себя. Он излучает радиоволны, принимает отражённые и по ним
строит изображение этого участка: где лежат или стоят предметы, отражающие волну.
Незнакомые термины — в [словаре](glossary.md).
## Отражение и задержка
Передающая антенна излучает синусоидальную волну частоты f. Волна идёт в воздухе со
скоростью c ≈ 3·10⁸ м/с, отражается от предметов и частично доходит до приёмной
антенны. Длина волны λ = c/f; на частоте 3 ГГц это 10 см. Сильнее всего отражают
металлические предметы; вообще отражает всё, чьи электрические свойства отличаются
от воздуха.
Передающая и приёмная антенны стоят в разных местах, поэтому путь волны складывается
из двух отрезков: от передающей антенны до предмета (R_tx) и от предмета до приёмной
(R_rx). Принятая волна запаздывает на
τ = (R_tx + R_rx) / c.
Предмет в 4 м от антенн даёт путь около 8 м и задержку около 27 нс.
Для синусоиды задержка проявляется как сдвиг фазы: принятый сигнал
A·cos(2πf(t − τ)) отстаёт от излучённого на φ = 2πfτ. Фаза известна только с
точностью до целого числа периодов, поэтому по одной частоте расстояние не
определить. Его определяют по тому, как фаза меняется с частотой.
## Векторный анализатор цепей и S21
Векторный анализатор цепей (ВАЦ, англ. VNA) выдаёт на один свой порт синусоиду
заданной частоты и измеряет амплитуду и фазу сигнала, пришедшего на другой порт,
относительно выданного. Результат измерения — комплексный коэффициент передачи S21
из порта 1 в порт 2: отношение волны, пришедшей на порт 2, к волне, поданной в порт 1.
По смыслу он похож на отношение комплексных амплитуд выходного и входного напряжений
в расчёте цепей переменного тока и совпадает с ним, когда оба порта согласованы:
модуль |S21| показывает ослабление, аргумент — сдвиг фазы.
В радаре к излучающему порту прибора подключена передающая антенна, к принимающему —
приёмная. Цепью между портами служит сцена, и S21 показывает, что сцена передаёт от
одной антенны к другой на данной частоте.
В системе работает LibreVNA — двухпортовый ВАЦ с открытой прошивкой, частотами до
6 ГГц и подключением по USB. Его основные величины:
| Величина | Что это | У LibreVNA |
| --- | --- | --- |
| Модуль S21 в дБ | 20·lg\|S21\|: −20 дБ — амплитуда в 10 раз меньше, −60 дБ — в 1000 раз | — |
| Мощность излучения, дБм | децибелы относительно 1 мВт: 10·lg(P / 1 мВт); 0 дБм = 1 мВт, −3 дБм ≈ 0,5 мВт, −40 дБм = 0,1 мкВт | от −40 до 0 дБм |
| Полоса ПЧ (IF bandwidth, IFBW) | полоса фильтра приёмника; на каждой частоте прибор накапливает сигнал около 1/IFBW (при 50 кГц — 20 мкс). Узкая полоса снижает шум, но замедляет измерение | от 6 Гц до 50 кГц |
| Число частотных точек | сколько частот в одной развёртке | до 4501 |
## Развёртка по частоте и дальность
Развёртка (sweep) — проход прибора по списку частот: S21 измеряется в N частотных
точках, равномерно расставленных от f_start до f_stop с шагом
Δf = (f_stop − f_start) / (N − 1).
Если в сцене один отражатель с задержкой τ, то S21(f) = a·e^(−j2πfτ): модуль
постоянен, а фаза линейно убывает с частотой, тем быстрее, чем дальше отражатель.
Несколько отражателей дают сумму таких слагаемых.
Обратное преобразование Фурье по частоте переводит набор значений S21(f) в функцию
времени с пиками в моменты τ каждого отражателя — так же, как частотная
характеристика цепи переводится в импульсную. Время пересчитывается в путь L = c·t.
Получается А-скан (профиль дальности): амплитуда отражения в зависимости от
расстояния. Радары, которые так измеряют дальность, называют радарами со ступенчатым
изменением частоты. Дальность здесь — половина пути, L/2: при близко стоящих антеннах
это и есть расстояние до отражателя.
Что способен показать А-скан, определяют две величины:
| Величина | Формула | Пример: развёртка 0,5–6 ГГц, 1031 точка, обработка в полосе 1,5–6 ГГц |
| --- | --- | --- |
| Разрешение по дальности — на каком расстоянии друг от друга два отражателя ещё видны раздельно | ΔR ≈ c / (2B), B — ширина полосы, по которой строится А-скан | B = 4,5 ГГц, ΔR ≈ 3,3 см; с окном Ханна (ниже) пик вдвое шире |
| Предельная дальность — дальше неё картина повторяется | R_max = c / (2Δf) | Δf = 5,34 МГц, R_max ≈ 28 м |
Отражение от предмета дальше R_max не исчезает: оно появляется на неверной, меньшей
дальности. Поэтому число точек выбирают так, чтобы R_max с запасом превышала самую
дальнюю интересующую дальность. Развёртка 1 МГц–6 ГГц из 201 точки даёт Δf ≈ 30 МГц и
R_max ≈ 5 м.
У каждого пика А-скана есть боковые лепестки — ложные слабые пики по сторонам.
При резких краях полосы первый лепесток всего на 13 дБ ниже главного пика, и рядом с
сильным отражателем видны призраки. Окно Ханна плавно уменьшает вклад крайних частот:
лепестки опускаются примерно до −31 дБ, а главный пик становится примерно вдвое шире.
Окно Ханна применяют режимы GPR и 3D SAR и калибровка по объекту; B-scan строится
без окна.
## Антенны, пары и матрица
Каждое сочетание передающей и приёмной антенны — пара (в программе combo) — даёт
свою развёртку S21. M передающих и N приёмных антенн дают M × N пар. В программе
передающая сторона пары называется выходом (output, с него прибор излучает),
приёмная — входом (input). Пара записывается как `вход:выход` или `in0/out1`.
Положение каждой антенны задаётся в таблицах геометрии: номер выхода или входа и
координаты X, Y, Z в метрах.
Пары смотрят на сцену из разных точек. Сложенные с правильными задержками, они дают
изображение, чёткость которого поперёк сцены определяется размером всей группы
антенн, а не одной антенны.
Пар больше, чем портов у прибора. Их набирают двумя способами.
ВЧ-ключ (коммутатор H7992) подключает порт прибора к одной из антенн, до четырёх.
Положение ключа задают два логических сигнала с GPIO компьютера. Программа выставляет
ключи для очередной пары, ждёт, пока сигнал установится, и снимает развёртку.
Многоприборная схема — три LibreVNA, работающие синхронно. Ведущий прибор (мастер)
на каждой частоте излучает по очереди с двух портов, два ведомых принимают каждый на
два порта, и за
одну развёртку снимаются 2 × 4 = 8 пар. В режиме переключения стимула внутри
частотной точки на каждом порту мастера стоит ключ, которым управляет контроллер
STM32: на каждой частоте каждый порт излучает дважды, через оба положения ключа, и
развёртка даёт 4 × 4 = 16 пар. Все 16 измеряются на одной частоте за доли
миллисекунды, поэтому движение радара почти не сдвигает их друг относительно друга.
Обход всех пар называется коллекцией (collection). Программа обрабатывает данные
коллекциями. У коллекции и у каждой её развёртки есть окно съёма — время начала и
конца измерения по часам компьютера; по нему программа учитывает движение радара.
## Калибровка и вычитание фона
Измеренный S21 описывает не только сцену. Кабели, ключи, тракты антенн и сам прибор
по-своему меняют амплитуду и фазу на каждой частоте, а антенны «видят» друг друга
напрямую. Эти составляющие программа убирает наборами, снятыми заранее (подробно — в
главе [Калибровка и предобработка](preprocessing.md)):
1. Набор калибровки (through) — эталонное измерение тракта. Каждая развёртка
делится на него, и частотная характеристика аппаратуры сокращается.
2. Набор референса — измерение пустой сцены. В нём всё, что не меняется: прямая
связь между антеннами, земля, неподвижные предметы. Референс вычитается, и
остаётся то, что появилось в сцене после его съёма. Временный референс снимается
одной кнопкой прямо во время работы, например после перестановки радара.
3. Режекторный фильтр (notch) обнуляет заданные полосы частот, например полосу
постоянной помехи; края полос сглаживаются.
4. Калибровка по объекту — поправка для каждой пары, найденная по известному
отражателю (металлическому шару) в измеренной точке. Она выравнивает пары по фазе,
чтобы в этой точке они складывались синфазно.
## Изображения
### B-scan
B-scan — А-сканы одной пары за последовательные развёртки, поставленные рядом:
по горизонтали номер развёртки, по вертикали дальность, цветом — амплитуда.
Неподвижный отражатель рисует горизонтальную линию, приближающийся — наклонную.
### Когерентная обратная проекция
Изображение по всем парам строится обратной проекцией. Для каждой точки P
изображения программа вычисляет для каждой пары задержку
τ = (|P − Tx| + |P − Rx|) / c, берёт значение А-скана этой пары в момент τ и
складывает комплексные значения всех пар. Если в P есть отражатель, слагаемые
совпадают по фазе и сумма велика. В остальных точках фазы разные, и слагаемые
взаимно гасятся. Яркость точки — модуль суммы. Сложение с учётом фазы называется
когерентным.
Режим GPR считает такую сумму в точках плоскости земли перед радаром (сетка 300 × 300:
поперёк и вдоль дальности), находит на карте яркие пятна — объекты — и отдаёт их
внешним программам через TCP-локатор.
### 3D SAR
Режим 3D SAR считает ту же сумму в каждой ячейке (вокселе) трёхмерной сетки над
землёй. К текущей коллекции можно добавить несколько предыдущих: если радар едет,
каждая снята из другой точки, и вместе они образуют синтезированную апертуру
(synthetic aperture radar, SAR) — как будто антенн больше и они расставлены шире.
Объём показывается проекциями максимума: спереди, сбоку, сверху и в наклонном виде.
### Что программа делает перед суммированием
Развёртка длится заметное время, а радар может ехать. Программа знает время каждой
частотной точки и скорость радара и поправляет фазу так, будто вся коллекция снята в
один момент. Скорость задают в настройках или передаёт внешний клиент локатора.
Пары различаются усилением: у них разные кабели, антенны и углы обзора. Программа
выравнивает уровни пар по дальности и нормирует каждую пару на медиану её А-скана.
Фаза при этом не меняется.
### Фон кадра и CLEAN
В 3D SAR программа сравнивает максимум кадра с фоном — медианой значений рабочей
части объёма, типичным уровнем помех. Если объектов CLEAN нет, кадр появляется на
экране, когда максимум превышает фон в 6 раз, и показывается в полную яркость начиная
с 8,5 раза: кадр без явной цели не выглядит целью.
Сильный отражатель окружён боковыми лепестками, которые закрывают слабые. Алгоритм
CLEAN находит самую сильную точку, вычитает её отклик из данных и повторяет, пока в
изображении есть точки в 4,5–5,5 раза выше фона, но не больше 15 раз. Затем каждый
найденный объект рисуется в своей шкале, и слабый объект виден рядом с сильным;
найденные объекты видны всегда, а порог фона прячет только остаток. Рамки вокруг групп
объектов показывают, где
объект по высоте: очень низко, на земле, над землёй или у края сцены.
## Радиофотонный радар
Второй тип радара в системе формирует СВЧ-сигнал с помощью лазеров (радиофотонный
радар, RPH). Плата лазеров меняет ток лазера LD1 ступенями — по умолчанию от 97,3 до
107 мА с шагом 0,05 мА, 50 мкс на ступень, — и вместе с током меняется частота
сигнала. Принимаются два сигнала: основной, прошедший через сцену, и опорный,
прошедший постоянную задержку. Их оцифровывает АЦП L-Card E-502 или плата RFG.
Фаза опорного сигнала растёт с частотой линейно, поэтому по ней программа узнаёт
частоту каждой ступени: две опорные точки «фаза — частота» задаются фазовой
калибровкой. Затем развёртка пересчитывается на равномерную сетку частот (по
умолчанию 2,1–5,5 ГГц, 2048 точек), и дальше данные обрабатываются так же, как
развёртки ВАЦ.
## Координаты
Положения антенн задаются в осях радара: X — вдоль ряда антенн (поперёк направления
обзора), Y — вверх
относительно радара, Z — вдоль оси, куда радар смотрит. Начало осей находится на
высоте h над землёй, и радар наклонён вниз на угол обзора p.
Изображения 3D SAR, рамки объектов и положение отражателя при калибровке по объекту
задаются в осях сцены: X — та же ось вдоль ряда антенн, Y — высота над землёй
(Y = 0 — земля), Z — вдоль
земли от радара.
```text
Y
│
h ●- - - - - - - - - - - - горизонталь
│ \ p — угол обзора
│ \
│ \ ось обзора радара (Z радара)
│ \
──┴─────────\────────────────────► Z сцены, вдоль земли
земля, Y = 0 ось обзора пересекает землю при Z = h / tg p
```
Настройки «глубины» — **Min depth (m)** и **Max depth (m)** в GPR, **MIN_DEPTH** и
**MAX_DEPTH** в 3D SAR —
ограничивают расстояние вдоль оси обзора радара, а не глубину под землёй. Высота h
задаётся настройкой **Radar height (m)** в GPR и **BP_GROUND_DEPTH_M** в 3D SAR, угол
обзора — **Look angle (°)** и **LOOK_ANGLE_DEG**. Формулы перехода между осями — в
главе [Режимы обработки](processing.md).
+475
View File
@@ -0,0 +1,475 @@
# Режимы обработки
Процессор (`data_processor`) превращает предобработанные коллекции в результат
выбранного режима. Режимов четыре: pass-through, B-scan, GPR и 3D SAR; в списке
**Mode** под кнопками управления они называются `pass_through`, `bscan`, `gpr` и `sar3d`.
Режим и его настройки меняются
на ходу, сбор при этом не перезапускается. Физический смысл обработки объяснён в
главе [Как работает радар](principles.md), здесь — что делает каждый режим и каждая
его настройка.
## Когда применяются настройки
Изменённая настройка обработки сразу уходит процессору. Программа записывает живые
настройки в `python_app/runtime/processing_live.json` с новым номером ревизии.
Процессор перечитывает файл примерно раз в 100 мс и пересчитывает новейшую
сохранённую коллекцию, а в режиме B-scan — все сохранённые.
Если программа не смогла проверить или записать настройки, поля возвращаются к
последним опубликованным значениям и появляется сообщение
`Failed to update live processing settings: …` (в веб-интерфейсе —
`Failed to apply web live settings: …`). Неверную правку таблицы антенн окно
отклоняет сразу, сообщением **Invalid Antenna Geometry**. Если файл не принял сам процессор,
он продолжает работать с последними принятыми настройками и пишет предупреждение в
свой журнал `python_app/runtime/logs/data_processor.err.log`.
GPR и 3D SAR рисуют только результаты текущей ревизии. После изменения программа до
0,8 с ждёт пересчитанный результат, поэтому старая картинка на экране не мелькает.
Если процессор не работает, вид остаётся пустым до следующего результата.
Часть настроек только перерисовывает готовый результат: параметры вида pass-through,
**Subtract mean A-scan** и **Scans to show (stopped)** в B-scan, **Render mode** в GPR,
**SAR_CLOUD_THRESHOLD**, **Zoom (%)** и **SAR_MARK_ENABLE** в 3D SAR. Они сохраняются в
профиль, но процессору не передаются.
У страниц GPR и 3D SAR свои поля для одинаковых по смыслу настроек: номера антенн,
границы дальности, полоса частот, скорость, угол обзора, вычитание среднего,
выравнивание пар. Процессор получает значения со страницы выбранного режима, а в
pass-through и B-scan — со страницы GPR. Поэтому переключение между GPR и 3D SAR
меняет и эти значения: угол обзора по умолчанию — 0° в GPR и 19,5° в 3D SAR.
Границы полосы частот (**Start (MHz)**, **Stop (MHz)**, **F_START MHz**,
**F_STOP MHz**) не выходят за развёртку прибора, а у радиофотонного радара — за его
выходную полосу. Значения за пределами программа подрезает к границе; если это
случилось после изменения развёртки, в журнале появляется
`Processing frequency limits were clamped to the active radar sweep.`
## Pass-through
Показывает предобработанный S21 каждой пары по частоте: модуль в дБ (20·lg|S21|) и
фазу в градусах. Обработки нет; режим нужен, чтобы проверить сигнал, калибровку и
референс. Подпись кривой — `in<вход>/out<выход>`. У одиночного LibreVNA и
радиофотонного радара в режимах pass-through и B-scan снимается только пара `0:0`,
если оба ВЧ-ключа в `mock` или у обоих одно положение. Результаты других режимов здесь не
рисуются: после переключения на pass-through экран ждёт первого результата этого
режима.
Все параметры этого режима только перерисовывают график.
| Параметр | По умолчанию | Что делает |
| --- | --- | --- |
| **Show magnitude** | включён | показывает график модуля |
| **Show phase** | включён | показывает график фазы |
| **Unwrap phase** | выключен | убирает скачки фазы на 360° (развёртывает её); шкала фазы подбирается по данным, без развёртки она закреплена на ±180° |
| **Switch combos** | пусто — все пары | список пар `вход:выход` через запятую, например `0:0,1:0`. При ошибке в записи показываются все пары, а в журнале — `Invalid pass-through switch-combo filter.` |
| **Fix magnitude Y range** | выключен | закрепляет шкалу модуля; без неё шкала подбирается по данным |
| **Y min (dB)**, **Y max (dB)** | −100 и 0 | границы закреплённой шкалы, от −240 до 240 дБ; доступны при включённой **Fix magnitude Y range** |
## B-scan
Для каждой пары строит А-скан и укладывает А-сканы последовательных развёрток в
изображение: по горизонтали номер развёртки (**Sweep #**), по вертикали дальность
(**Range (m)**). На экране — одна пара; её выбирают в списке **B-scan pair** в
секции ключей (пары записаны как `вход : выход`), а кнопка **Next Combination**
переходит к следующей: сначала меняется выход, затем вход, после последней пары снова
первая. В списке все пары, которые есть в истории, независимо от текущего списка пар
сбора. Список и кнопка доступны только в режиме B-scan, кнопка — когда пар хотя бы
две.
Как строится А-скан:
1. Из развёртки берутся точки в полосе от **Start (MHz)** до **Stop (MHz)**. Если их
меньше двух, профиля нет, и развёртка в изображение не попадает.
2. Обратное преобразование Фурье без окна.
3. Отсчёт с временем t соответствует пути c·t (c = 299 792 458 м/с). Дальность
отсчитывается от **Cut (m)**: d = (c·t − 2·Cut) / 2, показываются отсчёты с
0 ≤ d ≤ **Max depth (m)**.
4. Значение — модуль, вещественная часть или фаза отсчёта (**Axis**), умноженное на
d в степени **Gain**.
| Параметр | По умолчанию | Пределы | Что делает |
| --- | --- | --- | --- |
| **Axis** | `abs` | `abs`, `real`, `phase` | что рисовать: модуль, вещественную часть или фазу (в радианах). Для `abs` — шкала от тёмного к светлому, для остальных — сине-бело-красная, симметричная относительно нуля |
| **Cut (m)** | 0,824 | 0–2 м | дальность, с которой начинается изображение; всё ближе отрезается, а ось дальности начинается с нуля в этой точке |
| **Max depth (m)** | 1,0 | 0,1–20 м | сколько метров дальности показывать после **Cut (m)** |
| **Gain** | 1,0 | 0–3 | усиление с дальностью: значение умножается на d^Gain, чтобы далёкие слабые отражения были видны рядом с близкими |
| **Start (MHz)**, **Stop (MHz)** | 100 и 8800, подрезаются по развёртке | в пределах развёртки | полоса частот, по которой строится А-скан |
| **Scans to show (stopped)** | 50 | 1–1000 | сколько последних развёрток показывать и пересчитывать, когда сбор остановлен |
| **Subtract mean A-scan** | выключен | — | вычитает из каждого столбца средний по показанным развёрткам А-скан: неподвижные отражения (горизонтальные полосы) пропадают, остаётся то, что меняется |
Во время сбора изображение показывает последние развёртки в пределах истории: не
больше 50 и не больше ёмкости колец `preprocessed` и `results`. Если истории меньше,
чем задано, в журнале появляется предупреждение
`B-scan is set to show W sweeps but only R are retained; the image shows what history there is.`
Если ось дальности изменилась (другие **Cut (m)**, **Max depth (m)** или полоса),
изображение начинается заново.
Когда сбор остановлен, изменение настройки B-scan пересчитывает всё изображение:
через 0,3 с после последнего изменения программа заново отправляет процессору
сохранённые развёртки (до **Scans to show (stopped)** штук) и собирает картинку
из ответов. Так одно изображение никогда не смешивает разные настройки. Пересчёт
возможен, только если конвейер в этом сеансе запускала эта же программа и процессор
ещё работает. Развёртки, которые процессор не вернул, в картинку не попадают, и в
журнале появляется `B-scan replay recovered R of S re-sent sweeps; the image leaves the others out.`
Если не вернулась ни одна, остаётся прежнее изображение из истории.
Если хотя бы один ВЧ-ключ работает с настоящим железом (`driver_mode: "native"`),
B-scan требует ровно одну пару сбора:
`B-scan with native switches requires exactly one run combo (now N)`.
## GPR
Строит изображение плоскости земли перед радаром когерентной обратной проекцией по
всем выбранным парам и находит на нём объекты. Объекты отображаются на картинке и
уходят клиентам TCP-локатора.
Порядок обработки:
1. Выбор антенн: номера из **Input positions** и **Output positions**, у которых есть
строка в таблице геометрии. Пустое поле — все антенны с геометрией.
2. Вычитание среднего предыдущих коллекций и поправка на движение (раздел «Общие
шаги GPR и 3D SAR»).
3. А-скан каждой пары в полосе **Start (MHz)**–**Stop (MHz)** с окном Ханна.
4. Выравнивание пар по дальности (**PAIR_DEPTH_EQUALIZE**) и нормировка каждой пары
на медиану её А-скана в пределах **Min depth (m)**–**Max depth (m)**; нормировка
выполняется всегда.
5. Сетка 300 × 300 точек на плоскости земли: поперёк — от крайних выбранных антенн
с запасом 2 м в обе стороны, вдоль — дальность от 0,1 м до **Max depth (m)**.
Положение плоскости земли относительно радара задают **Radar height (m)** и
**Look angle (°)**.
6. В каждой точке — сумма вкладов всех пар с весом компенсации дальности (степень
**Range compensation power**, вес не больше 5). Яркость — модуль суммы; точки ближе
**Min depth (m)** остаются нулевыми.
7. Карта сглаживается и нормируется к максимуму, затем на ней ищутся объекты.
Поиск объектов. Программа берёт самый яркий пик и выделяет вокруг него связную
область ярче 0,75 пика. Область меньше 10 см² отбрасывается. Положение объекта —
взвешенный центр яркой части. Затем окрестность объекта гасится, и поиск
повторяется. Он останавливается, когда очередной пик слабее **Object threshold fraction**
от максимума карты, и не идёт дальше 10 шагов. Для каждого объекта считается оценка:
- `peak` — яркость пика;
- `combined` — 0,45 × яркость + 0,25 × когерентность (насколько вклады пар совпадают
по фазе) + 0,20 × превышение над местным фоном + 0,10 × контраст (отношение к
фону). Каждое слагаемое приведено к диапазону 0–1.
Объект считается боковым лепестком более сильного, если он не ярче 0,85 его пика,
сдвинут поперёк не меньше чем на 0,35 м, по дальности — не больше чем на 0,70 м, а
среднеквадратичное по парам расхождение путей волны до обоих объектов не больше 0,2 м.
С включённой **Remove sidelobe objects** такие объекты удаляются из списка и
стираются с карты.
Какие объекты показываются и уходят локатору:
1. Только внутри окна **Visible X min (m)**–**Visible X max (m)** и
**Visible Z min (m)**–**Visible Z max (m)**; то же окно задаёт границы графика.
2. Отбор по числу: если **Max detected objects** (N) и **Draw top M objects** (M)
больше нуля, то при числе объектов больше N не показывается ни один (сцена считается
засорённой), иначе показываются M лучших по оценке.
3. Фильтр приближения: объект показывается, только если он виден в
**Approach min frames** коллекциях подряд и смещается так, как должен смещаться
неподвижный предмет при движении радара к нему: между соседними коллекциями не
больше 2 с, сдвиг поперёк не больше 0,45 м, сдвиг по дальности отличается от
ожидаемого (скорость × cos угла обзора × время) не больше чем на 0,12 м или на
0,85 ожидаемого, смотря что больше. При значении 1 фильтр выключен.
| Параметр | По умолчанию | Пределы | Что делает |
| --- | --- | --- | --- |
| **Tx geometry**, **Rx geometry** | из профиля | — | таблицы положений антенн, см. [Окно программы](desktop.md) |
| **Input positions** | номера с геометрией, которые есть в парах сбора (если таких нет — все с геометрией) | номера через запятую | приёмные антенны в обработке; пустое поле — все с геометрией |
| **Output positions** | так же для выходов | номера через запятую | передающие антенны в обработке; пустое поле — все с геометрией |
| **Min depth (m)** | 2,0 | 0–50 м | ближняя граница дальности изображения и нормировки. Если она не меньше **Max depth (m)**, изображение пустое |
| **Max depth (m)** | 14,0 | 0,1–50 м | дальняя граница сетки и изображения |
| **Range compensation power** | 0,1 | 0–5 | степень компенсации ослабления с расстоянием: вклад умножается на ((R_tx·R_rx) / (R_tx,оп·R_rx,оп))^p, где R_tx,оп и R_rx,оп — расстояния от антенн пары до опорной точки на земле под серединой пары, на дальности 5 м; вес не больше 5. 0 — без компенсации |
| **Object threshold fraction** | 0,7 | 0–1 | поиск объектов останавливается на пике слабее этой доли максимума |
| **Score mode** | `combined` | `peak`, `combined` | как считать оценку объекта (см. выше) |
| **Motion mode** | `int_minus` | `int_minus`, `int_focus` | поправка на движение: полная или только расфокусирующая часть, без сдвига по дальности |
| **Look angle (°)** | 0,0 | от −90 до 90° | наклон радара вниз: задаёт плоскость земли, поправку на движение и фильтр приближения. При ±90° плоскость не определена (`Ground BP is undefined at a look angle of 90 degrees`) |
| **Direction sign** | 1 | −1 или 1 | знак поправки фазы на движение. Подсказка в программе: +1 — более поздние частоты дальше. В 3D SAR не используется |
| **Ignore socket speed** | выключен | — | включена — скорость берётся из поля **Speed (m/s)**; выключена — из клиента локатора, а поле подставляется, пока клиент 5 с не присылал скорость |
| **Speed (m/s)** | 0,0 | от −100 до 100 м/с | скорость радара вдоль земли, положительная — к сцене. 0 — поправки нет. Редактируется при включённой **Ignore socket speed** |
| **Render mode** | `heatmap` | `heatmap`, `objects_only` | вид: карта с объектами или только объекты (без пересчёта) |
| **Max detected objects** | 5 | 0–10000 | N в отборе по числу; 0 — без отбора |
| **Draw top M objects** | 2 | 0–10000 | M в отборе по числу; 0 — без отбора |
| **Approach min frames** | 3 | 1–100 | длина цепочки коллекций для фильтра приближения; 1 — фильтр выключен |
| **Start (MHz)**, **Stop (MHz)** | 3000 и 6000 | в пределах развёртки | полоса частот А-сканов |
| **Radar height (m)** | 1,25 | 0–50 м | высота начала осей радара над землёй |
| **Visible X min (m)**, **Visible X max (m)** | крайние антенны ± 2 м | от −100 до 100 м | окно объектов и графика поперёк; перепутанные min и max программа меняет местами, ширина не меньше 0,1 м |
| **Visible Z min (m)**, **Visible Z max (m)** | 0 и 14 | 0–100 м | окно объектов и графика по дальности; так же не уже 0,1 м |
| **Subtract mean of previous collections** | включён | — | см. «Общие шаги GPR и 3D SAR» |
| **Mean count** | 10 | 0–10000 | сколько предыдущих коллекций усреднять; 0 — не вычитать |
| **PAIR_DEPTH_EQUALIZE** | включён | — | выравнивание уровней пар по дальности |
| **Remove sidelobe objects** | включён | — | удалять объекты, похожие на боковые лепестки |
Изменение **Max detected objects**, **Draw top M objects** и окна видимости
передаётся процессору, только пока выбран режим GPR.
Изображение (**GPR coherent BP — ground plane**): по горизонтали **X (m)**, по
вертикали **Range (m)**. В режиме `heatmap` — цветная карта и объекты белыми ромбами
с оценкой (`0.83`); в режиме `objects_only` — только объекты красными кругами с
подписью `0.83 | x=1.2 | z=5.4`, а к заголовку добавляется `Objects Only`. Передающие
антенны показаны красными треугольниками, приёмные —
синими; они видны, если **Visible Z min (m)** не больше нуля (тогда нижний край
графика немного опускается ниже нуля).
GPR требует хотя бы двух разных пар сбора и таблиц геометрии, а в парах сбора должны
быть все сочетания выбранных входов и выходов: для входов 0, 1 и выходов 0, 1 — пары
`0:0`, `0:1`, `1:0`, `1:1`. Те же требования и сообщения — у 3D SAR. Сообщения при
отказе:
`GPR requires at least 2 distinct run combos (now N)`,
`GPR requires non-empty Tx/Rx geometry in run_config`,
`GPR input positions are missing from geometry config: [...]`,
`GPR output positions are missing from geometry config: [...]`,
`GPR run combos do not cover selected input/output positions: [...]`.
Трассы коллекции должны идти в порядке списка пар сбора, иначе процессор отбрасывает
коллекцию с ошибкой в своём журнале.
На рабочей станции с процессором Intel i7-8700 кадр GPR из 16 пар по 1031 точке
считается примерно за 0,13 с.
## 3D SAR
Считает трёхмерное изображение объёма над землёй перед радаром на видеокарте NVIDIA
(CUDA). Алгоритм и имена настроек взяты из исследовательского ноутбука
`Multi_depthnorm_clean.ipynb` (ячейки 1 и 5); ноутбук хранится вне репозитория.
Порядок обработки:
1. Выбор антенн (**INPUT_IDX**, **OUTPUT_IDX**), вычитание среднего предыдущих
коллекций и поправка на движение (раздел «Общие шаги GPR и 3D SAR»).
2. А-скан каждой пары в полосе **F_START MHz**–**F_STOP MHz** с окном Ханна; у всех
пар коллекции сетка частот должна совпадать.
3. Выравнивание пар (**PAIR_DEPTH_EQUALIZE**) и нормировка на медиану
(**SAR_NORMALIZE_EACH_FRAME**).
4. Апертура: текущая коллекция и до **SAR_FRAME_RADIUS** предыдущих. Антенны
предыдущих коллекций сдвигаются на путь, который радар проехал со скоростью
**SPEED_M_S**; более старые коллекции входят с меньшим весом (от 1 у текущей до 0,8 у
самой старой). Коллекции, которые не удалось обработать, пропускаются.
5. Сетка вокселей с шагами **SAR_VOXEL_M X**, **SAR_VOXEL_M Y**, **SAR_VOXEL_M Z** в осях
сцены: поперёк — от
крайних выбранных антенн с запасом 2 м, по высоте — **SAR_Y_RANGE_M min**–**SAR_Y_RANGE_M max**,
вдоль земли — все точки, расстояние которых по оси обзора лежит в пределах
**MIN_DEPTH**–**MAX_DEPTH**. **SAR_X_SIZE_M** и **SAR_Z_SIZE_M** больше нуля
меняют ширину и длину сетки относительно того же центра.
6. Обратная проекция в каждом вокселе с компенсацией дальности
(**SAR_USE_COMPENSATION**, степень **COMP_RANGE_POWER**, вес не больше 5).
Воксели вне **MIN_DEPTH**–**MAX_DEPTH** по оси обзора остаются нулевыми.
7. Сглаживание по Z и X (гауссово, 1,5 вокселя) и поиск максимума на опорных
плоскостях — тех, что не ближе 0,3 м к верхней и нижней границе высоты.
8. Фон кадра и CLEAN (см. ниже) и проекции.
Объём при настройках по умолчанию и антеннах примера с переключением стимула — около
10,6 млн вокселей (232 × 126 × 364). Объём работы растёт обратно пропорционально
произведению трёх шагов вокселя: если уменьшить все три вдвое, работы станет в восемь
раз больше.
### Фон кадра (BG_GATE_ENABLE)
Фон — медиана объёма по рабочим вокселям (ненулевым, на опорных плоскостях). Отношение
максимума кадра к фону показывает, насколько самый сильный отклик выделяется над
типичным уровнем помех. Пороги 6 и 8,5 — постоянные ноутбука.
Если объектов CLEAN нет, **BG_GATE_ENABLE** управляет яркостью всего кадра: при
отношении меньше 6 кадр не показывается, при 6 его максимум появляется на уровне
**SAR_CLOUD_THRESHOLD**, с 8,5 кадр показывается в полную яркость, а между 6 и 8,5
яркость растёт линейно.
Если CLEAN нашёл объекты, они видны всегда, каждый в своей шкале, при любом отношении
кадра к фону. **BG_GATE_ENABLE** тогда прячет только остаток после вычитания, если его
максимум меньше 6 фонов.
### CLEAN (SAR_CLEAN_ENABLE)
CLEAN разделяет сцену на объекты. На каждом шаге он строит изображение остатка на
отдельной сетке поиска (4 × 10 × 4 см по X, Y и Z, на всю область сцены), находит
точку, которая сильнее всего превышает порог, и вычитает её отклик из А-сканов всех
пар. Порог для вокселя — меньшее из двух: 5,5 медианы положительных вокселей
изображения и 4,5 медианы на той же дальности (по кольцам дальности шириной 0,25 м).
Вычитание идёт, пока ни один воксель не превышает порог, но не больше 15 шагов.
Шаги, точки которых лежат не дальше 0,35 м по X, 0,50 м по Y и 0,40 м по Z от первой
точки группы, объединяются в один объект. Затем каждый объект строится отдельно и
рисуется в своей шкале от 0 до 1 поверх остатка: слабый объект виден рядом с
сильным. Уровень объекта — отношение его пика к фону кадра.
CLEAN работает только на апертуре из одного кадра: всегда при
**SAR_FRAME_RADIUS** = 0, а при большем значении — пока пригодных предыдущих
коллекций нет (сразу после **Start**, после очистки истории). Иначе статус сообщает,
что CLEAN пропущен.
Сетка поиска CLEAN охватывает всю область сцены, даже если **SAR_X_SIZE_M** и
**SAR_Z_SIZE_M** сужают сетку кадра: отражатель сразу за краем кадра вычитается и не
оставляет внутри кадра боковой лепесток, похожий на объект.
### Рамки (SAR_MARK_ENABLE)
С **SAR_MARK_ENABLE** объекты CLEAN, которые не дальше 0,8 м друг от друга поперёк и
0,5 м вдоль земли (в том числе цепочкой), объединяются в группу, и вокруг группы рисуется
рамка. Центр группы — средняя высота её объектов, взвешенная по уровню (учитываются
объекты не слабее 0,4 сильнейшего), верх — верхняя граница их откликов. Классы
проверяются по порядку строк таблицы, рамка получает первый подходящий:
| Цвет | Класс | Условие |
| --- | --- | --- |
| серый пунктир | at the boundary — у края сцены | сильнейший объект группы у края: по оси обзора не дальше 0,25 м от **MAX_DEPTH** или 0,10 м от **MIN_DEPTH**, поперёк не дальше 0,2 м от края сетки или по высоте не дальше 0,4 м от её верха. Отклик может частично лежать за сценой, класс пока не определить |
| красный | very low — очень низко (как мина) | центр не выше 0,15 м и верх не выше 0,45 м |
| оранжевый | on the ground — на земле | центр не выше 0,35 м |
| синий | above the ground — над землёй | остальные |
Легенда рамок — строкой под видами; она видна, когда включены и
**SAR_MARK_ENABLE**, и **SAR_CLEAN_ENABLE**. Без рамок объекты пронумерованы и
подписаны уровнем в фонах: `1: 12 bg`.
### Виды
Четыре вида: **Front (X-Y)**, **Side (Z-Y)**, **Top (X-Z)** — проекции максимума
объёма спереди, сбоку и сверху — и **3D Projection**, наклонный вид. На видах с осью
высоты пунктиром показана земля (Y = 0), на трёх плоских видах — положения выбранных
антенн. Пустой квадрат отмечает максимум; его сторона — наибольшая из ширин отклика
на половине максимума вдоль осей вида (не меньше одного вокселя). Это размер
отклика, а не размер предмета. С объектами CLEAN и при пустом кадре квадрата нет.
**SAR_CLOUD_THRESHOLD** скрывает пиксели слабее заданной доли максимума, не меняя
шкалу. **Zoom (%)** от 100 до 1000 уменьшает окно всех четырёх видов и плавно сдвигает
его центр к самому яркому отклику; при 100% видна вся сцена.
Строка над видами показывает состояние:
| Текст | Когда |
| --- | --- |
| `SAR3D: waiting for measurements` | результатов ещё не было |
| `SAR3D: waiting for projection data` | результат без изображений |
| `SAR3D: ready — CUDA …` | изображение посчитано; разбор строки — ниже |
| `SAR3D: no signal — No signal inside the selected depth range` | в пределах **MIN_DEPTH**–**MAX_DEPTH** нет сигнала |
| `SAR3D: error — …` | ошибка настроек, данных или расчёта; текст ошибки |
| `SAR3D: warning — …` | нет подходящей видеокарты или программа собрана без CUDA |
Строка `ready` складывается из частей через `|`, например:
```text
SAR3D: ready — CUDA | peak 7.2 backgrounds (appears from 6, full scale from 8.5): scale ×0.83 | max X +0.12, Y +0.30, Z 4.85 m | level 0.0412 | zoom 100%
```
| Часть | Что значит |
| --- | --- |
| `CUDA`, `CUDA; CLEAN: …` | расчёт на видеокарте и итог CLEAN: `CLEAN: K objects, each in its own scale (S subtractions, residual maximum R backgrounds)`, `CLEAN: no objects (…)` или `CLEAN skipped: it needs a one-frame aperture, this one has N frames` |
| `peak N backgrounds` | отношение максимума кадра к фону; без объектов CLEAN дальше — состояние порога: `below the appearance level, frame empty`, `scale ×K` (кадр показан с множителем яркости K) или `full scale` |
| `boxes: …` | число рамок каждого класса, например `boxes: 1 very low, 0 on the ground, 2 above the ground` (`at the boundary` — только если такие есть) |
| `max X …, Y …, Z … m` | положение максимума в осях сцены; у X и Y указан знак |
| `level …` | уровень, на который нормированы виды: максимум объёма до нормировки |
| `zoom N%` | масштаб из **Zoom (%)** |
### Настройки 3D SAR
| Параметр | По умолчанию | Пределы | Что делает |
| --- | --- | --- | --- |
| **INPUT_IDX**, **OUTPUT_IDX** | номера с геометрией, которые есть в парах сбора (если таких нет — все с геометрией) | номера через запятую | приёмные и передающие антенны в обработке; пустое поле — все с геометрией |
| **F_START MHz**, **F_STOP MHz** | 1500 и 6000 | в пределах развёртки | полоса частот А-сканов |
| **MIN_DEPTH**, **MAX_DEPTH** | 2,0 и 8,0 | 0–50 и 0,1–50 м | границы расстояния по оси обзора: задают длину сцены вдоль земли и обнуляют воксели вне себя |
| **SAR_VOXEL_M X**, **SAR_VOXEL_M Y**, **SAR_VOXEL_M Z** | 0,025, 0,02, 0,02 | 0,01–0,1 м | шаги сетки. Сглаживание задано в вокселях, поэтому от шагов зависит и оно, а с ним фон кадра и изображения объектов CLEAN; сетка поиска CLEAN от них не зависит |
| **LOOK_ANGLE_DEG** | 19,5 | от −90 до 90° | наклон радара вниз: оси сцены, поправка на движение |
| **Ignore socket speed** | выключен | — | как в GPR |
| **SPEED_M_S** | 0,0 | от −100 до 100 м/с | скорость радара вдоль земли для поправки на движение и сдвига апертуры |
| **SAR_FRAME_RADIUS** | 0 | 0–24 | сколько предыдущих коллекций добавлять в апертуру. 0 — только текущая (нужно разнесение антенн по высоте); больше нуля — нужна точная скорость |
| **BP_GROUND_DEPTH_M** | 1,25 | 0–10 м | высота начала осей радара (строки Y = 0 таблиц антенн) над землёй |
| **COMP_RANGE_POWER** | 0,3 | 0–2 | степень компенсации ослабления с расстоянием |
| **SAR_USE_COMPENSATION** | включён | — | включает компенсацию дальности |
| **SAR_NORMALIZE_EACH_FRAME** | включён | — | нормирует каждую пару на медиану её А-скана в пределах **MIN_DEPTH**–**MAX_DEPTH** |
| **PAIR_DEPTH_EQUALIZE** | включён | — | выравнивание уровней пар по дальности |
| **Subtract mean of previous collections**, **Mean count** | включён, 10 | 0–10000 | вычитание среднего предыдущих коллекций |
| **BG_GATE_ENABLE** | включён | — | фон кадра (см. выше) |
| **SAR_CLEAN_ENABLE** | включён | — | CLEAN (см. выше) |
| **SAR_X_SIZE_M (0 = auto)** | 0 | 0–100 м | ширина сетки кадра поперёк; 0 — от антенн с запасом 2 м |
| **SAR_Y_RANGE_M min**, **SAR_Y_RANGE_M max** | −0,5 и 2,0 | от −50 до 50 м | границы высоты; max должно быть больше min |
| **SAR_Z_SIZE_M (0 = auto)** | 0 | 0–100 м | длина сетки кадра вдоль земли; 0 — по MIN_DEPTH–MAX_DEPTH |
| **SAR_CLOUD_THRESHOLD** | 0,65 | 0–1 | только вид: скрыть пиксели слабее этой доли |
| **Zoom (%)** | 100 | 100–1000 | только вид: приближение к самому яркому отклику |
| **SAR_MARK_ENABLE** | включён | — | только вид: рамки вместо номеров |
Время расчёта на GTX 1060 при сцене по умолчанию: около 0,1–0,2 с на кадр, фон кадра
добавляет около 0,1 с, CLEAN — ещё 0,4–0,5 с в зависимости от числа шагов.
Результат 3D SAR должен поместиться в слот кольца `results`. Если не помещается,
статус сообщает, сколько нужно места, и советует увеличить шаги вокселя, сузить
дальность или увеличить `rings.results.slot_size_bytes`.
### Видеокарта
3D SAR нужна видеокарта NVIDIA с драйвером. Ядра расчёта компилируются при запуске
библиотекой NVRTC под установленную карту. Если при сборке не нашлись заголовки и
библиотеки CUDA, программа собирается без 3D SAR, и в этом режиме над видами висит
предупреждение
`SAR3D requires an NVIDIA GPU with CUDA; this program was built without nvrtc.h, cuda.h, libnvrtc or libcuda. Install CUDA and rebuild.`
Для карты старее, чем умеет установленная NVRTC, предупреждение советует собрать
программу с NVRTC 12 (как это сделать — в главе [Разработка](development.md)).
Остальные режимы видеокарты не требуют.
## Общие шаги GPR и 3D SAR
### Вычитание среднего предыдущих коллекций
Из S21 каждой пары вычитается
комплексное среднее этой пары за до **Mean count** предыдущих коллекций. Остаётся то,
что изменилось: неподвижные отражения и прямая связь антенн гасятся, движущиеся
относительно радара предметы остаются. Процессор хранит столько коллекций, сколько
нужно для среднего и апертуры, независимо от ёмкости колец.
### Поправка на движение
Время каждой частотной точки программа берёт из окна съёма
развёртки: точки идут равномерно от начала окна до конца, а в режиме переключения
стимула стадии одной частоты идут по очереди в порядке номеров выходов. Фаза каждой
точки поворачивается на 4π·f·v·cos(p)·t / c (v — скорость, p — угол обзора, t —
время точки относительно середины коллекции; в GPR ещё со знаком
**Direction sign**), и вся коллекция приводится к одному моменту. При нулевой
скорости поправки нет. В GPR с **Motion mode** = `int_focus` из поправки убирается
её постоянная и линейная по частоте часть — чистый сдвиг по дальности. Скорость берётся из
клиента локатора,
если он присылал её в последние 5 с и флажок **Ignore socket speed** выключен,
иначе — из поля скорости.
### Выравнивание пар по дальности (PAIR_DEPTH_EQUALIZE)
Для каждой пары считается
плавный уровень А-скана по дальности: медиана в окне 1,5 м по блокам 0,10 м. Каждая
пара умножается на положительный коэффициент, зависящий от дальности, так что её
уровень становится средним геометрическим уровней всех пар. Различия между парами
уходят, общий спад с дальностью и фаза сохраняются. Нужно хотя бы две пары.
### Нормировка на медиану
Каждая пара делится на медиану модуля своего А-скана в
пределах границ дальности. В GPR выполняется всегда, в 3D SAR — с
**SAR_NORMALIZE_EACH_FRAME**.
### Компенсация дальности
Отражение слабеет с расстоянием примерно как 1/(R_tx·R_rx). Вклад пары в точку
умножается на ((R_tx·R_rx) / (R_tx,оп·R_rx,оп))^p, где R_tx,оп и R_rx,оп — расстояния от
антенн пары до опорной точки на земле под серединой пары, на дальности 5 м; вес
ограничен пятью.
## Геометрия
Антенны задаются в осях радара (X, Y, Z), изображения 3D SAR — в осях сцены (X, Y —
высота над землёй, Z — вдоль земли); см. рисунок в главе
[Как работает радар](principles.md). Ось X у обеих систем общая. Переход из осей
сцены в оси радара при высоте h и угле обзора p:
Y_радар = (Y − h)·cos p + Z·sin p,
Z_радар = −(Y − h)·sin p + Z·cos p.
Плоскость земли (Y = 0) в осях радара: Y_радар = (−h + Z_радар·sin p) / cos p. На ней
лежит сетка GPR; при p = ±90° плоскость не определена, и программа отказывает
(`Ground BP is undefined at a look angle of 90 degrees`).
«Глубина» в разных режимах означает разное:
| Где | Что означает |
| --- | --- |
| B-scan, **Cut (m)** и **Max depth (m)** | половина пути волны, отсчитанная от **Cut (m)** |
| GPR, **Min depth (m)** и **Max depth (m)** | координата Z_радар точки на плоскости земли |
| 3D SAR, **MIN_DEPTH** и **MAX_DEPTH** | координата Z_радар вокселя |
| Нормировка А-скана | половина пути волны |
Волна в GPR и 3D SAR считается идущей со скоростью 3·10⁸ м/с (воздух), в B-scan —
299 792 458 м/с.
+90
View File
@@ -0,0 +1,90 @@
# Неполадки
Сначала посмотрите журнал окна (секция **Runtime Log**) и строку состояния. Если там
пусто, а картинка не обновляется, причина обычно в журналах процессов в
`python_app/runtime/logs/`:
| Файл | Что в нём искать |
| --- | --- |
| `acquisition.err.log` | приборы не открываются, ожидание приборов, ошибки коллектора E-502 |
| `data_preprocessor.err.log` | отброшенные коллекции: не та сетка частот, нет пары в наборе |
| `data_processor.err.log` | ошибки обработки, отклонённый live-файл, 3D SAR, локатор |
| `radar.log` | всё, что было в окне журнала |
У каждого процесса есть ещё `<процесс>.out.log` (обычный вывод), а перед каждым
запуском прежние журналы переименовываются в `*.prev`: после автоматического
перезапуска причина падения лежит в `<процесс>.err.log.prev`. Без экрана журнал
программы — `journalctl -u radar.service -f`.
## Программа не запускается
| Признак | Причина | Что делать |
| --- | --- | --- |
| окно не открывается, в выводе ошибка о LibreVNA (`No compatible LibreVNA USB device found…`) | профиль с `driver_mode: "native"`, а прибор не подключён: при запуске окно читает пределы прибора | подключите прибор или запустите с профилем симулятора: `./start.sh --profile run_config.json` |
| окно открывается с ошибкой `Cannot switch processing mode to …` и закрывается, и так при каждом запуске | запомненный профиль читается, но его настройки нельзя применить (например, `SAR_Y_RANGE_M max` не больше min) | удалите `python_app/runtime/gui_session_state.json` и исправьте профиль |
| `Another radar instance already holds /tmp/radar_system.lock; aborting.` | программа уже запущена: в другом окне или службой | закройте другой экземпляр или `sudo systemctl stop radar.service` |
| служба перезапускается каждые 3 с | без экрана не удаётся **Start** (часто — не выбраны наборы) или нет программ в `build/bin/` | `journalctl -u radar.service`; выберите наборы и сохраните профиль, запустив окно на машине |
| `[start.sh] … Run an interactive './start.sh' once (online)…` | без экрана не хватает Python-пакетов | запустите `./start.sh` один раз с экраном и сетью |
## Start отказывает
| Сообщение | Причина | Что делать |
| --- | --- | --- |
| `Select all required preprocess sets in Preprocessing Panel before Start: …` | не выбраны калибровка или референс | выберите или снимите их в **Preprocessing…**; для симулятора — **Create Neutral S21 Sets** |
| наборы пропали из списков, в журнале `Preprocess selection reset: …` | изменилась развёртка, серийные номера или положения ключей — другой ключ радара | верните прежние настройки или снимите наборы заново |
| `Selected … set does not cover requested run combos` | набор снят для другой матрицы | снимите набор заново при текущих ключах |
| `rings.<имя>.slot_size_bytes must be at least N for this sweep` | слот кольца меньше коллекции | увеличьте `slot_size_bytes` в профиле |
| `B-scan with native switches requires exactly one run combo (now N)` | B-scan с настоящими ключами показывает одну пару | задайте одну пару в **Single combo** |
| `GPR requires at least 2 distinct run combos (now N)` и другие `GPR …` | для GPR и 3D SAR нужны пары и геометрия | проверьте пары сбора и таблицы геометрии |
| `Object calibration … differs from the captured state` | калибровка по объекту снята при других настройках, геометрии или наборе калибровки | выберите **Off** или снимите калибровку по объекту заново |
| `Stop all pipeline processes before loading a config profile` | профиль загружают при работающем сборе | нажмите **Stop** |
## Нет данных от приборов
| Признак | Причина | Что делать |
| --- | --- | --- |
| сбор идёт, приборы не опрашиваются, на графиках один движущийся точечный отражатель | профиль симулятора: `driver_mode: "mock"` (например, корневой `run_config.json`) | загрузите профиль с `driver_mode: "native"`; если в журнале `Failed to load the last selected config profile…`, запомненный профиль не прочитался — исправьте его и загрузите снова |
| `No datapoints received from at least one device; hardware trigger sync did not start. Check Trigger Out/In loop and 10 MHz reference wiring.` | многоприборная схема: не работает петля триггера | проверьте кабели петли и контроллер STM32 в режиме переключения стимула |
| `Packet not received from <серийный номер> within 1.0 s` | включена внешняя опора (`force_external_reference: true`), а 10 МГц не подана | выключите опору в профиле или проверьте, что 10 МГц доходит до всех приборов |
| `<прибор> unavailable (attempt N); retrying until present` | прибор не открывается; процесс сбора ждёт его бесконечно | проверьте USB и серийный номер в профиле; правило udev ставит `./start.sh` при обычном запуске |
| в режиме переключения стимула приборы падают с `LIBUSB_ERROR_IO` | полоса ПЧ 1 кГц | используйте 5–50 кГц, рабочее значение — 50 кГц |
| ключи не переключаются, ошибок нет | неверный GPIO-чип или ключ в `mock` | проверьте `gpiodetect`, `gpioinfo -c gpiochip0` и `driver_mode` ключей |
| `Failed to open GPIO chip '…'` или `Failed to request GPIO lines…` | чипа нет или линии заняты другой программой | проверьте чип и что линии свободны (`gpioinfo`) |
## Радиофотонный радар
| Сообщение | Причина | Что делать |
| --- | --- | --- |
| `…sweep rejected (count=n): covered [a, b] Hz does not span band [c, d] Hz…` | развёртка лазера не покрывает выходную полосу | проверьте фазовую калибровку (`rph_radar_calibrate`) и `radar.rph_radar.band` |
| `Timed out waiting for a RadioPhotonic radar (RPH) sweep covering the configured band` | за **Read timeout (s)** не пришло ни одной пригодной развёртки | проверьте, что лазер запущен (**Apply Radar**), и сигналы на входах |
| `Cannot load libx502api.so: …` | нет библиотек L-Card | положите `libx502api.so` и `libe502api.so` в `~/.local/lib` или задайте `LD_LIBRARY_PATH` в **Environment JSON** |
| `Timed out waiting for RadioPhotonic radar (RPH) TTY …` | коллектор не запустился | смотрите строки `Collector: …` в `acquisition.err.log`; проверьте **Executable path** (`make rph_radar_collector`) |
| `ADC source clock leads host monotonic clock; restart acquisition` | часы АЦП ушли вперёд часов компьютера | перезапустите сбор |
| `Serial port '…' is busy — another program is already using the laser controller…` | порт платы лазеров занят другой программой | закройте её |
| `Device rejected command: STATE 0x…` | плата лазеров отвергла команду | проверьте значения токов и температур и питание платы |
| `RFG protocol violation: …` | на порт пришёл чужой пакет | привяжите `udp_bind` к проводному адресу, на котором работает плата |
## Картинка не обновляется или пустая
| Признак | Причина | Что делать |
| --- | --- | --- |
| сбор идёт, счётчик `History` растёт только у `raw` | препроцессор отбрасывает коллекции | `data_preprocessor.err.log`: обычно не совпадает сетка частот набора; снимите наборы заново |
| GPR пустой | нет антенн с геометрией среди выбранных или **Min depth (m)** не меньше **Max depth (m)** | проверьте поля и таблицы; ошибки — в `data_processor.err.log` |
| **Single Capture** в режиме GPR ждёт 300 с и останавливается | результат GPR пустой, и съём его не дождался | проверьте настройки GPR, как выше |
| `SAR3D: warning — …` | нет видеокарты NVIDIA или программа собрана без CUDA | глава [Установка и запуск](installation.md), раздел о видеокарте |
| `SAR3D: error — the views need N KiB but a results ring slot holds M KiB…` | изображения 3D SAR не помещаются в слот | увеличьте шаги вокселя, сузьте дальность или увеличьте `rings.results.slot_size_bytes` |
| `SAR3D: no signal — No signal inside the selected depth range` | в пределах MIN_DEPTH–MAX_DEPTH нет сигнала | проверьте границы дальности и полосу |
| кадр 3D SAR пустой, в строке `frame empty` | максимум ниже 6 фонов при включённой **BG_GATE_ENABLE** | это норма для пустой сцены; для проверки выключите **BG_GATE_ENABLE** |
| `Stopping pipeline after 400 consecutive reader poll failures` | конвейер остановлен из-за ошибок чтения колец | нажмите **Start**; повторяется — проверьте `/dev/shm` |
| `Status: result display error (acquisition continues)` | ошибка отрисовки; сбор продолжается | текст ошибки — в журнале |
## Прочее
| Признак | Причина | Что делать |
| --- | --- | --- |
| после перезапуска пропали настройки и выбор наборов | изменения в окне и вебе не пишутся в профиль сами | **Save Config…** |
| отказ в доступе к кольцам (`Permission denied`) | программу или тесты запускали от root, и кольца в `/dev/shm` принадлежат root | при закрытой программе удалите их: `sudo rm -f /dev/shm/radar_*` |
| веб-страница не открывается | порт занят или не тот адрес | проверьте вывод программы (`journalctl`) на `address already in use`; другой порт — `RADAR_SYSTEM_WEBUI_PORT` |
| `Disk recording stopped because the writer could not keep up` | диск не успевает за записью | пишите на более быстрый диск или меньше коллекций; записанное сохранено |
| `Snapshot directory already exists: …` | каталог набора данных уже есть | задайте другое **Name** |
| `Log panel overflow: N record(s) dropped…` | журнал в окне переполнился | всё есть в `radar.log` |
+226
View File
@@ -0,0 +1,226 @@
# Веб-интерфейс, кнопка и локатор
Кроме окна, с программой работают из браузера, GPIO-кнопкой и через TCP-локатор: он
отдаёт внешним программам найденные объекты и принимает скорость радара. Без экрана
(раздел «Работа без экрана») других способов нет.
## Веб-интерфейс
Веб-сервер работает внутри программы, и с окном, и без экрана. Откройте в браузере
`http://<адрес машины>:8080/`. Порт меняется переменной окружения
`RADAR_SYSTEM_WEBUI_PORT`; неверное значение заменяется на 8080. Если порт занят,
программа работает без веб-интерфейса, а ошибка (`address already in use`) видна
только в выводе процесса. Если сервер не смог запуститься по другой причине, в журнале
появляется `Failed to start web UI`.
> **Внимание.** У веб-интерфейса нет пароля и шифрования: любой, кто достаёт до
> порта по сети, управляет радаром. По адресам `/docs`, `/redoc` и `/openapi.json`
> открыто описание API. Подключайте машину только к доверенной сети.
### Страница
Верхняя строка — кнопки управления: **Start**, **Single Capture**, **Stop**,
**Capture Temporary Reference**, **Remove Last Measurement**. Они делают то же, что
одноимённые кнопки окна (глава [Окно программы](desktop.md)).
**Processor Output** — картинка с графиков окна программы. Масштабировать её в
браузере нельзя: она повторяет то, что показано в окне. В режиме B-scan над ней
список пар (**Input i / Output o**) и кнопка **Next Combination**, в режиме 3D SAR —
строка состояния 3D SAR.
Боковая панель:
| Блок | Элемент | Что делает |
| --- | --- | --- |
| **Config Profile** | список и **Load** | загружает профиль из каталога `run_configs/`, как **Load Config…**; доступна при остановленном сборе. Список читается при открытии страницы; новые файлы видны после её перезагрузки |
| | `current: <файл>` | текущий профиль |
| **Save / Record Dataset** | **Save path**, **Name (optional)** | каталог и имя набора данных; при открытии страницы берутся из окна, а сохранение и запись из браузера переписывают ими **Path** и **Name** окна (запись — и **Sweeps to record**) |
| | **Save Last 1000** | сохраняет до 1000 последних коллекций каждой стадии, как одноимённая кнопка окна |
| | **Sweeps to record** и **Start + Record to Disk** | запускает сбор и записывает на диск заданное число следующих коллекций; ниже — ход записи `recording N / M sweep(s)…` |
| **Processor Settings** | форма | настройки текущего режима обработки (ниже); заголовок сворачивает форму |
| | таблицы **Tx geometry**, **Rx geometry** | геометрия антенн в режимах GPR и 3D SAR: **−** удаляет антенну, **Add Antenna** добавляет следующую свободную |
| | **Apply** | отправляет изменённые поля |
| | **Clear Runtime History** | очищает историю, как одноимённая кнопка окна |
Нижняя строка: `running` — идёт ли сбор, `processor` — работает ли процессор,
`ring` — имя кольца результатов, `scans` — сколько коллекций в истории каждой стадии,
и признак картинки: `live` — кадры приходят, `stale` — новых кадров нет дольше 2 с
(так бывает и тогда, когда картинка просто не меняется), `no data` — кадров ещё не
было. Сообщения об успехе и ошибках всплывают на 2,6 с; ошибки — красным.
Если окно браузера уже 760 пикселей, панель уходит под картинку.
### Как выполняются действия
Каждая кнопка вызывает тот же код, что и кнопка окна, с теми же проверками. Действия
из нескольких браузеров выполняются по очереди. Если окно программы не начало
действие за 30 с, запрос отменяется
(`The desktop did not start the action in time; the request was cancelled`). Если
начало, но не закончило, браузер получает
`The desktop action is still running; check its status before retrying`, а действие
доводится до конца. Ошибки, которые возникают позже (например, на середине
однократного съёма), видны только в журнале.
### Форма настроек
Форма строится из полей окна: те же подписи, пределы и группы. В ней поле **Mode** и
поля текущего режима; смена режима сразу перестраивает форму. **Apply** отправляет
только изменённые поля, поэтому два человека, правящие разные поля, не мешают друг
другу. Порядок полей может отличаться от окна. Значения проверяются так же, как в
окне: вне пределов — `Numeric setting is outside its allowed range`, неизвестный
вариант — `Unknown setting option: …`; при ошибке не применяется ни одно поле из
отправленных. Дробное число округляется до точности поля окна. Изменения из окна
приходят в форму раз в секунду.
Только в окне программы:
- **Apply Radar**, **Load Config…** из произвольного места, **Save Config…**;
- окно **Preprocessing…**: съём и выбор наборов, калибровка по объекту;
- **Save S21 JSON**;
- секции **Radar** и **Switches**: развёртка, лазер, ключи, пары сбора;
- **Show magnitude**, **Show phase**, **Unwrap phase**, **Scans to show (stopped)**;
- журнал и его уровень, строка состояния;
- масштаб и сдвиг графиков мышью.
### Картинка и обновление
Программа снимает картинку графиков, только когда подключён хотя бы один браузер и
изображение изменилось, и не чаще 5 раз в секунду. Каждый браузер получает самую
свежую картинку, состояние и настройки; медленное соединение пропускает
промежуточные кадры. Пока окно занято синхронным съёмом (калибровка, референс),
картинка не обновляется. Браузер переподключается сам через 1,5 с после обрыва.
### API
Страница работает через HTTP API, которым можно пользоваться и из своих программ.
Тело запросов и ответов — JSON; ошибка действия — код 400 и
`{"detail": "<сообщение>"}`. Тело неверного вида (поле не того типа, нет обязательного
поля) даёт код 422 со списком ошибок в `detail`.
| Запрос | Тело | Что делает |
| --- | --- | --- |
| `GET /api/status` | — | состояние (то же, что в нижней строке страницы) |
| `POST /api/start` | — | **Start** |
| `POST /api/single_capture` | — | **Single Capture** |
| `POST /api/stop` | — | **Stop** |
| `POST /api/tmp_reference` | — | **Capture Temporary Reference** |
| `POST /api/remove_last` | — | **Remove Last Measurement** |
| `POST /api/clear_history` | — | **Clear Runtime History** |
| `GET /api/configs` | — | список профилей в `run_configs/` |
| `POST /api/load_config` | `{"name": "<файл>.json"}` | загрузить профиль |
| `POST /api/save_dataset` | `{"path": "…", "name": "…"}` | **Save Last 1000** |
| `POST /api/start_recording` | `{"path": "…", "name": "…", "count": N}` | **Start + Record to Disk**, N от 1 до 1 000 000 |
| `GET /api/live_settings` | — | описание полей формы |
| `POST /api/live_settings` | `{"<поле>": значение, …}` | применить настройки. Имена полей — из описания формы; у настроек обработки они совпадают с ключами `processing_live.json` |
| `POST /api/display_combo` | `{"input_pos": i, "output_pos": o}` | выбрать пару B-scan |
| `POST /api/next_combo` | — | **Next Combination** |
| `WS /ws` | — | поток сообщений `frame` (картинка PNG в base64), `status`, `settings` |
## GPIO-кнопка
Кнопка, подключённая к GPIO компьютера, снимает временный референс — то же, что
**Capture Temporary Reference**. Она настраивается в разделе `control_button`
профиля (глава [Файл конфигурации](configuration.md)) и читается один раз при запуске
программы.
Схема по умолчанию: кнопка между линией GPIO и землёй, внутренняя подтяжка к питанию,
нажатие — спад уровня, по одному действию на нажатие. Индикации на самой кнопке нет;
результат виден в журнале (`GPIO control button pressed: capturing tmp reference`, затем
`Tmp S21 Reference captured: …`) и на графике pass-through.
Нажатие, пришедшее, пока программа останавливает сбор перед съёмом, отбрасывается
(`Tmp reference capture already in progress; ignoring duplicate request`). Нажатие во
время самого съёма ждёт его конца и запускает ещё один съём. Ошибки съёма только
записываются в журнал, окон с ошибкой кнопка не
открывает. Если настройки неполные, кнопка не включается
(`GPIO control button configuration is incomplete or unsupported; watcher not started`).
Если линию не удалось открыть — `Failed to start GPIO control button watcher`; если
наблюдение за линией прервалось — `GPIO control button watcher stopped: …`. В обоих
случаях программа работает дальше без кнопки до перезапуска.
## TCP-локатор
Локатор — TCP-сервер внутри процессора. Он отдаёт подключившимся программам объекты,
найденные режимом GPR, и принимает от них скорость радара для поправки на движение.
Настройки — раздел `run.locator_server` профиля; по умолчанию порт 8888 на всех
интерфейсах. Сервер работает, пока работает процессор: от первого **Start** до
закрытия программы или перезапуска процессора. **Stop**, временный референс и
пересчёт B-scan процессор и локатор не останавливают.
Обмен в обе стороны идёт пакетами: заголовок из 8 байт — номер устройства и длина
данных (оба uint32, младший байт первым), затем текст JSON в UTF-8.
Пакет от радара:
```text
03 00 00 00 71 00 00 00
{"obs":[{"crs":0.3,"dst":4.3},{"crs":-1.0,"dst":6.0},{"crs":1.0,"dst":2.0}],"sts":1,"tim":"13:42:25.058","ver":1}
```
| Поле | Что это |
| --- | --- |
| `obs` | объекты: `crs` — координата X (вдоль ряда антенн, поперёк направления обзора), `dst` — дальность (Z), метры, округлены до 0,1 м |
| `sts` | всегда 1 |
| `tim` | местное время сборки пакета `ЧЧ:ММ:СС.ммм` (не время съёма) |
| `ver` | `run.locator_server.protocol_version` |
В заголовке — `run.locator_server.device_id`. Пакет уходит после обработки каждой
новой коллекции в любом режиме; вне режима GPR и без объектов список `obs` пуст.
Объекты — те же, что видны на графике GPR: внутри окна видимости, после отбора по
числу и фильтра приближения. При пересчёте после смены настроек уходит пакет только
для новейшей коллекции. Новый клиент сразу получает последний отправленный пакет.
Пакет к радару — скорость в м/с:
```text
00 00 00 00 0c 00 00 00
{"vlc":1.25}
```
Остальные поля и номер устройства в заголовке не читаются. Скорость считается свежей
5 с после прихода. Пока она свежая и на странице GPR или 3D SAR выключена
**Ignore socket speed**, она заменяет скорость из поля, а её изменение пересчитывает
новейшую коллекцию в GPR и 3D SAR. Использованная скорость сохраняется в каждом
результате.
Клиентов может быть несколько; у каждого очередь на `client_queue_size` пакетов (по
умолчанию 32), при переполнении выбрасываются старые. Клиент, который 5 с не принимает
данные или прислал пакет больше `max_payload_bytes` (по умолчанию 65 536 байт),
отключается. Сообщения локатора пишутся в
`python_app/runtime/logs/data_processor.err.log`; если порт занят, процессор работает
без локатора и пишет туда же `locator server disabled`.
## Работа без экрана
На машине без оператора (Raspberry Pi) программу запускает служба systemd
`radar.service` (установка — в главе [Установка и запуск](installation.md)). Она
запускает `./start.sh --headless --skip-build`: окно не показывается, через 0,5 с
выполняется **Apply Radar**, ещё через 1 с — **Start**, даже если **Apply Radar** не
удался. Если **Start** не удался, программа завершается, и systemd запускает её
снова через 3 с.
Профиль служба берёт последний использованный (из
`python_app/runtime/gui_session_state.json`), а если его нет — корневой
`run_config.json`, то есть симулятор. Поэтому перед установкой службы загрузите или
сохраните на машине нужный профиль. Если запомненный профиль не читается, программа
пишет `Failed to load the last selected config profile; falling back to root run_config.json.`,
берёт корневой профиль и запоминает уже его: исправленный профиль нужно загрузить снова.
| Канал | Для чего |
| --- | --- |
| браузер, `http://<машина>:8080/` | запуск и остановка, временный референс, сохранение данных, профили из `run_configs/`, настройки обработки |
| GPIO-кнопка | временный референс |
| TCP-локатор, порт 8888 | объекты и скорость |
| `sudo systemctl start` / `stop` / `restart radar.service` | управление службой |
| `journalctl -u radar.service -f` | журнал службы |
Без экрана нет окон с ошибками: всё пишется в журнал. Окно калибровки и
**Save Config…** без экрана недоступны: калибровку снимайте, запустив окно программы
на той же машине. При запуске окна `./start.sh` сначала останавливает службу; после
закрытия окна она сама не запускается.
Если процесс конвейера упал, программа перезапускает конвейер: первый раз сразу,
следующие подряд — не раньше чем через 6, 12, 24, 48, затем 60 с после предыдущей
попытки; когда данные снова пошли, счёт начинается заново. После 400 подряд ошибок чтения
колец конвейер останавливается и сам уже
не запускается: нажмите **Start** в браузере.
+111
View File
@@ -0,0 +1,111 @@
# Типовые сценарии
## Подготовка к измерениям
1. Подключите приборы, ВЧ-ключи и антенны (глава [Оборудование](hardware.md)).
2. Запустите программу с профилем установки:
`./start.sh --profile <профиль>`. Проверьте в секции **Radar** развёртку, а под
полями — пределы прибора: если они прочитаны, прибор на связи.
3. Нажмите **Apply Radar**.
4. Убедитесь, что в **Selected Preprocess Sets** выбраны наборы, снятые при этой
развёртке. Если списки пусты — снимите калибровку и референс (ниже).
5. Выберите режим pass-through (**Mode**), нажмите **Single Capture** и посмотрите
S21 каждой пары: у всех пар должен быть сигнал, без пар на уровне шума.
**Single Capture**, как и **Start**, требует выбранных наборов; при первой настройке
сигнал каждой пары виден на графиках окна предобработки во время съёма калибровки.
## Калибровка
Калибровку (through) снимают при первой настройке установки, после замены кабелей,
ключей или антенн и после смены развёртки: другая развёртка — другой ключ радара, и
старые наборы для неё не видны.
1. Нажмите **Preprocessing…**, задайте **Set name** и
**Sweeps per combo (median)**.
2. Подготовьте эталонный тракт для калибровки и нажмите **Start S21 Calibration**.
3. Снимайте пары (**Capture Current Combo** или **Capture All Remaining**), проверяя
каждую развёртку на графике; неудачную переснимите через **Undo Last Capture**.
4. Нажмите **Save Captured Set**.
Подробно — в главе [Калибровка и предобработка](preprocessing.md).
## Референс
Референс — пустая сцена: то, что должно остаться фоном.
1. Уберите из сцены цели, оставив всё постоянное на своих местах.
2. В окне **Preprocessing Setup** задайте новое **Set name** и нажмите
**Start S21 Reference**, затем снимите пары и сохраните набор.
На месте работы, когда сцена изменилась (новое место, переставили радар), снимите
временный референс: кнопка **Capture Temporary Reference**, та же кнопка в
веб-интерфейсе или GPIO-кнопка. Сбор приостанавливается на время съёма и
продолжается уже с новым референсом.
## Наблюдение
1. Выберите режим обработки в **Mode**: B-scan — дальность во времени для одной
пары, GPR — карта земли с объектами, 3D SAR — объём над землёй.
2. Нажмите **Start**.
3. Настраивайте режим на ходу: изменения применяются сразу и пересчитывают последний
результат (глава [Режимы обработки](processing.md)). Для GPR и 3D SAR задайте
высоту радара и угол обзора, как на установке.
4. Если радар движется, скорость передаёт клиент локатора; без него включите
**Ignore socket speed** и задайте скорость вручную.
5. Нажмите **Save Config…**, когда настройки подобраны.
## Сохранение данных
- Сохранить то, что уже есть в памяти (до 1000 коллекций каждой стадии): задайте
**Path** и **Name**, нажмите **Save Last 1000**.
- Записать следующие N коллекций: задайте **Sweeps to record**, **Path** и **Name**,
нажмите **Start + Record to Disk**. Ход записи виден в журнале и в веб-интерфейсе.
- Нужны развёртки S21 в JSON для других программ — **Save S21 JSON**.
Набор данных — новый каталог в **Path**; формат — в главе
[Данные на диске](data.md).
Если последняя коллекция испорчена (например, в кадр попал человек), нажмите
**Remove Last Measurement**: она удалится из истории в памяти. Уже записанные на диск
файлы останутся, а идущая запись на диск при этом завершится.
## Калибровка по объекту
Нужна, когда изображение GPR или 3D SAR размыто из-за того, что пары не совпадают по
фазе.
1. Поставьте металлический шар в сцену и измерьте положение его центра.
2. Нажмите **Preprocessing…**, затем **Create…** рядом с **Object calibration**.
3. Нажмите **Capture Data** и впишите X, Y, Z центра шара. Повторите для других
положений, если нужно.
4. Нажмите **Calculate and Save** и проверьте карты фаз **Before** и **After**.
5. Перезапустите сбор (**Stop**, затем **Start**): поправка применяется со следующего
запуска.
Подробно — в главе [Калибровка и предобработка](preprocessing.md).
## Работа без оператора
1. Настройте всё на самой машине с экраном: профиль, наборы, режим, и нажмите
**Save Config…**.
2. Установите службу: `sudo bash deploy/install-daemon.sh`, затем
`sudo systemctl start radar.service`.
3. Управляйте через браузер (`http://<машина>:8080/`), GPIO-кнопку и локатор.
Глава [Веб-интерфейс, кнопка и локатор](web.md), раздел «Работа без экрана».
## Обновление программы
1. На рабочей станции закоммитьте изменения и выполните `deploy/pack-usb.sh` (путь к
флешке можно не указывать, если подключена одна).
2. На машине закройте окно программы и из корня флешки выполните
`bash radar_update/update.sh`.
3. Проверьте `cat ~/Documents/radar_system/RELEASE`.
Глава [Установка и запуск](installation.md), раздел «Обновление с флешки».
## Окончание работы
1. Нажмите **Stop**.
2. Нажмите **Save Config…**, если меняли настройки или наборы.
3. Закройте окно: запись на диск дописывается, процессы конвейера останавливаются.
+80
View File
@@ -0,0 +1,80 @@
"""The user documentation names every profile key, live key and on-screen label, and its links resolve."""
import dataclasses
import re
import unittest
from pathlib import Path
from typing import Any, Iterator
from python_app.models.gui_profile_schema import GuiProfileModel
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
ROOT = Path(__file__).resolve().parents[2]
DOCS = ROOT / "docs"
GUI = ROOT / "python_app" / "gui"
# A full profile: every model section, both switches, geometry and the whole gui section.
PROFILE = ROOT / "run_config_examples" / "run_config_librevna_multi_per_point.example.json"
# Text the widgets show: constructor captions, and ("Label", widget) form rows of the sections and dialogs.
CAPTION = re.compile(r'\b(?:QPushButton|QCheckBox|QGroupBox|QLabel|QToolButton|addRow|setWindowTitle|setTitle)\(\s*"([^"%{}]{2,})"')
FORM_ROW = re.compile(r'\(\s*"([^"%{}]{1,60})"\s*,\s*[a-z_][a-z_0-9]*\s*\)')
LINK = re.compile(r"\]\(([^)#\s]+)(?:#[^)]*)?\)")
CODE = re.compile(r"(`+)(.+?)\1")
BOLD = re.compile(r"\*\*(.+?)\*\*")
HEADING = re.compile(r"^#+\s+(.+)$", re.MULTILINE)
def leaf_names(value: Any) -> Iterator[str]:
if isinstance(value, dict):
for key, item in value.items():
if isinstance(item, (dict, list)) and item:
yield from leaf_names(item)
else:
yield key
elif isinstance(value, list) and isinstance(value[0], dict):
yield from leaf_names(value[0])
def on_screen_labels() -> set[str]:
labels: set[str] = set()
for path in GUI.rglob("*.py"):
source = path.read_text(encoding="utf-8")
labels.update(CAPTION.findall(source))
if path.parent.name == "sections" or path.name.endswith(("_dialog.py", "_editor.py")):
labels.update(FORM_ROW.findall(source))
return labels
class DocumentationTest(unittest.TestCase):
def setUp(self) -> None:
self.chapters = {path: path.read_text(encoding="utf-8") for path in [ROOT / "README.md", *DOCS.glob("*.md")]}
configuration = self.chapters[DOCS / "configuration.md"]
self.configuration_names = {part for _, span in CODE.findall(configuration)
for part in re.split(r"[.,*\s\[\]{}\"]+", span) if part}
def test_configuration_names_every_profile_and_live_key(self) -> None:
profile = GuiProfileModel.load_from_path(PROFILE).to_dict()
live = [field.name for field in dataclasses.fields(ProcessingLiveConfig)]
missing = sorted(set(leaf_names(profile)).union(live) - self.configuration_names)
self.assertEqual(missing, [], "keys missing from docs/configuration.md")
def test_every_on_screen_label_is_documented(self) -> None:
# A label counts only as a whole bold span, code span or heading; a hint may continue with "…".
text = "\n".join(self.chapters.values())
spans = {*BOLD.findall(text), *(span.strip() for _, span in CODE.findall(text)), *HEADING.findall(text)}
def documented(label: str) -> bool:
label = label.strip()
return label in spans or any(span.startswith(label) and span[len(label):].lstrip().startswith("…")
for span in spans)
self.assertEqual(sorted(label for label in on_screen_labels() if not documented(label)), [])
def test_relative_links_resolve(self) -> None:
broken = [f"{path.name}: {target}" for path, text in self.chapters.items()
for target in LINK.findall(text)
if "://" not in target and not (path.parent / target).exists()]
self.assertEqual(broken, [])
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -340,7 +340,7 @@ btnApply.addEventListener("click", async () => {
btnResetHistory.addEventListener("click", async () => { btnResetHistory.addEventListener("click", async () => {
btnResetHistory.disabled = true; btnResetHistory.disabled = true;
try { try {
await api("/api/clear_history"); await api("/api/clear_history", {});
settingsNote.textContent = "History reset requested."; settingsNote.textContent = "History reset requested.";
} catch (err) { } catch (err) {
settingsNote.textContent = err.message; settingsNote.textContent = err.message;