From ad2b3e555a8848e5e9c072c96fe35895cb378d04 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Fri, 16 Oct 2026 18:35:02 +0300 Subject: [PATCH] added docs --- .gitignore | 1 - README.md | 647 ++---------------- .../rph_radar_collector/README.md | 169 ++--- docs/architecture.md | 203 ++++++ docs/configuration.md | 428 ++++++++++++ docs/data.md | 225 ++++++ docs/desktop.md | 341 +++++++++ docs/development.md | 160 +++++ docs/glossary.md | 64 ++ docs/hardware.md | 382 +++++++++++ docs/installation.md | 238 +++++++ docs/preprocessing.md | 300 ++++++++ docs/principles.md | 240 +++++++ docs/processing.md | 475 +++++++++++++ docs/troubleshooting.md | 90 +++ docs/web.md | 226 ++++++ docs/workflow.md | 111 +++ python_app/tests/test_documentation.py | 80 +++ python_app/webui/static/app.js | 2 +- 19 files changed, 3719 insertions(+), 663 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/data.md create mode 100644 docs/desktop.md create mode 100644 docs/development.md create mode 100644 docs/glossary.md create mode 100644 docs/hardware.md create mode 100644 docs/installation.md create mode 100644 docs/preprocessing.md create mode 100644 docs/principles.md create mode 100644 docs/processing.md create mode 100644 docs/troubleshooting.md create mode 100644 docs/web.md create mode 100644 docs/workflow.md create mode 100644 python_app/tests/test_documentation.py diff --git a/.gitignore b/.gitignore index 1a911db..4685461 100644 --- a/.gitignore +++ b/.gitignore @@ -252,7 +252,6 @@ AGENTS.md .claude/ .graphifyignore pyrightconfig.json -/docs/ test_end_2/ # --- device_firmware: PlatformIO / STM32G431 cart remote --- diff --git a/README.md b/README.md index d7c47d5..1c840fe 100644 --- a/README.md +++ b/README.md @@ -1,599 +1,84 @@ # Radar System -A real-time radar acquisition and imaging system for vector-network-analyzer (VNA) -and ADC front-ends. It drives the radar through a frequency sweep, calibrates and -preprocesses the measured S21 traces, runs a selectable imaging/detection -processor, and presents the live result in a desktop GUI and an in-process web -interface. The same application runs interactively on a workstation or unattended -as a headless Raspberry Pi appliance. +Программа радиолокационной системы. Она управляет радаром, снимает развёртки +коэффициента передачи S21 по всем парам антенн, калибрует их, строит изображения сцены +и показывает их в окне программы и в браузере. Найденные объекты она отдаёт внешним +программам по TCP. Программа работает на рабочей станции с экраном и на Raspberry Pi 5 +без оператора. -## 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). | -| `python_app/` | Python application: `gui/`, `orchestration/` (process supervisor, shared-memory readers, config), `hardware_full/` (device drivers and services), `webui/`, `storage/`, `models/`, `workflows/`, `scripts/` (producers and tools), and `tests/`. | -| `run_config_examples/` | Ready-made configuration profiles, one per hardware setup. | -| `deploy/` | systemd daemon installer, and the USB update kit for the Pi and Jetson (`pack-usb.sh`, `update.sh`). | -| `build/bin/` | Compiled C++ binaries. | -| `device_firmware/` | Device firmware and protocol documentation. | +| одиночный LibreVNA | один векторный анализатор цепей и ВЧ-ключи на антеннах | +| многоприборная схема | три синхронных LibreVNA; с контроллером ключей — матрица 4 × 4 пар за одну развёртку | +| радиофотонный радар | СВЧ-сигнал формируют лазеры, данные оцифровывает АЦП L-Card E-502 или плата RFG | +| симулятор | работа без железа: один движущийся точечный отражатель | -## 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 - -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 +```sh +./start.sh ``` -`make` builds the sar3d GPU code when it finds CUDA (see Requirements) and -prints a note when it does not. After installing the CUDA requirements into an -existing venv, run `make` again: it notices the change and rebuilds that part. -CUDA 13 is installed from `requirements.txt` on x86_64. For GPUs older than -sm_75, install NVRTC 12 outside `.venv` and build with -`make BUILD_DIR=/tmp/build-nvrtc12 CUDA_ROOTS="/nvidia/cuda_nvrtc /nvidia/cuda_runtime"`. -A separate build directory keeps the two CUDA builds independent. +`start.sh` создаёт окружение Python, ставит пакеты и собирает C++. Корневой профиль +`run_config.json` — симулятор. В окне нажмите **Preprocessing…** → +**Create Neutral S21 Sets**, затем **Start** и выберите режим в списке **Mode**. -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, -without the launcher's build step or single-instance lock. +1. [Как работает радар](docs/principles.md) — физика измерения и обработки для тех, + кто не работал с векторными анализаторами и радарами. +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 -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/architecture.md) — процессы, кольца, кадры, состав кода, + где что править. +- [Разработка](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 -producer generates synthetic S-parameters and no radar is needed. Mock acquisition -supports the LibreVNA models; RadioPhotonic radar (RPH) needs native acquisition. -Both LibreVNA modes share one simulator: a point reflector moves back and forth -along the antenna array and between depths 3.25–4.25 m over an eight-second cycle. -Each S21 uses the actual transmitter–target–receiver distance from the configured -XYZ coordinates; coincident antennas produce identical signals. All combinations -share the same target position and measured capture window, with collections paced -at 50 ms intervals. The root mock profile explicitly selects a 0–8 m B-scan -range. Both shipped mock profiles use a 0.445 m vertical antenna baseline -(Rx1/Rx3 and, for the four-output matrix, Tx2/Tx3), avoiding coincident pairs. -The general defaults are 0.824–1.0 m; stored profiles retain their range. The -simulator models a stationary radar, so radar speed should be zero. -The scene uses the antenna positions from its startup profile; live geometry edits -change the reconstruction of captured sweeps, whose samples remain unchanged. -The preprocessor still requires S21 calibration/reference sets. Start the GUI: - -```bash -./start.sh --profile run_config.json -``` - -Open **Preprocessing…**, use **Create Neutral S21 Sets**, then **Start**. This makes -identity calibration + zero reference, preserving the simulated S21 samples. - -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://: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`. 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. +| Каталог | Что | +| --- | --- | +| `python_app/` | окно программы, драйверы приборов, веб-интерфейс, хранение данных, тесты | +| `data_acq_and_processing/` | C++: препроцессор, процессор с режимами обработки и локатором, коллектор E-502, тесты | +| `run_config_examples/` | профили под каждое железо | +| `run_configs/` | профили для загрузки из веб-интерфейса | +| `deploy/` | служба systemd и обновление с флешки | +| `device_firmware/` | прошивки LibreVNA, контроллера ключей и пульта тележки | +| `docs/` | документация | +| `start.sh` | запуск: окружение Python, сборка, программа | +| `run_config.json` | профиль по умолчанию — симулятор | +| `Makefile` | сборка и тесты C++ | +| `requirements.txt` | Python-пакеты | diff --git a/data_acq_and_processing/rph_radar_collector/README.md b/data_acq_and_processing/rph_radar_collector/README.md index ceffbbb..b29e674 100644 --- a/data_acq_and_processing/rph_radar_collector/README.md +++ b/data_acq_and_processing/rph_radar_collector/README.md @@ -1,101 +1,110 @@ -# RadioPhotonic radar (RPH) collector +# Коллектор радиофотонного радара (RPH) -Application-owned acquisition binary for the **L-Card E-502** ADC. It captures the -dual-channel main + reference IQ stream and publishes complete `RPH1` sweeps over -a PTY. `RphRadarService` in `python_app/hardware_full/rph_radar` reads and -processes that stream for `python_app.scripts.rph_radar_raw_producer`. +Программа сбора для АЦП L-Card E-502, которой владеет приложение. Она принимает +двухканальный поток IQ (основной и опорный сигналы) и передаёт целые развёртки в +пакетах `RPH1` через псевдотерминал. Поток читает и обрабатывает `RphRadarService` из +`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.rph_radar.source: "e502"`, paired with the Python `rph_radar_raw_producer`. -The `rfg_udp` source reads the RFG board's UDP stream directly and does not use this -binary. LibreVNA acquisition uses Python drivers. +Это источник данных для `radar.model: "rph_radar"` с `radar.rph_radar.source: "e502"`. +Источник `rfg_udp` читает UDP-поток платы RFG напрямую и эту программу не использует; +LibreVNA работает через драйверы на Python. Как радар устроен целиком — в главе +[Оборудование](../../docs/hardware.md). -## Layout +## Состав -| Path | Contents | -|------|----------| -| `src/` | Collector implementation (`main.cpp`) and the PTY writer. | -| `include/` | PTY header and `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. | +| Путь | Что | +| --- | --- | +| `src/` | коллектор (`main.cpp`) и запись в псевдотерминал | +| `include/` | заголовок псевдотерминала и `switch_sequencer.h` | +| `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 -DI_SYN2 sweep ends. Duration limits and Stop never advance the switches. Every -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. +Коллектор использует общий драйвер ВЧ-ключей H7992 со своим набором заголовков и +каталогом объектов `build/rph/`. -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, -continuously; DI_SYN2 gates sweeps in software. Source times are the monotonic -clock immediately before `StreamsStart` plus the ADC sample index divided by the -configured sample rate. These are source-clock estimates, not hardware timestamps; -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. +Если ключами управляет коллектор, `SwitchSequencer` переключает ключи H7992 по +настоящим концам развёрток (спаду DI_SYN2). Ограничение длительности и остановка +ключи не переключают. При каждой смене пары коллектор отбрасывает оставшийся +декодированный блок АЦП и цифрового входа и после `max(1 ms, run.settling_ms)` +создаёт сеанс источника заново. Принадлежность развёртки новой паре по времени не +определяется. Цикл считается законченным, только когда для каждой пары есть целая +чистая развёртка. Если пара не меняется, сеанс сохраняется. -The collector uses the common H7992 driver, with its own include set and -`build/rph/` object tree. +Перезапуск сеанса: остановка циклического вывода DOUT с `WAIT_DONE`, остановка и +отключение потоков, возврат 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 -(four u64), input/output positions (two u16), flags (u32; bit 0 means dirty). -Without collector switching both positions are `0xffff`, and cycle equals sweep -ID. Payload points remain four u16 words: marker (`0x000a` main, `0x00a8` -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, -then starts a new collector after settling; phase-processing state is preserved. +Коллектор работает от внутренних часов АЦП. АЦП и цифровой вход запускаются вместе и +работают непрерывно; развёртки выделяет программа по линии DI_SYN2. Время отсчёта — +монотонные часы компьютера непосредственно перед `StreamsStart` плюс номер отсчёта, +делённый на частоту выборки. Это оценка по часам источника, а не аппаратная метка: +задержка запуска сдвигает начало отсчёта раньше. Очереди USB и псевдотерминала время +не сдвигают. Если оценка времени отсчёта оказалась позже момента его приёма, сбор +останавливается, чтобы не публиковать окно съёма из будущего. Расхождение часов АЦП и +компьютера может сработать на этой проверке при долгой работе на одной паре; её +предел и точность окон съёма нужно измерить на железе. Данные старого сеанса SDK +после переключения ключа чистыми не становятся ни при каком расхождении часов. -## Build +## Формат псевдотерминала + +Пакет — 48-байтный заголовок в порядке байтов little-endian (`<4sIQQQQHHI`) и данные: +`RPH1`, длина данных (u32), номер развёртки источника, номер цикла, начало и конец +съёма в нс (четыре u64), положения входа и выхода (два u16), флаги (u32; бит 0 — +развёртка испорчена). Без переключения коллектором оба положения равны `0xffff`, а +номер цикла совпадает с номером развёртки. Точка данных — четыре слова u16: метка +(`0x000a` основной сигнал, `0x00a8` опорный), номер ступени, I и Q со знаком. Кадров +границ и пар нет. При переполнении очередь выбрасывает целые развёртки; пакет, который +уже пишется, не обрезается. Наибольший размер данных — 1 048 544 байта. Целые циклы +собирает Python. При переключении ключей из Python прежний коллектор и чтение +псевдотерминала закрываются, а после паузы установления запускается новый коллектор; +состояние фазовой обработки сохраняется. + +## Сборка ```bash make rph_radar_collector # -> build/bin/rph_radar_collector ``` -It is **not** part of `make all`; `start.sh` builds it automatically when the -active config selects `radar.model == "rph_radar"`, unless `--skip-build` is set. -The Linux build uses the repository's common sources and vendored headers and -links with `-pthread -ldl -lutil`; no installed L-Card SDK is needed to build it. +В `make all` коллектор не входит; `start.sh` собирает его сам, если в профиле +`radar.model: "rph_radar"`, кроме запуска с `--skip-build`. Сборка в Linux берёт общие +исходники и заголовки из репозитория и линкуется с `-pthread -ldl -lutil`; +установленный SDK L-Card для сборки не нужен. -## Runtime +## Работа -The collector loads the proprietary `libx502api.so` / `libe502api.so` at run time -via `dlopen`, so those libraries must be discoverable by the dynamic loader. -The RPH radar service prepends `~/.local/lib` (their default install location) -unless the config pins `LD_LIBRARY_PATH` in `radar.rph_radar.env`. +Закрытые библиотеки `libx502api.so` и `libe502api.so` коллектор загружает при запуске +через `dlopen`, поэтому их должен находить загрузчик. Сервис RPH ставит +`~/.local/lib` (их обычное место) в начало пути поиска, если профиль не задаёт +`LD_LIBRARY_PATH` в `radar.rph_radar.env`. -There is one acquisition path: USB, differential ADC channels 2/3, maximum -internal-clock rate, rising/falling DI_SYN2 gating, DO1 pair subtraction and DO8 -reference overlay. `radar.rph_radar.args` sets ADC range, internal reference, -maximum sweep duration and DO8 period. The example's fixed-path options remain -accepted as guards; incompatible modes and unused diagnostic options are rejected. -The service appends `tty:` from `radar.rph_radar.tty_path`. +Путь съёма один: USB, дифференциальные каналы АЦП 2 и 3, наибольшая частота от +внутренних часов, выделение развёрток по фронту и спаду DI_SYN2, вычитание соседних +отсчётов с разными уровнями DO1 с усреднением и наложение опорного сигнала по DO8. +`radar.rph_radar.args` задаёт диапазон АЦП, внутреннюю опорную +частоту, наибольшую длительность развёртки и период DO8. Остальные параметры из +примера принимаются только как подтверждение этого пути; несовместимые режимы и +неиспользуемые отладочные параметры отвергаются. Параметр `tty:<путь>` из +`radar.rph_radar.tty_path` добавляет сервис. -When both RF switches use `driver_mode: "native"`, Python passes -`switching:` with the already decoded input/output switch settings, combos -and settling time. The collector reads no profile file. Otherwise, Python selects -the switches and starts a new collector after settling when the combination -changes. Calibration captures also use this Python-controlled path. +Если оба ВЧ-ключа в `driver_mode: "native"`, Python передаёт коллектору +`switching:` с уже прочитанными настройками ключей, парами и паузой +установления; профиль коллектор не читает. Иначе ключи переключает Python и при смене +пары после паузы запускает новый коллектор. Съём наборов калибровки тоже идёт этим +путём. -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 -Stop drains and closes the service's own child. No global process-name kill is -used. +Процессом коллектора владеет запустивший его сервис; вывод коллектора уходит в журнал +программы. В Linux смерть родителя завершает коллектор сигналом SIGKILL; обычный +**Stop** дочитывает и закрывает собственный дочерний процесс сервиса. Завершения +процессов по имени нет. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..01ab8f7 --- /dev/null +++ b/docs/architecture.md @@ -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, без видеокарты. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..eca7a29 --- /dev/null +++ b/docs/configuration.md @@ -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/<вид набора>//`. Смена любой +величины, из которой строится ключ, скрывает наборы, снятые при старых значениях. + +| Радар | Из чего строится | Пример (из профилей-примеров) | +| --- | --- | --- | +| `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`. Неизвестные ключи пропускаются. diff --git a/docs/data.md b/docs/data.md new file mode 100644 index 0000000..d05c017 --- /dev/null +++ b/docs/data.md @@ -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//<имя>.npz`, `.json` | наборы калибровки | окно предобработки | +| `python_app/data/s21_reference//<имя>.npz`, `.json` | наборы референса, в том числе `tmp_reference` | окно предобработки, временный референс | +| `python_app/data/s21_object_calibration//<имя>.npz`, `.json` | калибровки по объекту | окно калибровки по объекту | +| `python_app/data/preview_png/<вид>/<имя>//i<вход>_o<выход>.png` | картинки развёрток при съёме по вариантам радара | окно предобработки | +| `/<набор данных>/` | снимки и записи на диск; по умолчанию `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**). Имя каталога — `<префикс>_`, например +`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__<имя>_kind<вид>_<поле>.npy`, результаты пары — в подкаталоге +`block__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** создаёт в `/<набор данных>/` по файлу на пару +`<имя набора>_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//<имя>.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_*`. diff --git a/docs/desktop.md b/docs/desktop.md new file mode 100644 index 0000000..a82042b --- /dev/null +++ b/docs/desktop.md @@ -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 | + +Каталог набора данных — `/<префикс>_`. Префикс описывает снятое: +модель, полосу, число точек (у радиофотонного радара — `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**; `` — набор не выбран. Выбираются наборы в окне +**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`. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..ea28945 --- /dev/null +++ b/docs/development.md @@ -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`, локальные инструменты разработчиков. diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000..b67d0ab --- /dev/null +++ b/docs/glossary.md @@ -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) | радар с синтезированной апертурой; режим, строящий трёхмерное изображение сцены | diff --git a/docs/hardware.md b/docs/hardware.md new file mode 100644 index 0000000..20f157b --- /dev/null +++ b/docs/hardware.md @@ -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:` | период выхода 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 и форматы кадров между процессами программы. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..3fdbdf1 --- /dev/null +++ b/docs/installation.md @@ -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` — следующий + свободный номер; это копия для отката; +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 ~/Documents/radar_system +sudo systemctl start radar.service +``` + +Новую версию при откате лучше переименовать, а не удалять: в ней могут быть +измерения, снятые после обновления. + +На свежую машину (без прежней версии) `update.sh` спрашивает +`Install a fresh one there? [y/N]` и ставит корневой `run_config.json` из архива, то есть +симулятор: профиль с железом нужно выбрать после установки. diff --git a/docs/preprocessing.md b/docs/preprocessing.md new file mode 100644 index 0000000..7673a00 --- /dev/null +++ b/docs/preprocessing.md @@ -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 <вид>//<имя>` | +| в наборе нет какой-то пары сбора | `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** | вид снимаемого набора, сколько пар снято из скольких, какая пара следующая; без съёма — ``, `0 / 0`, после последней пары — `` | +| **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/<вид>/<имя>//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`. + +Если изменить таблицу геометрии так, что она перестала совпадать с геометрией +выбранной поправки, выбор поправки снимается, а работающий с ней сбор +приостанавливается и продолжается уже без неё. + diff --git a/docs/principles.md b/docs/principles.md new file mode 100644 index 0000000..3da4d6b --- /dev/null +++ b/docs/principles.md @@ -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). diff --git a/docs/processing.md b/docs/processing.md new file mode 100644 index 0000000..f59f5c9 --- /dev/null +++ b/docs/processing.md @@ -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 м/с. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..a74e3d7 --- /dev/null +++ b/docs/troubleshooting.md @@ -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` | diff --git a/docs/web.md b/docs/web.md new file mode 100644 index 0000000..78644a9 --- /dev/null +++ b/docs/web.md @@ -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** в браузере. diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000..59092c3 --- /dev/null +++ b/docs/workflow.md @@ -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. Закройте окно: запись на диск дописывается, процессы конвейера останавливаются. diff --git a/python_app/tests/test_documentation.py b/python_app/tests/test_documentation.py new file mode 100644 index 0000000..89438e7 --- /dev/null +++ b/python_app/tests/test_documentation.py @@ -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() diff --git a/python_app/webui/static/app.js b/python_app/webui/static/app.js index 097fe9a..607739e 100644 --- a/python_app/webui/static/app.js +++ b/python_app/webui/static/app.js @@ -340,7 +340,7 @@ btnApply.addEventListener("click", async () => { btnResetHistory.addEventListener("click", async () => { btnResetHistory.disabled = true; try { - await api("/api/clear_history"); + await api("/api/clear_history", {}); settingsNote.textContent = "History reset requested."; } catch (err) { settingsNote.textContent = err.message;