added docs
This commit is contained in:
@@ -252,7 +252,6 @@ AGENTS.md
|
||||
.claude/
|
||||
.graphifyignore
|
||||
pyrightconfig.json
|
||||
/docs/
|
||||
test_end_2/
|
||||
|
||||
# --- device_firmware: PlatformIO / STM32G431 cart remote ---
|
||||
|
||||
@@ -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="<dir>/nvidia/cuda_nvrtc <dir>/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://<host>:8080`; set
|
||||
`RADAR_SYSTEM_WEBUI_PORT` to use another port. It is available in desktop and
|
||||
headless mode. Browser actions are:
|
||||
|
||||
- **Start**, **Single Capture**, **Stop**, **Capture Temporary Reference** and **Remove Last Measurement**;
|
||||
- load a profile from `run_configs/*.json` using **Load**;
|
||||
- **Save Last 1000** and **Start + Record to Disk** with a path, optional name and recording count;
|
||||
- edit processor settings and antenna tables with **Apply**, or clear history with **Clear Runtime History**;
|
||||
- select a **B-scan pair** or advance with **Next Combination**.
|
||||
|
||||
Copy profiles from `run_config_examples/` into `run_configs/` to offer them in the
|
||||
browser. JSON export and calibration dialogs are desktop actions. PNG capture
|
||||
runs only while a browser is connected and the plot has changed, including zoom
|
||||
and resize, at most five times per second. Each client retains the latest frame,
|
||||
status and settings independently; reconnecting refreshes settings, and **Apply**
|
||||
sends only edited fields. Field labels, units and checkboxes match the desktop form.
|
||||
|
||||
## Data and preprocessing
|
||||
|
||||
### Snapshots and recording
|
||||
|
||||
**Save Last 1000** and **Save S21 JSON** keep up to 1000 available collections from
|
||||
each stage independently, including raw or preprocessed sweeps without a result.
|
||||
The limit is fixed; legacy profile `save_count` values are ignored. Saving
|
||||
keeps an active disk recording running.
|
||||
Without raw/preprocessed taps, result snapshots remain available, but S21 JSON
|
||||
requires retained traces.
|
||||
**Start + Record to Disk** has a separate count
|
||||
for future results. It keeps collections received after arming until that count,
|
||||
Stop, or exhaustion of the bounded writer queue. If storage falls behind,
|
||||
recording stops accepting data, drains the queue and final chunk, and reports the limit.
|
||||
Stop and closing the window also flush accepted data without blocking the GUI.
|
||||
Pausing acquisition for a capture or history action ends an active recording:
|
||||
the log reports the accepted count, requested count and stop reason before draining
|
||||
the writer. Latest-wins rings cannot recover sweeps overwritten before the GUI
|
||||
receives them. NumPy datasets preserve S21, capture
|
||||
windows, every supported result payload, and processing time. Failed exports remove
|
||||
their staging files, leaving existing datasets intact. Companion profiles retain
|
||||
the radar, antenna layout and preprocessing used for acquisition, including when
|
||||
another profile is loaded after Stop. Dataset name prefixes also describe the acquisition;
|
||||
a manual save is cancelled if acquisition is restarted during its final history drain.
|
||||
|
||||
Each NumPy dataset includes `processing_settings.json`, mapping result revisions to
|
||||
the live settings used for processing. Results retain their revision, capture window,
|
||||
and actual `speed_m_s` (including speed supplied by the locator). Settings
|
||||
changed while acquisition is stopped are recorded for the reprocessed results as well.
|
||||
**Remove Last Measurement** uses capture order and removes matching collections
|
||||
from every stage; the processor suppresses that sweep if it is still pending.
|
||||
**Clear Runtime History** clears all retained
|
||||
stages and rejects late results from before the cutoff. These actions leave saved
|
||||
files intact. A sweep is identified by `(collection_id, capture_start_ns)`; all
|
||||
stages preserve the producer's capture window. `monotonic_ns` is diagnostic only.
|
||||
|
||||
**Save S21 JSON** can add exports to an existing NumPy dataset. Existing JSON files
|
||||
are never overwritten; missing raw/preprocessed counterparts produce empty arrays,
|
||||
and stages pair only by the exact acquisition key. Its `vna_config_profile.json` companion
|
||||
leaves the NumPy dataset's `config_profile.json` intact. A streaming write failure
|
||||
keeps all collections already published; only the incomplete collection is discarded.
|
||||
Legacy snapshots without capture windows retain repeated IDs through one-to-one
|
||||
time/index matching; processing completion timestamps are not compared with
|
||||
acquisition timestamps.
|
||||
|
||||
### Calibration and reference sets
|
||||
|
||||
Calibration and reference sets keep their trace arrays and metadata together in one
|
||||
atomically replaced NPZ archive, with a readable JSON sidecar. Both files and their
|
||||
directory entries are synced before successful completion. Existing file permissions
|
||||
are retained; new files and directories respect the process umask. Earlier NPZ/JSON pairs
|
||||
remain supported. Invalid trace shapes, duplicate combinations and non-finite
|
||||
calibration/reference values are rejected before a saved set is replaced.
|
||||
Median captures also require identical frequency axes and channel coverage.
|
||||
Open **Preprocessing…** to create or select the ordinary S21 calibration and
|
||||
empty-scene reference. The processing order is through calibration → calibrated
|
||||
reference subtraction → notch → object correction → the selected processor.
|
||||
|
||||
### Object calibration
|
||||
|
||||
**Create…** in the **Object calibration** row opens a separate dialog. Use
|
||||
**Capture Data** to take fresh preprocessed sweeps, then enter the measured
|
||||
reflector centre X/Y/Z in metres and use **Calculate and Save**. The optional
|
||||
**Calculation Settings** section expands inside the scrollable dialog body;
|
||||
the calculation, cancellation and close buttons remain visible below it.
|
||||
Coordinates follow the notebook: X across the array, Y above the ground, Z along
|
||||
the ground.
|
||||
Height above ground, radar pitch and sphere radius follow `MIMO_calibration.ipynb`;
|
||||
several reflector positions may contribute to one calibration. The only diagnostic
|
||||
plot shows the phase of each antenna pair's contribution at the target point,
|
||||
before and after correction, as in cell 5 of `MIMO_calibration.ipynb`: Tx rows,
|
||||
Rx columns, phase relative to the complex sum of each map, with cell values and
|
||||
standard deviation in degrees on a cyclic ±180° scale.
|
||||
With partial antenna geometry, only the specified Tx × Rx pairs contribute to
|
||||
the calculation and phase maps. Acquisition still retains every physical stage;
|
||||
pairs outside that geometry receive unit correction and appear in `unchanged_combos`.
|
||||
The selected reference is already subtracted, and the previous object correction
|
||||
is disabled while these sweeps are acquired. A capture owns an exclusive pipeline
|
||||
pause until it finishes or is cancelled, including during automatic-restart backoff.
|
||||
An explicit **Stop** cancels automatic resumption.
|
||||
|
||||
Object corrections use the same NPZ collection format as other preprocessing sets:
|
||||
S21 holds each pair's multiplier on the captured frequency grid. Metadata retain
|
||||
the capture radar key, antenna geometry, through-set name and content hash, target
|
||||
positions, estimation settings and phase diagnostics. Saving uses the capture's
|
||||
configuration even if the current profile changes during calculation. The next
|
||||
**Start** checks the selected through set's S21 and frequency arrays, geometry and
|
||||
coverage of the requested combinations before applying the correction after notch
|
||||
filtering and before any processor. **Off** disables it. Changing antenna geometry
|
||||
disables an incompatible correction and reloads acquisition without restarting
|
||||
the application. Changing relative permittivity only updates processing settings.
|
||||
|
||||
Legacy `cal_*.npy` / `cal_meta.json` folders are not application calibration sets.
|
||||
Capture a set through **Create…** to use object correction; the legacy files on
|
||||
disk are left intact. Ordinary through/reference NPZ sets need no conversion.
|
||||
|
||||
Older NPZ sets and snapshots remain readable: extra S11 arrays are ignored;
|
||||
new trace files contain only frequency and S21.
|
||||
|
||||
## Runtime interfaces
|
||||
|
||||
Tap rings are optional: an empty `rings.raw_tap.name` or
|
||||
`rings.preprocessed_tap.name` disables that copy and its GUI history. Core processing
|
||||
and single capture still work without taps; object calibration needs the preprocessed
|
||||
tap. All producers share raw publication and hardware retry handling.
|
||||
|
||||
Raw and preprocessed IPC use **RAW3** and **PRP3** respectively: each trace contains
|
||||
float32 frequencies followed by complex64 S21 (12 bytes per point). The complete
|
||||
frame requires collection and per-trace capture windows; its size is `44 + combos * (28 + 12 * points)` bytes.
|
||||
Older RAW2/PPR2 bundles are incompatible and are rebuilt from NPZ sets at Start;
|
||||
update Python and C++ together. Results use RSL3; older RSL2 frames are rejected.
|
||||
|
||||
RF switches use H7992 with 1–4 positions and two GPIO control lines. Profiles use
|
||||
`driver_mode`, GPIO settings and the position count. An old `driver: "h7992"` is accepted;
|
||||
other explicit driver values or enabled inversion are rejected before GPIO opens.
|
||||
Two positions use the existing H7992 mapping `(A,B) = (0,0), (0,1)`; physical
|
||||
wiring and channel numbers follow the profile.
|
||||
|
||||
Single-device LibreVNA acquires the configured `run.combos` in order. Multi-device
|
||||
acquisition keeps the full matrix: SAR3D derives intra-sweep stage timing from
|
||||
the complete list of outputs sharing a capture window.
|
||||
|
||||
## Configuration
|
||||
|
||||
A profile selects the radar model and sweep, switches and acquisition combos,
|
||||
preprocessing sets, antenna geometry, ring sizes and GUI settings. The GUI loads
|
||||
`RADAR_SYSTEM_PROFILE` (set by `start.sh --profile`), otherwise the last profile in
|
||||
`python_app/runtime/gui_session_state.json`, otherwise root `run_config.json`.
|
||||
If a selected external profile cannot be loaded, it reports the error and falls
|
||||
back to the root profile. **Load Config** and **Save Config** manage desktop profiles.
|
||||
|
||||
Hardware examples are in `run_config_examples/`. The root `run_config.json` is
|
||||
intentionally mock; select a hardware profile explicitly on a real device. A
|
||||
change of model, serials, sweep parameters or multi-device switch positions changes
|
||||
the `radar_key` used to find calibration/reference sets; select or capture matching
|
||||
sets before **Start**. Changing the multi-device reference-clock source also resets
|
||||
display history. For RadioPhotonic radar (RPH), identity follows the phase anchors, output
|
||||
band and acquisition/laser settings; executable/TTY paths and LibreVNA-only sweep
|
||||
fields do not affect it. RPH processing frequency controls use its output band.
|
||||
Phase, band and timeout errors are rejected before a running pipeline is stopped.
|
||||
|
||||
Profiles with model `kamil_adc` are rejected with an explicit error: use
|
||||
`rph_radar` and the `radar.rph_radar` section. An inactive `radar.kamil_adc`
|
||||
section left in older saved profiles is ignored; there are no aliases or automatic
|
||||
data migrations. Old RPH calibration/reference sets use a different
|
||||
measurement key and are not selected by the renamed mode.
|
||||
|
||||
### RPH phase calibration
|
||||
|
||||
Use `run_config_examples/run_config_rph_radar.example.json` (E-502) or
|
||||
`run_config_rph_radar_rfg.example.json` (RFG board) for **RadioPhotonic radar (RPH)**.
|
||||
Apply the desired laser settings with **Apply Radar**, then stop acquisition before
|
||||
running the calibration tool:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200
|
||||
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200 --apply
|
||||
```
|
||||
|
||||
Pass the path to your active RPH profile. The first command reports measured phase
|
||||
anchors; `--apply` captures again and writes only `phase0_rad` and `phase1_rad` in
|
||||
`radar.rph_radar.phase_calibration`. Frequency anchors must already describe the
|
||||
known sweep endpoints. The tool uses current acquisition settings, sends no laser
|
||||
commands, preserves every other JSON byte, and refuses to overwrite a profile
|
||||
edited during capture. Band and point count stay unchanged. `--warmup` controls
|
||||
how many initial sweeps to discard (default 10).
|
||||
|
||||
## Deployment
|
||||
|
||||
On a Raspberry Pi, `deploy/install-daemon.sh` installs a systemd service that runs
|
||||
the application through `start.sh --headless --skip-build` and restarts it on
|
||||
failure. The launcher holds a single-instance lock for the radar, rings and locator
|
||||
port; an interactive launch first asks systemd to stop an active daemon.
|
||||
|
||||
**Updating a device from a USB flash drive.** On the development machine
|
||||
`deploy/pack-usb.sh /path/to/drive` writes the host part of the **last commit**
|
||||
and its update script into `radar_update/` on the flash drive. Uncommitted changes,
|
||||
venv, build outputs, recorded data and local configs are excluded.
|
||||
On the Pi or Jetson, close the program and run, from the drive's folder:
|
||||
|
||||
```bash
|
||||
bash radar_update/update.sh # or: --python 3.13 for a fresh venv
|
||||
```
|
||||
|
||||
Run the updater as the normal device user. It installs the newest archive into
|
||||
`~/Documents/radar_system` (or `RADAR_DIR`), renaming the current version to the
|
||||
next free `radar_system<N>`. It preserves:
|
||||
|
||||
- `run_config.json` and changed/added `run_config_examples/*.json` by independent
|
||||
copy; a conflicting release profile is placed beside the device profile as `.new`;
|
||||
- `python_app/runtime/` by copy, excluding logs and `*.pids`;
|
||||
- `python_app/data*` and extra non-hidden top-level entries absent from the release
|
||||
by hard links, excluding `build`, `graphify-out`, `device_firmware`, `docs` and `RELEASE`;
|
||||
- the venv by independent copy, or a fresh one when `--python` is supplied.
|
||||
|
||||
Saved data is written through replacement or new files, preserving the backup's
|
||||
hard links. `RELEASE` gets a new inode. The updater builds the C++ pipeline and ADC
|
||||
collector, then restarts `radar.service` only if it was running before the update.
|
||||
Keep the device's real hardware profile: the release's root profile is mock.
|
||||
|
||||
For rollback, stop `radar.service` and close the GUI, rename the new installation
|
||||
aside to preserve measurements taken since the update, then rename the chosen
|
||||
backup to `radar_system`. Restart the service if it is normally used.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
|
||||
make check-native
|
||||
make check-sar3d PYTHON=.venv/bin/python
|
||||
make check-gpr
|
||||
ASAN_OPTIONS=detect_leaks=0 make check-native NATIVE_SANITIZERS=address,undefined
|
||||
```
|
||||
|
||||
The tests cover failed writes and settings commits, recordings during manual Save,
|
||||
repeated Start/Stop/Close, web-action cancellation, malformed IPC frames, hardware
|
||||
resource cleanup with mock failures, and an isolated deployment update. Native tests
|
||||
also exercise mutated frames, ring concurrency, locator input and ADC whole-sweep PTY overflow recovery.
|
||||
Firmware host tests check cart timer rollover and SBUS packing; they do not validate
|
||||
physical timing. Real devices, GPIO and the CUDA backend need their own hardware run.
|
||||
|
||||
`check-sar3d` runs the real CUDA processor (`data_acq_and_processing/tests/test_backprojection.py`).
|
||||
It compares it with `sar3d_reference.npz`: compact float32/complex64 inputs, the three
|
||||
maximum projections of the notebook's displayed volume, its peak/level, response
|
||||
widths and background ratio for ten cases. Six cover compensation/normalization,
|
||||
equalization, background, scene and timing. Four are frames of point reflectors in noise
|
||||
for CLEAN, which also compare the objects and their boxes: with and without the background
|
||||
gate, with a scene where the stop at each range's background adds steps, and with a
|
||||
narrowed scene. The box rules are also checked
|
||||
one by one on synthetic objects. The expected values come from unchanged functions in cells 1 and 5
|
||||
of the original notebook, with each frame's previous-collection mean subtracted
|
||||
from its input spectra by NumPy. The same test checks the published
|
||||
status for invalid input, a volume beyond float32, a result too large for the ring slot,
|
||||
an empty and a very weak signal, and that the views are normalized at their peak on
|
||||
finer grids. Without a usable GPU it checks only the CUDA warning and the box rules and
|
||||
skips the rest; the notebook itself is not needed to run it. To regenerate the references,
|
||||
pass the original notebook and the Horns script (both are kept outside the repository):
|
||||
|
||||
```bash
|
||||
.venv/bin/python data_acq_and_processing/tests/generate_backprojection_reference.py \
|
||||
--notebook /path/to/Multi_depthnorm_clean.ipynb \
|
||||
--gpr-script /path/to/Horns_motion_4x4_ground.py
|
||||
```
|
||||
|
||||
The generator also writes `gpr_reference.npz` from the unchanged Horns coherent BP
|
||||
functions, with the notebook equalizer inserted before scalar normalization.
|
||||
`make check-gpr` compares the CPU processor with those images, including motion and
|
||||
previous-collection subtraction. Neither source file is needed to run the tests.
|
||||
|
||||
The archives record source SHA256 hashes and NumPy/SciPy versions. Another build
|
||||
can be checked with `make check-sar3d BUILD_DIR=/path/to/build` or by running
|
||||
`test_backprojection.py Sar3dTest` with
|
||||
`BACKPROJECTION_FIXTURE=/path/to/build/native_tests/backprojection_fixture`.
|
||||
For older GPUs, pass the same `BUILD_DIR` and NVRTC 12 `CUDA_ROOTS` used to build.
|
||||
|
||||
For development, install `pyright` into the venv, then run
|
||||
`.venv/bin/pyright python_app` to check all Python modules and tests.
|
||||
|
||||
## Application architecture
|
||||
|
||||
Invalid device/switch settings and undersized raw/preprocessed slots fail before
|
||||
acquisition starts. Oversized processed results report the required slot size in
|
||||
the log; switching to a mode that fits resumes processing without a restart.
|
||||
|
||||
`AppWindow` assembles controllers and shuts them down. `WindowView` owns layout
|
||||
and typed sections; controllers receive concrete dependencies, without GUI mixins
|
||||
or duplicate window state. `ProfileController` owns startup selection, session
|
||||
state and profile persistence. `RadarSettingsController` applies device limits,
|
||||
while `LiveSettingsController` publishes revisions and updates plots.
|
||||
|
||||
GUI scalar settings use dataclass metadata in `models/gui_profile_schema.py`:
|
||||
profile validation, desktop values, live processing fields, processing log summaries
|
||||
and web forms share those bindings. Hardware fields and geometry have their own
|
||||
typed mappings and validators. Section builders return typed widget groups; their
|
||||
controllers connect signals directly, with no string table of action callbacks.
|
||||
`gui/controllers/config_controller.py` reads and applies scalar metadata without owning
|
||||
the window or acquisition processes. Web batches roll back together when validation
|
||||
or live-file publication fails; desktop settings also revert to the last publication.
|
||||
Profile loading blocks editor signals until all settings are populated.
|
||||
|
||||
`HistoryController` owns retained acquisitions; `PlotController` selects the view
|
||||
and supplies independent B-scan, GPR, trace and SAR renderers with typed settings.
|
||||
Renderers own their plots and display caches, without access to the acquisition
|
||||
processes. Every plot change passes through `PlotController`, which counts changes
|
||||
of the shown image for the browser stream. The display shows the newest acquisition,
|
||||
including results read while stopping. Stopped B-scan replay uses the last run's
|
||||
ring, matches exact acquisition keys and the requested settings revision, and
|
||||
cancels when that run changes; sweeps the processor does not return are left out
|
||||
rather than mixing settings in one image. With no retained preprocessed sweeps
|
||||
(including disabled taps), the processor recomputes its own retained history.
|
||||
Capture previews preserve the selected processing view.
|
||||
`PipelineController` owns processes, ring readers, polling/recovery, Single Capture
|
||||
and replay. Its pause prevents recovery from starting a producer over a hardware
|
||||
capture; Stop or Close cancels any pending resumption, including during a drain.
|
||||
Validation rejects Start before stopping existing processes. Pauses resume only
|
||||
continuous acquisition, retry failed resumption with the usual backoff, and release
|
||||
ownership before presenting capture errors. GPIO errors are logged without dialogs.
|
||||
Rendering failures leave acquisition and the locator running and log each cause once;
|
||||
repeated ring-read failures retain their separate recovery and stop policy.
|
||||
`PreprocessController` owns calibration/reference sessions and asset selection.
|
||||
One `SequentialCaptureSession` handles both current settings and a list of sweep
|
||||
variants; a single variant adds no extra acquisition or reconfiguration. Multi-device
|
||||
collection always captures one complete synchronized cycle. The native
|
||||
`DataPreprocessor` owns all three bundles in `preprocessing/bundles`: through,
|
||||
through-calibrated reference and object correction, applied in that order with notch
|
||||
filtering before object correction.
|
||||
Object capture checks both acquisition processes and releases its pause only after
|
||||
completion or cancellation; losing the producer reports a failure immediately.
|
||||
`SnapshotController` coordinates manual exports, paused Clear/Remove operations
|
||||
and Start + Record to Disk.
|
||||
History deletion is retained before publishing it to the processor, so a failed
|
||||
settings write can be retried without restoring deleted sweeps. `RecordingController`
|
||||
owns a bounded writer session; its disk thread receives data and storage only.
|
||||
A processor restart during Start + Record to Disk does not finish the newly armed session.
|
||||
Temporary references and neutral sets clear retained processing history without
|
||||
restarting the locator while the pipeline is paused.
|
||||
|
||||
`WebBridge` queues browser actions onto Qt and exposes immutable status/frame
|
||||
snapshots. Clear and Remove use separate action endpoints; live settings only edit
|
||||
settings. Trace display options (including its Y range) do not publish processing
|
||||
revisions. A timeout cancels queued actions; an action already running reports that
|
||||
state without inviting a duplicate request. The GPIO controller invokes the same
|
||||
reference-capture action as desktop and web, and ignores queued presses after Close.
|
||||
`LogController` batches worker logs and returns web-action errors to the browser
|
||||
without opening a desktop modal. Stop/Close also cancels delayed launcher startup.
|
||||
|
||||
GUI profiles, run configs, session state and live processing settings use shared
|
||||
atomic writers with unique temporary files. Profiles and run/session settings sync
|
||||
their file and directory; frequent live settings avoid extra disk syncs. A failed
|
||||
write before replacement leaves the previous file intact. The launcher and supervisor
|
||||
share acquisition producer selection.
|
||||
| Каталог | Что |
|
||||
| --- | --- |
|
||||
| `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-пакеты |
|
||||
|
||||
@@ -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:<path>` 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:<JSON>` 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:<JSON>` с уже прочитанными настройками ключей, парами и паузой
|
||||
установления; профиль коллектор не читает. Иначе ключи переключает 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** дочитывает и закрывает собственный дочерний процесс сервиса. Завершения
|
||||
процессов по имени нет.
|
||||
|
||||
@@ -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, без видеокарты.
|
||||
@@ -0,0 +1,428 @@
|
||||
# Файл конфигурации
|
||||
|
||||
Все настройки программы хранятся в профиле — файле JSON. В профиле две части:
|
||||
конфигурация сбора и обработки (разделы `radar`, `switches`, `control_button`,
|
||||
`logging`, `run`, `preprocess`, `gpr`, `rings`) и состояние окна (раздел `gui`).
|
||||
Незнакомые термины (полоса ПЧ, дБм, пара, коллекция) — в [словаре](glossary.md).
|
||||
|
||||
| Где | Что |
|
||||
| --- | --- |
|
||||
| `run_config.json` в корне | профиль для разработки: симулятор многоприборной схемы, `driver_mode: "mock"`, уровень журнала `debug` |
|
||||
| `run_config_examples/` | готовые профили под каждое железо (таблица ниже) |
|
||||
| `run_configs/` | профили, которые можно загрузить из веб-интерфейса |
|
||||
| `python_app/runtime/run_config.json` | рабочая копия, которую программа пишет при каждом **Start**; по ней работают процессы конвейера |
|
||||
|
||||
> **Внимание.** Корневой `run_config.json` — профиль симулятора: с ним программа не
|
||||
> обращается к железу. На машине с радаром работайте с профилем из
|
||||
> `run_config_examples/`, скопированным и настроенным под установку.
|
||||
|
||||
## Как профиль загружается и сохраняется
|
||||
|
||||
При запуске программа берёт профиль по порядку:
|
||||
|
||||
1. путь из переменной окружения `RADAR_SYSTEM_PROFILE` (её задаёт
|
||||
`./start.sh --profile ПУТЬ`);
|
||||
2. последний использованный профиль из `python_app/runtime/gui_session_state.json`;
|
||||
3. корневой `run_config.json`.
|
||||
|
||||
Относительный путь отсчитывается от каталога проекта. Если выбранный профиль не
|
||||
читается, программа пишет
|
||||
`Failed to load the last selected config profile; falling back to root run_config.json.`
|
||||
и загружает корневой; если не читается файл сеанса —
|
||||
`Failed to read GUI session-state; using root run_config.json.` Если не читается и
|
||||
корневой профиль, программа не запускается. Профиль без раздела `gui` загружается со
|
||||
значениями окна по умолчанию; при запуске (кроме корневого профиля) об этом пишется
|
||||
`Loaded legacy run config without GUI defaults: <путь>`, при **Load Config…** —
|
||||
`kind=legacy run config`
|
||||
в строке `Config profile loaded: …`.
|
||||
|
||||
**Load Config…** загружает другой профиль; это возможно только при остановленном
|
||||
сборе (`Stop all pipeline processes before loading a config profile`) и не во время
|
||||
съёма наборов.
|
||||
**Save Config…** записывает текущие значения всех полей окна, выбранные наборы и
|
||||
геометрию антенн. Загруженный или сохранённый профиль становится стартовым для
|
||||
следующего запуска.
|
||||
|
||||
> **Внимание.** Изменения в окне и в веб-интерфейсе, в том числе выбор наборов и
|
||||
> временный референс, в профиль сами не записываются. После перезапуска программы они
|
||||
> пропадут, если не нажать **Save Config…**.
|
||||
|
||||
Правила чтения:
|
||||
|
||||
- нет раздела или ключа — берётся значение по умолчанию. Умолчания схемы — минимальные
|
||||
заглушки (`points: 1`, `if_bandwidth_hz: 1.0`, пустые имена колец), а не рабочие
|
||||
настройки: начинайте с профиля-примера;
|
||||
- типы строгие: целое число нельзя записать как `5.0`, логическое — как `1`; ошибка
|
||||
называет ключ (`points must be a JSON integer`);
|
||||
- неизвестные ключи пропускаются и при сохранении исчезают;
|
||||
- раздел `gui` с неизвестным номером версии отвергается (`Unsupported gui.version: N`).
|
||||
|
||||
## Когда изменение вступает в силу
|
||||
|
||||
| Обозначение | Когда |
|
||||
| --- | --- |
|
||||
| сразу | сразу, в том числе во время сбора |
|
||||
| Apply Radar | при нажатии **Apply Radar** (без экрана — автоматически при запуске) |
|
||||
| Start | при следующем **Start** или **Single Capture**: программа заново пишет рабочую копию профиля и перезапускает процессы сбора и предобработки |
|
||||
| Start, процессор | как Start, но перезапускается и процессор, а с ним TCP-локатор: его клиенты отключаются, история обработки очищается |
|
||||
| профиль | ключа нет в окне: правьте файл и загрузите его через **Load Config…** при остановленном сборе |
|
||||
|
||||
Процессор перезапускается, если изменились: ключ радара (`radar_key`), опорная частота
|
||||
многоприборной схемы, `driver_mode` радара и ключей, число положений ключей, список
|
||||
пар, любой ключ раздела `rings`, любой ключ `run.locator_server`, путь к live-файлу
|
||||
или `run.idle_sleep_ms`. Смена наборов предобработки процессор не перезапускает
|
||||
(исключение — нейтральные наборы).
|
||||
|
||||
## Ключ радара
|
||||
|
||||
`radar_key` — строка, по которой программа находит наборы калибровки и референса:
|
||||
они лежат в каталогах `python_app/data/<вид набора>/<radar_key>/`. Смена любой
|
||||
величины, из которой строится ключ, скрывает наборы, снятые при старых значениях.
|
||||
|
||||
| Радар | Из чего строится | Пример (из профилей-примеров) |
|
||||
| --- | --- | --- |
|
||||
| `librevna` | модель, серийный номер (`no_serial`, если пуст), начало и конец развёртки, число точек, полоса ПЧ, мощность | `librevna_no_serial_st1000000_sp6000000000_p201_if50000_pw-10` |
|
||||
| `librevna_multi` | то же для мастера и обоих ведомых, плюс положения ключей `_out`, `_in`, `_stim` | `librevna_multi_209E307A5532_20A1307D5532_2072306C5532_st500000000_sp6000000000_p1031_if50000_pw-3_out1_in1_stim2` |
|
||||
| `rph_radar` | первые 16 знаков хеша SHA-256 от выходной полосы, фазовой калибровки, параметров коллектора (`args`) или источника `rfg_udp`, числа положений ключей и настроек лазера (если плата лазеров включена или источник — RFG) | `rph_radar_2afdaa32e154b122` |
|
||||
|
||||
`dwell_us`, путь к программам и тайм-ауты в ключ не входят.
|
||||
|
||||
## Профили-примеры
|
||||
|
||||
| Файл | Радар | Что в нём |
|
||||
| --- | --- | --- |
|
||||
| `run_config.json` (корневой) | `librevna_multi`, `mock` | симулятор схемы с переключением стимула: 16 пар, 1 МГц–6 ГГц, 201 точка, вертикальная база антенн 0,445 м, журнал `debug` |
|
||||
| `run_config_librevna.example.json` | `librevna`, `native` | одиночный LibreVNA, выходной ключ на 2 положения (линии 17, 27), входной на 4 (линии 22, 23), 8 пар, 1 МГц–6 ГГц, 201 точка |
|
||||
| `run_config_librevna_multi.example.json` | `librevna_multi`, `native` | приборы `209E307A5532` (мастер), `20A1307D5532`, `2072306C5532`; 8 пар, 1 МГц–6 ГГц, 201 точка |
|
||||
| `run_config_librevna_multi.example2.json` | `librevna_multi`, `native` | то же для второго комплекта: `206830985532`, `206930965532`, `206930A15532` |
|
||||
| `run_config_librevna_multi_per_point.example.json` | `librevna_multi`, `native` | переключение стимула, комплект `209E307A5532`; 16 пар, 0,5–6 ГГц, 1031 точка, −3 дБм, `dwell_us: 2`, измеренная геометрия антенн; режим `sar3d` |
|
||||
| `run_config_librevna_multi_per_point_lowband.example.json` | `librevna_multi`, `native` | переключение стимула, комплект `206830985532`; 30–200 МГц, 201 точка — проверка ключей на низких частотах; у антенн y = z = 0, Tx2 и Tx3 совпадают с Tx0 и Tx1 |
|
||||
| `run_config_rph_radar.example.json` | `rph_radar`, `e502` | АЦП E-502, плата лазеров на `/dev/ttyUSB0` в режиме `variation`, ключи на 2 и 4 положения, 8 пар |
|
||||
| `run_config_rph_radar_rfg.example.json` | `rph_radar`, `rfg_udp` | плата RFG на `192.168.2.1:5000`, плата лазеров на `/dev/serial/by-id/usb-Silicon_Labs_CP2102_…`, одна пара, измеренные фазовые якоря |
|
||||
| `run_config_simulator.example.json` | `librevna`, `mock` | симулятор одиночного LibreVNA: 8 пар, 1 МГц–6 ГГц, 201 точка; режим `gpr` |
|
||||
|
||||
`run_configs/run_config.json` — профиль симулятора одиночного LibreVNA для
|
||||
веб-интерфейса, близкий к `run_config_simulator.example.json`.
|
||||
|
||||
## radar
|
||||
|
||||
| Ключ | Тип, по умолчанию | Что делает | Когда |
|
||||
| --- | --- | --- | --- |
|
||||
| `radar.model` | строка, `"librevna"` | тип радара: `"librevna"`, `"librevna_multi"` или `"rph_radar"`. Старое имя `"kamil_adc"` отвергается с подсказкой перейти на `rph_radar` | профиль; Start, процессор |
|
||||
| `radar.serial` | строка, `""` | серийный номер прибора; у многоприборной схемы — мастера (обязателен). Пустой у одиночного — первый найденный прибор | профиль; Start, процессор |
|
||||
| `radar.driver_mode` | строка, `"mock"` | `"native"` — настоящее железо, `"mock"` — симулятор. Радиофотонному радару нужен `"native"` | профиль; Start, процессор |
|
||||
|
||||
### radar.sweep
|
||||
|
||||
Развёртка LibreVNA. Радиофотонный радар эти ключи не использует (у него
|
||||
`radar.rph_radar.band`), а `points` у него в профиль не записывается. Поля редактируются в секции
|
||||
**Radar**; с подключённым прибором они подрезаются под его пределы.
|
||||
|
||||
| Ключ | Тип, по умолчанию | Поле | Что делает | Ключ радара |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `start_hz` | число, `1000000.0` | **Start (Hz)** | начало развёртки, Гц, не меньше 0 | да |
|
||||
| `stop_hz` | число, `6000000000.0` | **Stop (Hz)** | конец развёртки, Гц, больше начала | да |
|
||||
| `points` | целое, `1` | **Points** | число частотных точек | да |
|
||||
| `if_bandwidth_hz` | число, `1.0` | **IF bandwidth (Hz)** | полоса ПЧ, Гц, больше 0 | да |
|
||||
| `stimulus_power_dbm` | число, `-30.0` | **Stimulus power (dBm)** | мощность излучения, дБм | да |
|
||||
| `dwell_us` | целое, `0` | — | дополнительная задержка на каждой стадии прибора, мкс; при отправке значение молча ограничивается диапазоном 0–65535, а прибор — 10 239 мкс. В режиме переключения стимула в это время контроллер переключает ключ; в примерах `2` | нет |
|
||||
|
||||
Изменения применяются при **Start**; у одиночного LibreVNA — ещё и при
|
||||
**Apply Radar**.
|
||||
|
||||
### radar.multi_device
|
||||
|
||||
Только для `librevna_multi`; другие радары эти ключи игнорируют. В окне не
|
||||
редактируются.
|
||||
|
||||
| Ключ | Тип, по умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `slave_serials` | список строк, `[]` | серийные номера двух ведомых, ровно два. Первый даёт входы 0 и 1, второй — 2 и 3; порядок важен. Можно записать строкой через запятую; старое имя `slave_serial_numbers` тоже читается |
|
||||
| `force_external_reference` | логическое, `false` | внешняя опорная частота 10 МГц на всех трёх приборах. Включайте, только если опора подана на все приборы (глава [Оборудование](hardware.md)) |
|
||||
| `recovery_attempts` | целое, `3` | сколько раз переподключать приборы после сбоя, прежде чем передать ошибку процессу сбора |
|
||||
| `output_switch_positions` | целое, `1` | положения настоящего ВЧ-ключа на выходах мастера, которым управляет компьютер (линии из `switches.port1`). 1 — ключа нет. Ни один пример его не использует |
|
||||
| `input_switch_positions` | целое, `1` | то же для входов ведомых (линии из `switches.port2`) |
|
||||
| `stimulus_switch_positions` | целое, `1` | положения ключа переключения стимула внутри частотной точки; `2` — режим переключения стимула. Прошивка мастера поддерживает 1, 2 и 4, контроллер STM32 — 2 |
|
||||
|
||||
Все ключи, кроме `recovery_attempts`, входят в ключ радара или в условие перезапуска
|
||||
процессора.
|
||||
|
||||
### radar.rph_radar
|
||||
|
||||
Только для `rph_radar`. Поля без пометки «—» редактируются в секции **Radar**.
|
||||
|
||||
| Ключ | Тип, по умолчанию | Поле | Что делает |
|
||||
| --- | --- | --- | --- |
|
||||
| `source` | строка, `"e502"` | — | источник: `"e502"` — АЦП L-Card через `rph_radar_collector`, `"rfg_udp"` — плата RFG по UDP |
|
||||
| `udp_bind` | строка, `"0.0.0.0:5000"` | — | только RFG: адрес `IPv4:порт`, на котором принимаются пакеты |
|
||||
| `project_dir` | строка, `""` | **Project dir** | рабочий каталог коллектора; пусто — каталог проекта |
|
||||
| `executable_path` | строка, `""` | **Executable path** | путь к коллектору, обычно `build/bin/rph_radar_collector` |
|
||||
| `tty_path` | строка, `""` | **TTY path** | путь псевдотерминала, через который коллектор передаёт развёртки; старый файл по этому пути удаляется перед запуском |
|
||||
| `args` | список строк, `[]` | **Arguments** (по строке на параметр) | параметры коллектора (глава [Оборудование](hardware.md)); `tty:` здесь задавать нельзя |
|
||||
| `env` | объект строк, `{}` | **Environment JSON** | переменные окружения коллектора; `~` и `$ПЕРЕМЕННАЯ` раскрываются. Если `env` не задаёт `LD_LIBRARY_PATH`, в начало унаследованного `LD_LIBRARY_PATH` добавляется `~/.local/lib` |
|
||||
| `startup_timeout_s` | число, `5.0` | **Startup timeout (s)** | сколько ждать появления псевдотерминала после запуска коллектора |
|
||||
| `sweep_timeout_s` | число, `5.0` | **Read timeout (s)** | сколько ждать пригодной развёртки, покрывающей полосу, прежде чем переподключиться |
|
||||
| `stop_timeout_s` | число, `2.0` | **Stop timeout (s)** | ожидание остановки коллектора; на деле не больше 0,5 с, затем он завершается принудительно |
|
||||
| `phase_calibration.phase0_rad`, `freq0_hz` | `0.0`, `2046000000.0` | — | первая опорная точка «фаза опорного сигнала — частота» |
|
||||
| `phase_calibration.phase1_rad`, `freq1_hz` | `300.0`, `5612000000.0` | — | вторая опорная точка; фазы и частоты точек должны различаться |
|
||||
| `band.start_hz`, `band.stop_hz` | `2100000000.0`, `5500000000.0` | — | выходная полоса: развёртка пересчитывается на её сетку; развёртка, не покрывающая полосу, отбрасывается |
|
||||
| `band.points` | `2048` | — | число точек выходной сетки, не меньше 2 |
|
||||
|
||||
Для `source: "e502"` поля `executable_path` и `tty_path` обязательны, а `project_dir`, если
|
||||
задан, должен быть каталогом. Все три тайм-аута должны быть конечными положительными
|
||||
числами (`radar.rph_radar.<ключ> must be finite and positive`).
|
||||
|
||||
Фазы `phase0_rad` и `phase1_rad` записывает программа фазовой калибровки
|
||||
(`rph_radar_calibrate --apply`). Полоса и якоря входят в ключ радара, поэтому после
|
||||
новой фазовой калибровки старые наборы не видны.
|
||||
|
||||
### radar.laser_control
|
||||
|
||||
Плата лазеров радиофотонного радара. Поля — в секции **Radar**, применяются кнопкой
|
||||
**Apply Radar**.
|
||||
|
||||
| Ключ | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `enabled` | `false` | программировать плату при **Apply Radar** |
|
||||
| `port` | `""` | последовательный порт платы, 115 200 бод |
|
||||
| `mode` | `"manual"` | `"manual"` — постоянные токи и температуры, `"variation"` — развёртка тока LD1 (плате RFG нужен этот режим) |
|
||||
| `pi_coeff1_p`, `pi_coeff1_i` | `1280`, `25` | коэффициенты ПИ-регулятора первого канала |
|
||||
| `pi_coeff2_p`, `pi_coeff2_i` | `1280`, `179` | коэффициенты ПИ-регулятора второго канала |
|
||||
| `manual.temp1`, `manual.temp2` | `24.0`, `24.9` | температуры лазеров, °C, 15–40 |
|
||||
| `manual.current1`, `manual.current2` | `97.3`, `102.0` | токи лазеров, мА, 15–110 |
|
||||
| `variation.variation_type` | `"CHANGE_CURRENT_LD1"` | что меняется; другого варианта нет |
|
||||
| `variation.static_temp1`, `static_temp2` | `24.0`, `24.9` | температуры во время развёртки |
|
||||
| `variation.static_current1`, `static_current2` | `97.3`, `102.0` | постоянные токи во время развёртки |
|
||||
| `variation.min_value`, `max_value` | `97.3`, `107.0` | пределы развёртки тока LD1, мА, начало меньше конца |
|
||||
| `variation.step` | `0.05` | шаг тока, мА, 0,002–0,5 |
|
||||
| `variation.time_step` | `50` | длительность ступени, мкс, 20–100 |
|
||||
| `variation.delay_time` | `10` | пауза между развёртками, мс, 3–10 |
|
||||
|
||||
## switches
|
||||
|
||||
Два ВЧ-ключа: `port1` — выходной (передающие антенны), `port2` — входной (приёмные).
|
||||
Таблица положений — в главе [Оборудование](hardware.md).
|
||||
|
||||
| Ключ | Тип, по умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `name` | строка, `""` | имя ключа в сообщениях и в GPIO (`radar_<имя>`) |
|
||||
| `driver_mode` | `"mock"` | `"native"` — управлять линиями GPIO, `"mock"` — только помнить положение |
|
||||
| `radar_port` | целое, `0` | порт прибора, к которому подключён ключ: 1 или 2. Задайте его явно: `0` не проходит проверку при **Start** (`switches.<имя>.radar_port must be 1 or 2`) |
|
||||
| `positions` | `1` | число положений, у настоящего ключа 1–4. Задаёт число выходов или входов матрицы |
|
||||
| `default_position` | `0` | положение после открытия ключа, меньше `positions` |
|
||||
| `gpio_chip` | `""` | GPIO-чип, например `/dev/gpiochip0`; для `native` обязателен |
|
||||
| `pin_a`, `pin_b` | `-1` | номера линий A и B на чипе; для `native` — разные и неотрицательные |
|
||||
|
||||
Поле `driver` допускается только со значением `"h7992"`, `invert_logic` — только
|
||||
`false`; оба при сохранении убираются. Другие значения отвергаются ещё до обращения к
|
||||
GPIO. У многоприборной схемы программа сама переписывает в этих разделах `driver_mode`
|
||||
(`"mock"`), `radar_port`, `positions` (положения настоящего ключа × выходы или входы
|
||||
приборов) и `default_position` (`0`): значения из файла не используются. Читаются
|
||||
только `name`, `gpio_chip`, `pin_a` и `pin_b` — для настоящих ключей на выходах мастера
|
||||
или входах ведомых, если `multi_device.output_switch_positions` или
|
||||
`input_switch_positions` больше 1; их режим берётся из `radar.driver_mode`. Изменения
|
||||
применяются при **Start**;
|
||||
число положений и режим перезапускают процессор.
|
||||
|
||||
## control_button
|
||||
|
||||
GPIO-кнопка, которая снимает временный референс. Настройки читаются один раз при
|
||||
запуске программы.
|
||||
|
||||
| Ключ | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `enabled` | `false` | включить кнопку |
|
||||
| `gpio_chip` | `"/dev/gpiochip0"` | GPIO-чип |
|
||||
| `pin` | `-1` | номер линии на чипе (не номер контакта разъёма); отрицательный — кнопка не включается |
|
||||
| `active_low` | `true` | `true` — нажатие опускает линию в 0 (кнопка на землю), `false` — поднимает в 1 |
|
||||
| `bias` | `""` | подтяжка: `"pull_up"`, `"pull_down"`, `"disabled"`; пусто — к питанию при `active_low`, к земле иначе |
|
||||
| `debounce_ms` | `50` | подавление дребезга средствами ядра, мс; 0 — без подавления |
|
||||
| `action` | `"capture_tmp_reference"` | действие по нажатию; другого нет |
|
||||
|
||||
## logging
|
||||
|
||||
| Ключ | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `level` | `"info"` | наименьший уровень записей журнала: `debug`, `info`, `warning` или `error`. Меняется списком **Level** над журналом окна; записывается при **Save Config…** |
|
||||
|
||||
На уровне `debug` в журнал попадают трассы тайминга переключений; корневой профиль
|
||||
работает с ним.
|
||||
|
||||
## run
|
||||
|
||||
| Ключ | Тип, по умолчанию | Что делает | Когда |
|
||||
| --- | --- | --- | --- |
|
||||
| `settling_ms` | целое, `0` | пауза после переключения ВЧ-ключей перед развёрткой, мс. Поле **Settling (ms)** | Start |
|
||||
| `idle_sleep_ms` | целое, `2` | сколько процессы предобработки и обработки спят, когда нет входных данных, мс | Start, процессор |
|
||||
| `continuous` | логическое, `false` | непрерывный сбор или одна коллекция. Программа сама ставит `true` при **Start** и `false` при **Single Capture**; значение из профиля важно только для `./start.sh --producer-only` | — |
|
||||
| `processing_live_config_path` | строка, `""` | путь к live-файлу. Программа всегда подставляет `python_app/runtime/processing_live.json` своего каталога | — |
|
||||
| `combos` | список `{"input", "output"}`, `[]` | пары сбора в порядке обхода (ниже) | Start, процессор |
|
||||
| `locator_server` | объект | TCP-локатор (ниже) | Start, процессор |
|
||||
|
||||
### run.combos
|
||||
|
||||
Список пар `{"input": вход, "output": выход}`; пустой список — полная матрица:
|
||||
выходы по порядку, внутри каждого — все входы. Пар не больше 4096, повторы и номера
|
||||
вне матрицы отвергаются.
|
||||
|
||||
- Одиночный LibreVNA и радиофотонный радар: при **Start** список берётся из окна —
|
||||
**Run combos** (`вход:выход` через запятую, пусто — полная матрица) или
|
||||
**Single combo**. В режимах pass-through и B-scan, если оба ключа `mock` или у обоих
|
||||
не больше одного положения, программа снимает только пару `0:0`.
|
||||
- Многоприборная схема: список всегда равен полной матрице; в окне он показан, но не
|
||||
редактируется.
|
||||
|
||||
### run.locator_server
|
||||
|
||||
TCP-сервер, который отдаёт объекты GPR внешним программам и принимает от них
|
||||
скорость (протокол — в главе [Веб-интерфейс, кнопка и локатор](web.md)). В окне не
|
||||
редактируется. Старое имя раздела `run.locator` тоже читается.
|
||||
|
||||
| Ключ | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `host` | `"0.0.0.0"` | адрес, на котором сервер ждёт клиентов; `0.0.0.0` — все интерфейсы |
|
||||
| `port` | `8888` | TCP-порт, 1–65535 |
|
||||
| `device_id` | `3` | номер устройства в заголовке каждого пакета |
|
||||
| `protocol_version` | `1` | поле `ver` в пакетах |
|
||||
| `max_payload_bytes` | `65536` | наибольший размер входящего пакета; клиент с большим пакетом отключается |
|
||||
| `client_queue_size` | `32` | очередь пакетов на клиента; при переполнении выбрасываются старые |
|
||||
| `logger_name` | `"locator_runtime"` | не используется |
|
||||
|
||||
Ключ `enabled` в этом разделе программа не сохраняет: локатор включён всегда.
|
||||
|
||||
## preprocess
|
||||
|
||||
| Ключ | Что делает |
|
||||
| --- | --- |
|
||||
| `s21.calibration.set_name` | имя выбранного набора калибровки (through) |
|
||||
| `s21.reference.set_name` | имя выбранного набора референса |
|
||||
| `s21.object_calibration.set_name` | имя выбранной калибровки по объекту; пусто — не применять |
|
||||
| `s21.*.bundle_path` | путь к файлу набора для препроцессора; программа заполняет его сама при **Start**, в профиле — пусто |
|
||||
| `notch.enabled` | включить режекторный фильтр, по умолчанию `false` |
|
||||
| `notch.bands_hz` | полосы подавления `[нижняя, верхняя]`, Гц, по умолчанию `[]` |
|
||||
| `notch.taper_width_hz` | ширина плавного края, Гц, не меньше 0, по умолчанию `40000000.0` |
|
||||
| `notch.taper_type` | форма края: `"cosine"` (по умолчанию) или `"hard"`; как работает фильтр — в главе [Калибровка и предобработка](preprocessing.md) |
|
||||
|
||||
Наборы выбираются в окне **Preprocessing Setup** и записываются в профиль при
|
||||
**Save Config…**. Изменения применяются при **Start**.
|
||||
|
||||
## gpr
|
||||
|
||||
Геометрия антенн для GPR и 3D SAR и для симулятора. Редактируется таблицами
|
||||
**Tx geometry** и **Rx geometry** на страницах GPR и 3D SAR; правка таблицы сразу
|
||||
уходит процессору.
|
||||
|
||||
| Ключ | Запись | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `tx_geometry` | `{"output_pos", "x_m", "y_m", "z_m"}` | положения передающих антенн: номер выхода и координаты в осях радара, м |
|
||||
| `rx_geometry` | `{"input_pos", "x_m", "y_m", "z_m"}` | положения приёмных антенн: номер входа и координаты, м |
|
||||
|
||||
Записей может быть меньше, чем выходов или входов: антенны без записи в GPR и 3D SAR
|
||||
не участвуют. Номер должен быть меньше числа выходов (входов), без повторов, иначе
|
||||
`gpr.tx_geometry output_pos is out of range [0, N)`. При сужении матрицы лишние
|
||||
записи нужно удалить. Отсутствующие `y_m` и `z_m` считаются нулями. Оси — в главе
|
||||
[Как работает радар](principles.md).
|
||||
|
||||
## rings
|
||||
|
||||
Пять колец разделяемой памяти, через которые процессы передают данные
|
||||
(глава [Устройство программы](architecture.md)):
|
||||
|
||||
| Кольцо | Кто пишет | Кто читает |
|
||||
| --- | --- | --- |
|
||||
| `raw` | процесс сбора | препроцессор |
|
||||
| `raw_tap` | процесс сбора, копия | окно программы |
|
||||
| `preprocessed` | препроцессор | процессор |
|
||||
| `preprocessed_tap` | препроцессор, копия | окно программы |
|
||||
| `results` | процессор | окно программы |
|
||||
|
||||
| Ключ | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `name` | `""` | имя сегмента разделяемой памяти, например `/radar_raw`. Пустое имя у `raw_tap` или `preprocessed_tap` отключает копию и соответствующую историю окна |
|
||||
| `capacity` | `1` | сколько кадров помещается в кольцо; при переполнении затирается самый старый |
|
||||
| `slot_size_bytes` | `4096` | наибольший размер кадра, байт, кратный 8 |
|
||||
|
||||
Слоты `raw`, `preprocessed` и включённых копий должны вмещать коллекцию:
|
||||
44 + пары × (28 + 12 × точки) байт. Например, 16 пар по 1031 точке требуют 198 444
|
||||
байт. Программа проверяет это до запуска
|
||||
(`rings.raw.slot_size_bytes must be at least N for this sweep`). Слот `results` должен
|
||||
вмещать результат; изображения 3D SAR при настройках по умолчанию занимают около
|
||||
1 МБ. Во всех примерах слоты по 2 МиБ (`2097152`) и ёмкость 50.
|
||||
|
||||
Ёмкость влияет и на историю: процессор хранит до 50 коллекций, но не больше
|
||||
ёмкости `preprocessed` и `results`, а B-scan во время сбора показывает столько же. В
|
||||
GPR и 3D SAR процессор при этом хранит не меньше, чем нужно обработке: 1 +
|
||||
**Mean count** (если включено вычитание среднего) + **SAR_FRAME_RADIUS** (в 3D SAR),
|
||||
независимо от ёмкости колец.
|
||||
Калибровке по объекту нужно кольцо `preprocessed_tap`.
|
||||
|
||||
## gui
|
||||
|
||||
Состояние окна: значения всех полей, которые программа запоминает. Раздел
|
||||
записывается целиком при **Save Config…**. Если его нет, программа берёт значения по
|
||||
умолчанию, а часть вычисляет из профиля: номера антенн — из геометрии и списка пар,
|
||||
окно видимости GPR — из положения антенн, каталог сохранения —
|
||||
`python_app/data/snapshots`.
|
||||
|
||||
| Ключ | Поле в окне | Смысл |
|
||||
| --- | --- | --- |
|
||||
| `version` | — | версия раздела, всегда `1` |
|
||||
| `switches.combo_mode` | кнопки **Select** у **Run combos** и **Single combo** | откуда брать пары сбора: `"text"` или `"single"` |
|
||||
| `switches.combos_text` | **Run combos** | список пар `вход:выход` |
|
||||
| `switches.single_input`, `single_output` | **Input**, **Output** в строке **Single combo** | одна пара |
|
||||
| `processing.selected_mode` | **Mode** | режим обработки: `pass_through`, `bscan`, `gpr`, `sar3d` |
|
||||
| `processing.pass_through.*` | страница pass-through | `show_magnitude`, `show_phase`, `unwrap_phase`, `combo_filter`, `fixed_y_enabled`, `y_min_db`, `y_max_db` |
|
||||
| `processing.bscan.*` | страница B-scan | `axis`, `cut_m`, `max_depth_m`, `gain`, `start_freq_mhz`, `stop_freq_mhz`, `subtract_mean_ascan`, `history_window_scans` |
|
||||
| `processing.gpr.*` | страница GPR | `input_positions`, `output_positions`, `min_depth_m`, `max_depth_m`, `range_comp_power`, `object_min_frac`, `score_mode`, `motion_mode`, `look_angle_deg`, `direction_sign`, `speed_m_s`, `ignore_socket_speed_enabled`, `max_detected_objects_to_draw`, `draw_top_m_objects`, `object_approach_min_frames`, `start_freq_mhz`, `stop_freq_mhz`, `background_subtract_enabled`, `background_mean_count`, `remove_sidelobe_objects_enabled`, `bp_ground_depth_m`, `pair_depth_equalize`, `render_mode`, `visible_x_min_m`, `visible_x_max_m`, `visible_z_min_m`, `visible_z_max_m` |
|
||||
| `processing.sar3d.*` | страница 3D SAR | `input_positions`, `output_positions`, `min_depth_m`, `max_depth_m`, `start_freq_mhz`, `stop_freq_mhz`, `speed_m_s`, `ignore_socket_speed_enabled`, `look_angle_deg`, `frame_radius`, `voxel_x_m`, `voxel_y_m`, `voxel_z_m`, `bp_ground_depth_m`, `comp_range_power`, `use_compensation`, `normalize_each_frame`, `pair_depth_equalize`, `background_subtract_enabled`, `background_mean_count`, `bg_gate_enabled`, `clean_enabled`, `x_size_m`, `y_min_m`, `y_max_m`, `z_size_m`, `cloud_threshold`, `zoom_percent`, `mark_enabled` |
|
||||
| `data_actions.save_path` | **Path** | каталог для сохранения данных; относительный путь — от рабочего каталога процесса |
|
||||
| `data_actions.save_name` | **Name** | имя сохраняемого набора данных, по умолчанию `snapshot_manual` |
|
||||
| `data_actions.record_count` | **Sweeps to record** | сколько коллекций записать на диск, по умолчанию 100 |
|
||||
| `preprocess_dialog.set_name` | **Set name** | имя нового набора, по умолчанию `set_001` |
|
||||
| `preprocess_dialog.radar_config_dir` | **Directory** | каталог вариантов радара |
|
||||
| `preprocess_dialog.use_all_radar_configs` | **Capture current combo for all found radar configs** | съём по всем вариантам |
|
||||
| `preprocess_dialog.median_sweep_count` | **Sweeps per combo (median)** | развёрток на пару, по умолчанию 5 |
|
||||
|
||||
Ключи страниц обработки названы по полям окна; что делает каждое поле, сколько оно по
|
||||
умолчанию и в каких пределах — в главе [Режимы обработки](processing.md). Типы,
|
||||
варианты выбора, объявленные в схеме границы и версия проверяются при чтении, и
|
||||
нарушение отвергает весь профиль: `gui.processing.gpr.score_mode must be one of: peak, combined`,
|
||||
`gui.processing.sar3d.zoom_percent must be <= 1000`. Значение в пределах схемы, но вне
|
||||
пределов поля окна поле молча подрезает к своей границе (например, `frame_radius: 1000`
|
||||
станет 24), и подрезанное значение запишется при следующем **Save Config…**.
|
||||
`preprocess_dialog.median_sweep_count` меньше 1 поднимается до 1.
|
||||
|
||||
## processing_live.json
|
||||
|
||||
Живые настройки процессора. Файл `python_app/runtime/processing_live.json` пишет
|
||||
программа при каждом изменении настройки обработки, смене режима, правке геометрии,
|
||||
очистке истории и удалении последнего измерения; читает его только процессор. Править
|
||||
его вручную не нужно: программа перезаписывает его целиком.
|
||||
|
||||
В файле 53 ключа. Ключи настроек обработки соответствуют полям окна:
|
||||
|
||||
| Ключ | Поле |
|
||||
| --- | --- |
|
||||
| `processor_mode` | **Mode** |
|
||||
| `bscan_axis`, `bscan_cut_m`, `bscan_max_depth_m`, `bscan_gain`, `bscan_start_freq_mhz`, `bscan_stop_freq_mhz` | поля B-scan |
|
||||
| `gpr_input_positions`, `gpr_output_positions`, `gpr_min_depth_m`, `gpr_max_depth_m`, `gpr_start_freq_mhz`, `gpr_stop_freq_mhz`, `gpr_speed_m_s`, `gpr_look_angle_deg`, `ignore_socket_speed`, `gpr_background_subtract_enabled`, `gpr_background_mean_count`, `pair_depth_equalize` | общие поля GPR и 3D SAR — со страницы выбранного режима |
|
||||
| `gpr_range_comp_power`, `gpr_object_min_frac`, `gpr_score_mode`, `gpr_motion_mode`, `gpr_direction_sign`, `gpr_max_detected_objects_to_draw`, `gpr_draw_top_m_objects`, `gpr_object_approach_min_frames`, `gpr_remove_sidelobe_objects_enabled`, `gpr_bp_ground_depth_m`, `gpr_visible_x_min_m`, `gpr_visible_x_max_m`, `gpr_visible_z_min_m`, `gpr_visible_z_max_m` | поля GPR |
|
||||
| `sar3d_frame_radius`, `sar3d_voxel_x_m`, `sar3d_voxel_y_m`, `sar3d_voxel_z_m`, `sar3d_bp_ground_depth_m`, `sar3d_comp_range_power`, `sar3d_use_compensation`, `sar3d_normalize_each_frame`, `sar3d_x_size_m`, `sar3d_y_min_m`, `sar3d_y_max_m`, `sar3d_z_size_m`, `sar3d_bg_gate_enabled`, `sar3d_clean_enabled` | поля 3D SAR |
|
||||
| `tx_geometry`, `rx_geometry` | таблицы геометрии; заменяют `gpr` профиля в процессоре |
|
||||
|
||||
Служебные ключи:
|
||||
|
||||
| Ключ | Что делает |
|
||||
| --- | --- |
|
||||
| `settings_revision` | номер ревизии настроек (время записи по монотонным часам, нс). Процессор возвращает его в каждом результате, и окно рисует GPR и 3D SAR только текущей ревизии |
|
||||
| `reprocess_current_result` | пересчитывать ли сохранённые коллекции при новой ревизии; `false` — пока окно само пересчитывает остановленный B-scan |
|
||||
| `history_clear_before_ns` | граница **Clear Runtime History**: коллекции, начатые не позже этого момента, удаляются из истории процессора |
|
||||
| `history_removed` | пары `{"collection_id", "capture_start_ns"}`, удалённые **Remove Last Measurement**; список очищается при **Clear Runtime History** |
|
||||
|
||||
Процессор отвергает файл целиком, если в нём ошибка JSON, число не того типа или вне
|
||||
float32, неизвестное значение `processor_mode`, `gpr_score_mode` или
|
||||
`gpr_motion_mode`, геометрия с номером вне матрицы или с повтором, отрицательные
|
||||
`sar3d_x_size_m` или `sar3d_z_size_m`, `sar3d_y_max_m` не больше `sar3d_y_min_m`.
|
||||
Тогда он работает с последними принятыми настройками и пишет
|
||||
`data_processor warning: failed to refresh live processing config: …` в
|
||||
`python_app/runtime/logs/data_processor.err.log`. Неизвестные ключи пропускаются.
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
# Данные на диске
|
||||
|
||||
Программа хранит данные в трёх местах: наборы предобработки и наборы данных — в
|
||||
`python_app/data/` (или в каталоге, выбранном в **Path**), своё рабочее состояние — в
|
||||
`python_app/runtime/`, обмен между процессами — в разделяемой памяти `/dev/shm`.
|
||||
Каталоги `python_app/data*` и `python_app/runtime` в git не попадают.
|
||||
|
||||
> **Внимание.** Наборы калибровки и референса в `python_app/data/` — незаменимые
|
||||
> измерения. При обновлении программы их переносит `update.sh`; при ручном переносе
|
||||
> копируйте этот каталог целиком.
|
||||
|
||||
## Где что лежит
|
||||
|
||||
| Путь | Что | Кто пишет |
|
||||
| --- | --- | --- |
|
||||
| `python_app/data/s21_calibration/<radar_key>/<имя>.npz`, `.json` | наборы калибровки | окно предобработки |
|
||||
| `python_app/data/s21_reference/<radar_key>/<имя>.npz`, `.json` | наборы референса, в том числе `tmp_reference` | окно предобработки, временный референс |
|
||||
| `python_app/data/s21_object_calibration/<radar_key>/<имя>.npz`, `.json` | калибровки по объекту | окно калибровки по объекту |
|
||||
| `python_app/data/preview_png/<вид>/<имя>/<radar_key>/i<вход>_o<выход>.png` | картинки развёрток при съёме по вариантам радара | окно предобработки |
|
||||
| `<Path>/<набор данных>/` | снимки и записи на диск; по умолчанию `python_app/data/snapshots` | **Save Last 1000**, **Save S21 JSON**, **Start + Record to Disk** |
|
||||
| `python_app/runtime/` | рабочее состояние программы (ниже) | программа |
|
||||
| `/dev/shm/<имя кольца>` | кольца разделяемой памяти | процессы конвейера |
|
||||
| `/tmp/radar_system.lock` | блокировка единственного экземпляра | `start.sh` |
|
||||
| `radar.rph_radar.tty_path`, например `/tmp/ttyADC_data` | ссылка на псевдотерминал коллектора E-502 | `rph_radar_collector` |
|
||||
| `/etc/udev/rules.d/99-radar-librevna.rules` | доступ к LibreVNA по USB | `start.sh` |
|
||||
| `/etc/systemd/system/radar.service`, `/etc/sudoers.d/radar` | служба и право её перезапускать | `deploy/install-daemon.sh` |
|
||||
|
||||
Все файлы JSON и наборы программа записывает через временный файл и замену, а важные
|
||||
ещё и сбрасывает на диск (fsync). Оборванная запись оставляет прежний файл целым;
|
||||
от неё может остаться временный файл вида `.<имя>.<32 знака>.tmp`, а от оборванного
|
||||
сохранения набора данных — каталог `.<имя набора>.<32 знака>`; их можно удалить.
|
||||
|
||||
## Наборы предобработки
|
||||
|
||||
Набор лежит в каталоге своего вида и ключа радара (глава
|
||||
[Файл конфигурации](configuration.md)) и состоит из двух файлов:
|
||||
|
||||
| Файл | Содержимое |
|
||||
| --- | --- |
|
||||
| `<имя>.npz` | архив NumPy без сжатия: для каждой пары `freq_i<вход>_o<выход>` (частоты, float32) и `s21_i<вход>_o<выход>` (S21, complex64), плюс `metadata_json` — описание набора |
|
||||
| `<имя>.json` | то же описание в читаемом виде |
|
||||
|
||||
Описание: `collection_id`, `monotonic_ns`, `capture_start_ns`, `capture_end_ns`,
|
||||
список `combos` (вход, выход, окно съёма, имена массивов) и `metadata`. У калибровки
|
||||
по объекту в `metadata` записано, при каких условиях она снята: ключ радара,
|
||||
геометрия, имя и хеш набора калибровки, настройки расчёта, координаты отражателя,
|
||||
параметры антенн, диагностика фаз и пары без поправки (`unchanged_combos`).
|
||||
|
||||
Старые наборы без `metadata_json` читаются по файлу `.json`; лишние массивы (например,
|
||||
S11) пропускаются.
|
||||
|
||||
Имя набора — любая строка, кроме пустой, `.`, `..` и строк с `/` или `\`. Существующее
|
||||
имя программа не перезаписывает; исключение — `tmp_reference`, который заменяется при
|
||||
каждом съёме. Перед записью проверяется, что пары не повторяются, массивы одной длины
|
||||
и значения конечны.
|
||||
|
||||
## Наборы данных
|
||||
|
||||
Набор данных — каталог со снимком истории (**Save Last 1000**) или записью на диск
|
||||
(**Start + Record to Disk**). Имя каталога — `<префикс>_<Name>`, например
|
||||
`librevna_multi_0.001-6GHz_201pts_-10dBm_run_A`; символы кроме латиницы, цифр, `.`,
|
||||
`_` и `-` заменяются на `_`. Префикс: модель, полоса в единицах большей границы,
|
||||
число точек (у радиофотонного радара — `adc`) и мощность. Пустое имя —
|
||||
`snapshot_<ГГГГММДД>_<ЧЧММСС>` по UTC. Существующий каталог программа не трогает.
|
||||
|
||||
```text
|
||||
<набор данных>/
|
||||
config_profile.json профиль, с которым сняты данные
|
||||
processing_settings.json ревизии настроек обработки
|
||||
raw/ сырые коллекции
|
||||
0000_id<номер>_ns<время>/
|
||||
preprocessed/ предобработанные коллекции
|
||||
results/ результаты обработки
|
||||
```
|
||||
|
||||
Коллекции внутри стадии нумеруются с нуля. Время в имени каталога — метка своей
|
||||
стадии, поэтому у одной коллекции оно разное в `raw`, `preprocessed` и `results`.
|
||||
Сопоставляйте стадии по паре `collection_id` и `capture_start_ns` из `meta.json`.
|
||||
|
||||
Каталог коллекции в `raw/` и `preprocessed/`:
|
||||
|
||||
| Файл | Содержимое |
|
||||
| --- | --- |
|
||||
| `i<вход>_o<выход>_freq.npy` | частоты, float32 |
|
||||
| `i<вход>_o<выход>_s21.npy` | S21, complex64 |
|
||||
| `meta.json` | `collection_id`, `monotonic_ns`, `capture_start_ns`, `capture_end_ns`, `trace_count`, список трасс с их окнами съёма и именами файлов |
|
||||
|
||||
Каталог коллекции в `results/` содержит `meta.json` (номер, время обработки,
|
||||
`settings_revision`, окно съёма, использованная скорость `speed_m_s`, список
|
||||
результатов) и массивы результатов. Результаты всей коллекции — файлы
|
||||
`collection_<NNN>_<имя>_kind<вид>_<поле>.npy`, результаты пары — в подкаталоге
|
||||
`block_<NNN>_i<вход>_o<выход>/`.
|
||||
|
||||
| Вид | Поля |
|
||||
| --- | --- |
|
||||
| 1 — трасса | `freq` float32, `trace` complex64 |
|
||||
| 2 — число | `scalar` float32 |
|
||||
| 3 — изображение | `x_axis`, `y_axis` float32, `image` float32 размером (строки по y, столбцы по x) |
|
||||
| 4 — таблица | `table` float32 (строки, столбцы) |
|
||||
|
||||
| Режим | Что сохраняется |
|
||||
| --- | --- |
|
||||
| pass-through | для каждой пары `pass_through` (вид 1): частота и S21 |
|
||||
| B-scan | для каждой пары `bscan` (вид 1): в поле `freq` — дальность в метрах, в `trace` — значение (мнимая часть 0) |
|
||||
| GPR | `gpr_accumulator` (вид 3): карта, оси X и Z; `gpr_points` (вид 4): строки `[x_m, z_m, score]` |
|
||||
| 3D SAR | `sar3d_front`, `sar3d_side`, `sar3d_top`, `sar3d_oblique` (вид 3); `sar3d_oblique_basis`; `sar3d_peak` `[уровень, X, Y, Z]`; `sar3d_background` `[отношение, 6, 8.5]`; `sar3d_objects` `[X, Y, Z, уровень]` и `sar3d_marks` `[класс, X0, X1, Y0, Y1, Z0, Z1]` или `sar3d_target_width` `[X, Y, Z]`; `sar3d_status` `[код, байты текста UTF-8]` |
|
||||
|
||||
Классы рамок 3D SAR: 0 — очень низко, 1 — на земле, 2 — над землёй, 3 — у края. Коды
|
||||
состояния 3D SAR: 0 — посчитано, 1 — нет сигнала, 2 — ошибка, 3 — нет CUDA.
|
||||
|
||||
`processing_settings.json` — словарь «номер ревизии → настройки обработки» в формате
|
||||
`processing_live.json`: по `settings_revision` из `meta.json` результата видно, с
|
||||
какими настройками он посчитан. При ручном сохранении туда попадают все ревизии
|
||||
текущего сеанса, при записи — только использованные. `config_profile.json` — полный
|
||||
профиль; разделы радара, ключей, пар, предобработки и геометрии взяты из конфигурации,
|
||||
с которой шёл сбор, даже если потом загрузили другой профиль.
|
||||
|
||||
### Запись на диск
|
||||
|
||||
**Start + Record to Disk** создаёт каталог сразу и пишет в него коллекции, снятые
|
||||
после нажатия; счётчик считает результаты. Данные копятся порциями по 200 коллекций и
|
||||
уходят в очередь записи на 8 порций; каждая коллекция появляется на диске целиком.
|
||||
Если диск не успевает и очередь заполнилась, запись останавливается с
|
||||
`Disk recording stopped because the writer could not keep up`; записанное остаётся.
|
||||
Если сбор так и не начался и ничего не пришло, каталог удаляется.
|
||||
|
||||
### Экспорт S21 JSON
|
||||
|
||||
**Save S21 JSON** создаёт в `<Path>/<набор данных>/` по файлу на пару
|
||||
`<имя набора>_i<вход>_o<выход>_s21_vna_bscan_history.json` и профиль
|
||||
`vna_config_profile.json`. Основные поля файла:
|
||||
|
||||
| Поле | Что |
|
||||
| --- | --- |
|
||||
| `format` | `"vna-system-history-v1"` |
|
||||
| `input_index`, `output_index`, `channel` | пара и канал (`"s21"`) |
|
||||
| `primary_stage` | `"preprocessed"` |
|
||||
| `sweep_history` | список коллекций: `timestamp`, `sweep_points` (сырая развёртка, пары `[re, im]`), `calibrated_points` (предобработанная), `reference_points` (всегда пусто), `vna_config` (начало, конец, число точек) |
|
||||
|
||||
`timestamp` — время конца съёма развёртки этой пары (если его нет — коллекции) в
|
||||
секундах монотонных часов компьютера (отсчёт от его загрузки), а не дата и время. Сырая и
|
||||
предобработанная развёртки
|
||||
сопоставляются по паре номер — начало съёма; если пары нет, массив пуст.
|
||||
|
||||
### Как открыть данные в Python
|
||||
|
||||
```python
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
# Набор предобработки
|
||||
with np.load("python_app/data/s21_reference/<radar_key>/<имя>.npz", allow_pickle=False) as archive:
|
||||
meta = json.loads(str(archive["metadata_json"].item()))
|
||||
for combo in meta["combos"]:
|
||||
frequency, s21 = archive[combo["freq_key"]], archive[combo["s21_key"]]
|
||||
|
||||
# Предобработанные коллекции набора данных
|
||||
from python_app.storage.npz.vna_history_json import collection_directories
|
||||
|
||||
for directory in collection_directories(Path("<набор данных>") / "preprocessed"):
|
||||
meta = json.loads((directory / "meta.json").read_text())
|
||||
for trace in meta["traces"]:
|
||||
frequency = np.load(directory / trace["freq_file"])
|
||||
s21 = np.load(directory / trace["s21_file"])
|
||||
|
||||
# Результаты (например, карта GPR)
|
||||
for directory in collection_directories(Path("<набор данных>") / "results"):
|
||||
meta = json.loads((directory / "meta.json").read_text())
|
||||
for payload in meta["collection_payloads"]:
|
||||
if payload["name"] == "gpr_accumulator":
|
||||
image = np.load(directory / payload["image_file"])
|
||||
```
|
||||
|
||||
`collection_directories` возвращает каталоги коллекций по порядку номеров. Загрузить
|
||||
набор данных обратно в окно программы нельзя: он предназначен для обработки
|
||||
отдельно.
|
||||
|
||||
## Рабочие файлы
|
||||
|
||||
Каталог `python_app/runtime/` программа создаёт и ведёт сама.
|
||||
|
||||
| Файл | Что | Можно удалить |
|
||||
| --- | --- | --- |
|
||||
| `gui_session_state.json` | путь последнего профиля: с него программа стартует | да; программа стартует с корневого профиля. Это и способ выйти из ситуации, когда запомненный профиль не даёт программе запуститься |
|
||||
| `processing_live.json` | живые настройки процессора (глава [Файл конфигурации](configuration.md)) | да; программа запишет его заново |
|
||||
| `run_config.json` | рабочая копия профиля последнего **Start** | при остановленном сборе |
|
||||
| `s21_calibration_bundle.bin`, `s21_reference_bundle.bin`, `s21_object_calibration_bundle.bin` | выбранные наборы в формате для препроцессора, пересобираются при каждом **Start** | при остановленном сборе |
|
||||
| `supervisor_children.pids` | номера процессов конвейера: если программа упала, следующий запуск завершает оставшиеся | когда процессы конвейера не работают |
|
||||
| `logs/` | журналы (ниже) | при закрытой программе |
|
||||
|
||||
## Журналы
|
||||
|
||||
| Где | Что |
|
||||
| --- | --- |
|
||||
| `python_app/runtime/logs/radar.log` и `radar.log.1` … `radar.log.5` | журнал программы: то же, что в окне журнала, строки `дата время \| уровень \| источник \| сообщение`. Файл до 2 МиБ, хранятся пять предыдущих |
|
||||
| `python_app/runtime/logs/acquisition.out.log`, `acquisition.err.log` | вывод процесса сбора, в том числе ожидание приборов и сообщения коллектора E-502 (`Collector: …`) |
|
||||
| `python_app/runtime/logs/data_preprocessor.*.log`, `data_processor.*.log` | вывод препроцессора и процессора: отброшенные коллекции, отклонённый live-файл, сообщения 3D SAR (`sar3d: …`) и локатора (`locator: …`) |
|
||||
| `*.prev` рядом | вывод прошлого запуска процесса или начало текущего, если файл превысил 8 МиБ |
|
||||
| `journalctl -u radar.service` | при работе службой: вывод программы, включая строки `[start.sh] …` |
|
||||
|
||||
Уровень записей задаёт `logging.level` профиля или список **Level** в окне.
|
||||
Подробности ошибки, показанной окном (например, трассировка вызовов), видны в окне
|
||||
ошибки и в окне журнала, а в `radar.log` попадает само сообщение. Если процесс конвейера
|
||||
завершился сам,
|
||||
отчёт с последними 16 КиБ его вывода попадает в журнал программы. Раз в 50 замеров
|
||||
программа пишет строку `metrics: <стадия> n=… avg=…ms min=…ms max=…ms` со временем
|
||||
сбора, обработки и отрисовки.
|
||||
|
||||
## Разделяемая память
|
||||
|
||||
Кольца лежат в `/dev/shm` под именами из раздела `rings` профиля (без начальной `/`).
|
||||
При ёмкости 50 и слоте 2 МиБ кольцо занимает около 100 МиБ виртуальной памяти, но
|
||||
страницы выделяются только по мере записи. Кольца создаёт первый запустивший их
|
||||
процесс с правами 0660 и при выходе не удаляет. `./start.sh --clean-shm` удаляет
|
||||
кольца `/radar_raw`, `/radar_raw_tap`, `/radar_preprocessed`, `/radar_preprocessed_tap`,
|
||||
`/radar_results` и те же имена с окончанием `_rph_radar`. Кольца с окончанием
|
||||
`_simulator` он не трогает, а их используют корневой профиль, оба профиля симулятора и
|
||||
оба примера с переключением стимула (`run_config_librevna_multi_per_point*.example.json`).
|
||||
Их удаляйте вручную при закрытой программе: `rm /dev/shm/radar_*_simulator`.
|
||||
|
||||
> **Внимание.** Если программу хоть раз запустили от root (например, тесты через
|
||||
> `sudo`), кольца станут принадлежать root, и обычный пользователь получит отказ в
|
||||
> доступе. Удалите их при закрытой программе: `sudo rm -f /dev/shm/radar_*`.
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
# Окно программы
|
||||
|
||||
Окно называется **Radar System Control** и открывается развёрнутым на весь экран.
|
||||
Слева — графики выбранного режима обработки, справа — панель управления. Работу ведут
|
||||
несколько процессов — конвейер: процесс сбора управляет железом, препроцессор
|
||||
калибрует данные, процессор их обрабатывает и держит TCP-локатор для внешних программ;
|
||||
между собой они передают данные через кольца в разделяемой памяти (главы
|
||||
[Устройство программы](architecture.md) и [Словарь](glossary.md)).
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────┬─┬──────────────────────────────────┐
|
||||
│ │ │ Actions (не прокручивается)│
|
||||
│ │ │ ┌──────────────────────────────┐ │
|
||||
│ графики │›│ │ Data Actions │ │
|
||||
│ (вид зависит от режима) │ │ │ Switches │ │
|
||||
│ │ │ │ Selected Preprocess Sets │ │
|
||||
│ │ │ │ Processing │ │
|
||||
│ │ │ │ Radar │ │
|
||||
│ │ │ └──────────────── прокрутка ───┘ │
|
||||
│ │ │ Status: … History: … │
|
||||
│ │ │ Runtime Log Level ▾ │
|
||||
│ │ │ [журнал] │
|
||||
└──────────────────────────────────────┴─┴──────────────────────────────────┘
|
||||
```
|
||||
|
||||
Стрелка между графиками и панелью (подсказка **Hide settings panel** или
|
||||
**Show settings panel**) прячет и показывает всю панель; графики тогда занимают всю ширину.
|
||||
Положение
|
||||
стрелки не запоминается. Секция **Actions** со списком **Mode** всегда на месте,
|
||||
остальные секции прокручиваются. Меню, панели инструментов и сочетаний клавиш нет.
|
||||
|
||||
Многие действия приостанавливают сбор: останавливают процесс сбора и препроцессор, но
|
||||
оставляют работать процессор и локатор, а по окончании продолжают сбор, если он шёл.
|
||||
Так работают **Apply Radar**, **Clear Runtime History**, **Remove Last Measurement**,
|
||||
**Capture Temporary Reference** и съём наборов. Приостановка завершает и запись на
|
||||
диск. **Stop** отменяет ожидающее продолжение. Если в это время идёт другое такое
|
||||
действие, программа отвечает `Wait for the current pipeline operation to finish`.
|
||||
|
||||
## Actions
|
||||
|
||||
| Кнопка | Что делает |
|
||||
| --- | --- |
|
||||
| **Start** | запускает непрерывный сбор и обработку |
|
||||
| **Single Capture** | снимает одну коллекцию, показывает результат и останавливает сбор |
|
||||
| **Stop** | останавливает сбор |
|
||||
| **Apply Radar** | отправляет настройки радара в железо, не запуская сбор |
|
||||
| **Load Config…** | загружает профиль из файла |
|
||||
| **Save Config…** | сохраняет текущие настройки в профиль |
|
||||
| **Preprocessing…** | открывает окно **Preprocessing Setup**: наборы калибровки и референса, калибровка по объекту ([Калибровка и предобработка](preprocessing.md)) |
|
||||
| **Clear Runtime History** | очищает историю в памяти |
|
||||
| **Mode** | режим обработки: `pass_through`, `bscan`, `gpr`, `sar3d` ([Режимы обработки](processing.md)) |
|
||||
|
||||
### Start
|
||||
|
||||
Перед запуском программа проверяет всё и при любой ошибке отказывает, ничего не
|
||||
остановив; ошибка показывается окном `Failed to start pipeline: <тип>: <текст>`.
|
||||
Проверяются:
|
||||
|
||||
- значения полей (например, буквы в поле **Points**: `invalid literal for int()…`);
|
||||
- профиль сбора: режим драйвера, модель, ключи, линии GPIO, размер слотов колец;
|
||||
- требования режима обработки (B-scan, GPR, 3D SAR — в главе
|
||||
[Режимы обработки](processing.md));
|
||||
- выбранные наборы калибровки и референса (глава
|
||||
[Калибровка и предобработка](preprocessing.md)).
|
||||
|
||||
Если проверка прошла, программа:
|
||||
|
||||
1. перезапускает процессор, если изменились настройки, от которых он зависит
|
||||
(глава [Файл конфигурации](configuration.md));
|
||||
2. записывает файлы наборов и рабочую копию профиля в `python_app/runtime/`;
|
||||
3. очищает историю, если процессор запускается заново (первый **Start**, перезапуск на
|
||||
шаге 1) или изменились профиль сбора, наборы или режекторный фильтр (тогда в
|
||||
журнале `History reset because run settings changed`);
|
||||
4. запускает процесс сбора и препроцессор и ждёт до 3 с, пока они поднимутся;
|
||||
5. начинает читать кольца; строка состояния — **Status: running**.
|
||||
|
||||
Если во время работы процесс конвейера завершился, программа пишет отчёт с хвостом
|
||||
его вывода и перезапускает конвейер: первый раз сразу, следующие подряд — не раньше
|
||||
чем через 6, 12, 24, 48, затем 60 с после предыдущей попытки, пока не получится. Повторное
|
||||
нажатие **Start** при работающем сборе отвечает
|
||||
`Pipeline is already running`.
|
||||
|
||||
### Single Capture
|
||||
|
||||
Делает то же, что **Start**, но процесс сбора снимает одну коллекцию и завершается.
|
||||
Если шёл непрерывный сбор, он сначала останавливается. Программа ждёт результат
|
||||
коллекции, снятой после нажатия, рисует его и останавливает сбор
|
||||
(`Single capture completed`). Строка состояния — **Status: single capture running**.
|
||||
Если результата нет 300 с, сбор останавливается с
|
||||
`Single capture timed out after 300s with no result; stopping.` В режиме 3D SAR, если
|
||||
результат не посчитался, строка состояния показывает **Status: no signal**,
|
||||
**Status: error** или **Status: warning**.
|
||||
|
||||
### Stop
|
||||
|
||||
Останавливает процесс сбора, дочитывает кольца (до 0,35 с), останавливает
|
||||
препроцессор и дочитывает результаты (до 0,25 с). Процессор и локатор продолжают
|
||||
работать: так остановленный B-scan можно пересчитать, а клиенты локатора остаются
|
||||
подключёнными. Запись на диск завершается. **Stop** отменяет и ожидающие действия:
|
||||
автозапуск, продолжение после приостановки, съём для калибровки по объекту,
|
||||
пересчёт B-scan.
|
||||
|
||||
### Apply Radar
|
||||
|
||||
Приостанавливает сбор, проверяет профиль и:
|
||||
|
||||
- одиночный LibreVNA (`native`) — открывает прибор, отправляет настройки развёртки,
|
||||
закрывает;
|
||||
- многоприборная схема — приборы не трогает (их настраивает процесс сбора), только
|
||||
перечитывает пределы мастера;
|
||||
- радиофотонный радар — программирует плату лазеров, если она включена в профиле;
|
||||
- режим `mock` у любой модели — ничего (`Radar pre-configuration skipped (mock mode)`).
|
||||
|
||||
Если работает только процессор, а настройки, от которых он зависит, изменились,
|
||||
**Apply Radar** перезапускает процессор и очищает историю.
|
||||
|
||||
В журнал пишется `Radar settings applied: start=…, stop=…, points=…, ifbw=…, power=…`.
|
||||
Профиль кнопка не меняет.
|
||||
|
||||
### Load Config… и Save Config…
|
||||
|
||||
**Load Config…** открывает выбор файла (**Load Config Profile**) и загружает профиль:
|
||||
все поля, таблицы геометрии, выбранные наборы, режим. Сбор должен быть остановлен,
|
||||
иначе — `Stop all pipeline processes before loading a config profile`. Ошибка чтения
|
||||
показывается как `Failed to load config profile: …`, текущий профиль при этом не
|
||||
меняется.
|
||||
|
||||
**Save Config…** (**Save Config Profile**) записывает профиль; имя без расширения
|
||||
получает `.json`, существующий файл перезаписывается. Записываются все поля окна,
|
||||
выбранные наборы и геометрия. Загруженный или сохранённый профиль становится
|
||||
стартовым для следующего запуска.
|
||||
|
||||
### Clear Runtime History
|
||||
|
||||
Приостанавливает сбор, отбрасывает непрочитанные данные колец, очищает историю
|
||||
окна и процессора, очищает графики и продолжает сбор. Файлы на диске не трогает;
|
||||
подтверждения не спрашивает. Поздние результаты коллекций, снятых до очистки,
|
||||
отбрасываются.
|
||||
|
||||
## Data Actions
|
||||
|
||||
| Элемент | Что делает |
|
||||
| --- | --- |
|
||||
| **Save Last 1000** | сохраняет до 1000 последних коллекций каждой стадии (сырые, предобработанные, результаты) в новый каталог набора данных |
|
||||
| **Save S21 JSON** | сохраняет S21 предобработанной стадии в файлы JSON, по файлу на пару |
|
||||
| **Remove Last Measurement** | удаляет новейшую коллекцию из истории всех стадий |
|
||||
| **Capture Temporary Reference** | снимает временный референс и сразу его применяет ([Калибровка и предобработка](preprocessing.md)) |
|
||||
| **Start + Record to Disk** | запускает сбор, если он остановлен, и записывает на диск следующие **Sweeps to record** коллекций |
|
||||
| **Sweeps to record** | сколько коллекций записать, от 1 до 1 000 000 |
|
||||
| **Path**, **Browse…** | каталог, в котором создаются наборы данных |
|
||||
| **Name** | имя набора данных; пусто — `snapshot_<дата>_<время>` по UTC |
|
||||
|
||||
Каталог набора данных — `<Path>/<префикс>_<Name>`. Префикс описывает снятое:
|
||||
модель, полосу, число точек (у радиофотонного радара — `adc`) и мощность. Каталог
|
||||
не должен существовать (`Snapshot directory already exists`). Рядом с данными
|
||||
кладутся профиль, с которым они сняты (`config_profile.json`), и все ревизии
|
||||
настроек обработки (`processing_settings.json`). Состав файлов — в главе
|
||||
[Данные на диске](data.md).
|
||||
|
||||
Перед сохранением программа дочитывает данные из колец; идущий сбор и запись при
|
||||
этом не прерываются. Если в памяти нет ни одной коллекции, сохранение отказывает:
|
||||
`No runtime data is available for save`. Окно хранит в памяти до 1000 коллекций каждой
|
||||
стадии; что старее,
|
||||
не сохранить. Без колец-копий (`raw_tap`, `preprocessed_tap`) сохраняются только
|
||||
результаты.
|
||||
|
||||
**Save S21 JSON** пишет по файлу на пару: для каждой коллекции — сырая и
|
||||
предобработанная развёртка, рядом профиль `vna_config_profile.json`. Его можно
|
||||
направить в существующий каталог набора данных: существующие файлы данных JSON не
|
||||
перезаписываются, а `vna_config_profile.json` заменяется.
|
||||
|
||||
**Remove Last Measurement** приостанавливает сбор и удаляет из истории окна и
|
||||
процессора самую новую коллекцию; если её результат ещё в пути, процессор его не
|
||||
выдаст. Повторное нажатие удаляет следующую.
|
||||
|
||||
**Start + Record to Disk** записывает коллекции, которые придут после нажатия. Запись
|
||||
заканчивается, когда записано заданное число, по **Stop**, при приостановке сбора
|
||||
(очистка истории, временный референс, съём) или если диск не успевает и очередь
|
||||
записи переполнилась; причина пишется в журнал. Одновременно идёт только одна запись
|
||||
(`A recording is already in progress…`), а каталог должен быть новым. Закрытие окна
|
||||
дописывает принятые данные.
|
||||
|
||||
## Switches
|
||||
|
||||
| Элемент | Что делает |
|
||||
| --- | --- |
|
||||
| **Settling (ms)** | пауза после переключения ВЧ-ключей перед развёрткой, мс (`run.settling_ms`) |
|
||||
| **Run combos** и **Select** | список пар сбора `вход:выход` через запятую; пусто — полная матрица. **Select** делает этот список источником пар |
|
||||
| **Single combo**: **Input**, **Output** и **Select** | одна пара сбора; **Select** делает её источником пар |
|
||||
| **B-scan pair** | какую пару показывать в B-scan; доступно в режиме B-scan |
|
||||
| **Next Combination** | следующая пара B-scan; доступно, когда пар в истории хотя бы две |
|
||||
|
||||
Пары сбора применяются при **Start**. У многоприборной схемы список всегда равен
|
||||
полной матрице и не редактируется. В режимах pass-through и B-scan, если оба ключа
|
||||
`mock` или у обоих одно положение, снимается только пара `0:0`.
|
||||
|
||||
## Selected Preprocess Sets
|
||||
|
||||
Показывает выбранные наборы: **S21 calibration**, **S21 reference**,
|
||||
**Object calibration**; `<not selected>` — набор не выбран. Выбираются наборы в окне
|
||||
**Preprocessing Setup**.
|
||||
|
||||
## Processing
|
||||
|
||||
Страница настроек выбранного режима. Каждое поле описано в главе
|
||||
[Режимы обработки](processing.md). В режимах GPR и 3D SAR над настройками — таблицы
|
||||
геометрии антенн.
|
||||
|
||||
### Таблицы геометрии
|
||||
|
||||
**Tx geometry** — передающие антенны: **Output** (номер выхода), **X (m)**, **Y (m)**,
|
||||
**Z (m)**. **Rx geometry** — приёмные: **Input**, **X (m)**, **Y (m)**, **Z (m)**.
|
||||
Координаты — в осях радара (глава [Как работает радар](principles.md)).
|
||||
|
||||
- **Add Antenna** добавляет строку со следующим свободным номером; строка начинает
|
||||
действовать после ввода X, и до этого кнопка недоступна. Недоступна она и тогда,
|
||||
когда строк столько же, сколько выходов или входов.
|
||||
- **Remove Selected** удаляет выделенные строки.
|
||||
- Каждая правка проверяется: номер — целый, в пределах матрицы, без повторов;
|
||||
координаты — числа; требования GPR и 3D SAR выполняются. Неверная правка
|
||||
откатывается с сообщением **Invalid Antenna Geometry**.
|
||||
- Принятая правка сразу уходит процессору и пересчитывает результат. Если она не
|
||||
совпадает с геометрией выбранной калибровки по объекту, выбор этой калибровки
|
||||
снимается.
|
||||
|
||||
## Radar
|
||||
|
||||
Для LibreVNA и симулятора секция показывает развёртку:
|
||||
|
||||
| Поле | Ключ профиля |
|
||||
| --- | --- |
|
||||
| **Start (Hz)** | `radar.sweep.start_hz` |
|
||||
| **Stop (Hz)** | `radar.sweep.stop_hz` |
|
||||
| **Points** | `radar.sweep.points` |
|
||||
| **IF bandwidth (Hz)** | `radar.sweep.if_bandwidth_hz` |
|
||||
| **Stimulus power (dBm)** | `radar.sweep.stimulus_power_dbm` |
|
||||
|
||||
Под полями — пределы прибора: `Limits: freq=…, points=1..N, ifbw=…, power=…`. В режиме
|
||||
`mock` и у радиофотонного радара пределов нет: `Device limits are not available.` (с
|
||||
`driver_mode: "native"` без подключённого прибора окно не открывается — глава
|
||||
[Неполадки](troubleshooting.md)). С известными пределами подпись поля дополняется
|
||||
диапазоном, а значения вне его подрезаются с предупреждением
|
||||
`Radar fields were adjusted to satisfy device limits.` Поле применяется, когда из него
|
||||
уходит фокус или нажат Enter. Если
|
||||
изменение меняет ключ радара, выбор наборов сбрасывается, а границы частот обработки
|
||||
подрезаются под новую развёртку.
|
||||
|
||||
Для радиофотонного радара вместо развёртки показаны поля коллектора и платы
|
||||
лазеров:
|
||||
|
||||
| Поле | Ключ профиля |
|
||||
| --- | --- |
|
||||
| **Project dir**, **Executable path**, **TTY path** | `radar.rph_radar.project_dir`, `executable_path`, `tty_path` |
|
||||
| **Arguments** (по строке на параметр) | `radar.rph_radar.args` |
|
||||
| **Environment JSON** | `radar.rph_radar.env` |
|
||||
| **Startup timeout (s)**, **Read timeout (s)**, **Stop timeout (s)** | `startup_timeout_s`, `sweep_timeout_s`, `stop_timeout_s` |
|
||||
| **Board enabled** | `radar.laser_control.enabled` |
|
||||
| **Board port**, **Board mode** | `port`, `mode` (`manual` или `variation`) |
|
||||
| **PI 1 P**, **PI 1 I**, **PI 2 P**, **PI 2 I** | `pi_coeff1_p`, `pi_coeff1_i`, `pi_coeff2_p`, `pi_coeff2_i` |
|
||||
| **Temperature 1**, **Temperature 2**, **Current 1**, **Current 2** (режим `manual`) | `manual.temp1`, `temp2`, `current1`, `current2` |
|
||||
| **Static temp 1**, **Static temp 2**, **Static current 1**, **Static current 2**, **Min value**, **Max value**, **Step**, **Time step**, **Delay time** (режим `variation`) | `variation.static_temp1` … `delay_time` |
|
||||
|
||||
Поля платы доступны при включённой **Board enabled**; видны поля только выбранного
|
||||
режима. Единицы и пределы — в главе [Файл конфигурации](configuration.md). Поля
|
||||
коллектора применяются при **Start**, поля лазера — кнопкой **Apply Radar**.
|
||||
|
||||
## Строка состояния и журнал
|
||||
|
||||
Под секциями — строка состояния и счётчик истории
|
||||
`History: raw=0, preprocessed=0, results=0` (сколько коллекций каждой стадии в памяти).
|
||||
|
||||
| Состояние | Что значит |
|
||||
| --- | --- |
|
||||
| **Status: idle** | сбор остановлен |
|
||||
| **Status: running** | идёт непрерывный сбор |
|
||||
| **Status: single capture running** | идёт однократный съём |
|
||||
| **Status: error** | в журнал записана ошибка; состояние держится до следующего изменения |
|
||||
| **Status: result display error (acquisition continues)** | не удалось нарисовать результат; сбор продолжается |
|
||||
| **Status: runtime display/recording error (acquisition continues)** | ошибка отображения или записи на диск; сбор продолжается |
|
||||
| **Status: no signal**, **Status: warning** | итог однократного съёма в режиме 3D SAR |
|
||||
|
||||
**Runtime Log** — журнал программы. Список **Level** (**Debug**, **Info**,
|
||||
**Warning**, **Error**) задаёт наименьший уровень записей; он применяется сразу и
|
||||
сохраняется в профиль при **Save Config…**. Стрелка справа прячет и показывает журнал.
|
||||
В окне хранится до 2000 последних записей; при переполнении появляется
|
||||
`Log panel overflow: N record(s) dropped (they are still in the log file).` Полный
|
||||
журнал — в `python_app/runtime/logs/radar.log` (глава [Данные на диске](data.md)).
|
||||
Повторяющиеся ошибки отрисовки показываются один раз.
|
||||
|
||||
## Графики
|
||||
|
||||
| Режим | Что на экране |
|
||||
| --- | --- |
|
||||
| pass-through | **Pass-Through S21**: модуль (**Magnitude (dB)**) и фаза (**Phase (deg)**) по частоте, кривая на пару, легенда `in<вход>/out<выход>` |
|
||||
| B-scan | **B-scan S21 in<вход>/out<выход> \| sweeps=N**: номер развёртки — дальность |
|
||||
| GPR | **GPR coherent BP — ground plane**: карта плоскости земли и объекты |
|
||||
| 3D SAR | **Front (X-Y)**, **Side (Z-Y)**, **Top (X-Z)**, **3D Projection** и строка состояния 3D SAR над ними |
|
||||
|
||||
Подробно виды описаны в главе [Режимы обработки](processing.md). Приставку единиц на
|
||||
осях (Гц, МГц, ГГц, м, мм) график подбирает сам.
|
||||
|
||||
Графики управляются мышью так, как принято в библиотеке pyqtgraph: колесо меняет
|
||||
масштаб, перетаскивание сдвигает, правая кнопка открывает меню (**View All** —
|
||||
вернуть весь график, экспорт картинки). Щелчок по строке легенды прячет и показывает
|
||||
кривую. Графики окна видны и в веб-интерфейсе.
|
||||
|
||||
Последняя развёртка снятого набора (калибровка, референс, временный референс)
|
||||
рисуется на графике pass-through с подписью вида `Tmp S21 Reference last trace`.
|
||||
|
||||
## Сообщения
|
||||
|
||||
Ошибка действия открывает окно с ошибкой и пишется в журнал. Без экрана, для
|
||||
действий из веб-интерфейса и для GPIO-кнопки окон нет: ошибка уходит в журнал, а
|
||||
веб-интерфейсу — в ответ на запрос. Причины частых ошибок — в главе
|
||||
[Неполадки](troubleshooting.md).
|
||||
|
||||
Окно ошибки называется по действию и обычно показывает
|
||||
`<заголовок>: <тип ошибки>: <текст>` (у записи на диск — `<заголовок>: <текст>`):
|
||||
|
||||
| Заголовок | Действие |
|
||||
| --- | --- |
|
||||
| `Failed to start pipeline` | **Start**, **Single Capture** |
|
||||
| `Failed to stop pipeline` | **Stop** |
|
||||
| `Failed to apply radar settings` | **Apply Radar** |
|
||||
| `Failed to load config profile`, `Failed to save config profile` | **Load Config…**, **Save Config…** |
|
||||
| `Failed to open preprocessing panel` | **Preprocessing…** |
|
||||
| `Failed to clear runtime history`, `Failed to remove last runtime measurement` | **Clear Runtime History**, **Remove Last Measurement** |
|
||||
| `Failed to save snapshot`, `Failed to save VNA history JSON` | **Save Last 1000**, **Save S21 JSON** |
|
||||
| `Failed to start disk recording` | **Start + Record to Disk** |
|
||||
| `Failed to capture tmp reference` | **Capture Temporary Reference** |
|
||||
| `Cannot switch processing mode to <режим>` | **Mode** |
|
||||
| `Failed to update live processing settings`, `Failed to update plot display settings`, `Failed to render processing settings` | правка настроек обработки и вида |
|
||||
| `Failed to recompute the B-scan` | пересчёт остановленного B-scan |
|
||||
| `Failed to start <вид> sequence`, `Failed to capture combo; previous captures kept, retry when ready`, `Failed to undo last preprocess capture`, `Failed to save preprocess set`, `Failed to abort capture sequence`, `Failed to refresh preprocessing sets`, `Failed to create neutral S21 sets` | окно **Preprocessing Setup** |
|
||||
| `Failed to capture object-calibration data`, `Failed to calculate object calibration`, `Failed to save object calibration`, `Failed to update object calibration after geometry change` | калибровка по объекту |
|
||||
| `Cannot perform data action`, `Cannot modify runtime history during active capture sequence` | действие с данными во время съёма или остановки |
|
||||
| `Shutdown operation failed` | закрытие окна |
|
||||
|
||||
Пока идёт съём наборов, часть действий недоступна, и программа объясняет почему:
|
||||
`Finish the active capture sequence first`, `Wait for preprocessing capture to finish`,
|
||||
`Wait for object-calibration capture to finish`, `Wait for pipeline shutdown to finish`.
|
||||
@@ -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`, локальные инструменты разработчиков.
|
||||
@@ -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) | радар с синтезированной апертурой; режим, строящий трёхмерное изображение сцены |
|
||||
@@ -0,0 +1,382 @@
|
||||
# Оборудование
|
||||
|
||||
Тип радара задаёт поле `radar.model` профиля, а `radar.driver_mode` — работать с
|
||||
настоящим железом (`"native"`) или с симулятором (`"mock"`). Ключи профиля описаны
|
||||
в главе [Файл конфигурации](configuration.md), принцип измерения — в главе
|
||||
[Как работает радар](principles.md), термины — в [словаре](glossary.md).
|
||||
|
||||
| Радар | `radar.model` | Что подключается | Процесс сбора |
|
||||
| --- | --- | --- | --- |
|
||||
| Одиночный LibreVNA | `librevna` | один LibreVNA по USB, два ВЧ-ключа на GPIO | `python_app.scripts.matrix_raw_producer` |
|
||||
| Многоприборная схема | `librevna_multi` | три LibreVNA по USB, петля триггера; в режиме переключения стимула — контроллер STM32 и ключи на портах мастера | `python_app.scripts.matrix_raw_producer` |
|
||||
| Радиофотонный радар с АЦП | `rph_radar`, `radar.rph_radar.source: "e502"` | АЦП L-Card E-502 по USB, плата лазеров по последовательному порту, два ВЧ-ключа на GPIO | `python_app.scripts.rph_radar_raw_producer` и программа `rph_radar_collector` |
|
||||
| Радиофотонный радар с платой RFG | `rph_radar`, `radar.rph_radar.source: "rfg_udp"` | плата RFG по Ethernet, плата лазеров по последовательному порту; одна пара антенн | `python_app.scripts.rph_radar_raw_producer` |
|
||||
| Симулятор | `librevna` или `librevna_multi` с `driver_mode: "mock"` | ничего | `python_app.scripts.matrix_raw_producer` |
|
||||
|
||||
Для радиофотонного радара симулятора нет: он работает только с `driver_mode: "native"`.
|
||||
|
||||
Процесс сбора открывает приборы и при неудаче повторяет попытки бесконечно, с паузой
|
||||
от 1 до 10 с: прибор можно подключить и после запуска. Его сообщения пишутся в
|
||||
`python_app/runtime/logs/acquisition.err.log`, а в журнал окна попадают, только если
|
||||
процесс завершился. Ошибки сбора и их признаки собраны в главе
|
||||
[Неполадки](troubleshooting.md).
|
||||
|
||||
## LibreVNA
|
||||
|
||||
LibreVNA — двухпортовый векторный анализатор цепей (ВАЦ) с USB. В системе используются платы
|
||||
ревизии B. Прошивки — изменённая версия 1.6.5, протокол версии 14 (программа
|
||||
проверяет версию при подключении одиночного прибора).
|
||||
|
||||
| Предел | Значение |
|
||||
| --- | --- |
|
||||
| Частота | 0–6 ГГц |
|
||||
| Полоса ПЧ | 6 Гц–50 кГц |
|
||||
| Число точек | до 4501 |
|
||||
| Мощность | от −40 до 0 дБм |
|
||||
| Задержка на стадии `dwell_us` | до 10 239 мкс |
|
||||
|
||||
Ниже 25 МГц генератор прибора переключается на другую микросхему, и развёртка идёт
|
||||
с неравномерными длинными паузами: если развёртка должна быть быстрой, начинайте её не
|
||||
ниже 30 МГц (так сделано в `run_config_librevna_multi_per_point_lowband.example.json`).
|
||||
|
||||
С `driver_mode: "native"` программа при запуске окна, загрузке профиля и нажатии
|
||||
**Apply Radar** читает пределы прибора и подрезает поля секции **Radar** под них
|
||||
(`Radar fields were adjusted to satisfy device limits.` в журнале). У многоприборной
|
||||
схемы пределы читаются с мастера. Если прибор с `driver_mode: "native"` не
|
||||
подключён, окно программы не открывается: чтение пределов при старте завершается
|
||||
ошибкой.
|
||||
|
||||
USB-доступ к прибору даёт правило udev `/etc/udev/rules.d/99-radar-librevna.rules`
|
||||
(группа `plugdev`). Его ставит `./start.sh` при обычном запуске с профилем LibreVNA;
|
||||
при запуске без экрана (`--headless`) правило не ставится.
|
||||
|
||||
## Одиночный LibreVNA
|
||||
|
||||
Прибор подключается по USB; если `radar.serial` пуст, берётся первый найденный.
|
||||
Порт 1 излучает, порт 2 принимает. Выходной ВЧ-ключ (`switches.port1`) подключает
|
||||
порт 1 к одной из передающих антенн, входной (`switches.port2`) — порт 2 к одной из
|
||||
приёмных. Ключами управляет компьютер через GPIO.
|
||||
|
||||
Сбор одной коллекции:
|
||||
|
||||
1. Для каждой пары из списка сбора программа выставляет выходной ключ, затем входной,
|
||||
и ждёт время установления `run.settling_ms`.
|
||||
2. Запускает развёртку и принимает все точки. Если какая-то точка не пришла,
|
||||
коллекция не публикуется, а прибор открывается заново.
|
||||
3. Публикует коллекцию в кольцо `raw` и начинает следующую.
|
||||
|
||||
S21 одиночного прибора — отношение сигнала порта 2 к сигналу опорного приёмника.
|
||||
**Apply Radar** открывает прибор, отправляет настройки развёртки и закрывает его.
|
||||
|
||||
## Многоприборная схема
|
||||
|
||||
Три LibreVNA: мастер (`radar.serial`) и два ведомых
|
||||
(`radar.multi_device.slave_serials`, ровно два). Мастер на каждой частоте излучает по
|
||||
очереди с порта 1 и порта 2, ведомые не излучают и принимают каждый на оба порта.
|
||||
S21 — сигнал приёмника ведомого, делённый на сигнал опорного приёмника мастера на той
|
||||
же стадии.
|
||||
|
||||
| Номер | Что это |
|
||||
| --- | --- |
|
||||
| вход 0, 1 | первый ведомый из `slave_serials`, порт 1 и порт 2 |
|
||||
| вход 2, 3 | второй ведомый, порт 1 и порт 2 |
|
||||
| выход 0, 1 (без переключения стимула) | мастер, порт 1 и порт 2 |
|
||||
| выход 0, 1, 2, 3 (переключение стимула, два положения) | порт 1 положение A, порт 1 положение B, порт 2 положение A, порт 2 положение B |
|
||||
|
||||
Номер выхода совпадает с номером стадии прибора. Многоприборная схема всегда снимает
|
||||
полную матрицу: список пар `run.combos` у неё заменяется всеми парами, и в окне он не
|
||||
редактируется.
|
||||
|
||||
Приборы синхронизированы аппаратным сигналом запуска. Петля триггера:
|
||||
`TRIGGER_OUT` мастера → первый ведомый → второй ведомый → `TRIGGER_IN` мастера.
|
||||
Вернувшийся сигнал означает «все готовы». Мастер измеряет по вернувшемуся сигналу,
|
||||
поэтому задержки в петле одинаковы для всех приборов и в отношении «приёмник/опорный»
|
||||
сокращаются.
|
||||
|
||||
Опорная частота: поле `radar.multi_device.force_external_reference` включает внешнюю
|
||||
опору 10 МГц сразу на всех трёх приборах (раздельно задать нельзя). Во всех профилях
|
||||
оно выключено. Включайте его, только если опора 10 МГц действительно подана на все
|
||||
приборы. Признак отсутствующей опоры при включённом поле — мастер не отдаёт точки:
|
||||
`Packet not received from <серийный номер> within 1.0 s`.
|
||||
|
||||
Сбор: перед первой коллекцией программа настраивает опору на всех приборах,
|
||||
затем отправляет настройки развёртки сначала ведомым, потом мастеру, чтобы приёмники
|
||||
были готовы раньше, чем мастер выдаст запуск. Развёртка идёт непрерывно. Каждый
|
||||
прибор передаёт свои точки по отдельному USB; программа ждёт начала развёртки на всех
|
||||
трёх и принимает цикл, только если начала совпали по времени. Если пропало не больше
|
||||
5 % точек трассы, они заменяются нулями (`Zero-filled dropped sweep point(s) to keep the cycle…`),
|
||||
иначе цикл отбрасывается.
|
||||
|
||||
| Сбой | Предел |
|
||||
| --- | --- |
|
||||
| от прибора нет ни одной точки | 1,5 с |
|
||||
| нет начала развёртки | 6 с |
|
||||
| цикл не закончился | 8 с |
|
||||
| повторные попытки без переподключения | 2 |
|
||||
| переподключение всех приборов | `radar.multi_device.recovery_attempts` раз (по умолчанию 3), не дольше 10 с |
|
||||
|
||||
После этого процесс сбора пересоздаёт подключение и ждёт приборы бесконечно. Если
|
||||
не пришло ни одной точки ни от одного прибора, в журнале процесса сбора будет
|
||||
`No datapoints received from at least one device; hardware trigger sync did not start. Check Trigger Out/In loop and 10 MHz reference wiring.`
|
||||
|
||||
**Apply Radar** в этой схеме приборы не трогает: их настраивает процесс сбора
|
||||
(`Multi-device raw producer will configure all LibreVNA devices`), кнопка только
|
||||
перечитывает пределы мастера.
|
||||
|
||||
Прошивки: мастер работает с образом `combined.vnafw` (обычная раскладка стадий с
|
||||
аппаратным триггером) или `combined_per_point_switching.vnafw` (переключение стимула),
|
||||
ведомые — с `combined_external_lo_slave.vnafw`. Образы и правила их сборки — в
|
||||
разделе «Прошивки» ниже.
|
||||
|
||||
## Переключение стимула внутри частотной точки
|
||||
|
||||
В этом режиме одна развёртка даёт матрицу 4 × 4 вместо 2 × 4: на каждом порту
|
||||
мастера стоит ВЧ-ключ, и на каждой частоте каждый порт излучает дважды, через оба
|
||||
положения ключа. Все 16 пар одной частоты измеряются за доли миллисекунды, поэтому
|
||||
движение радара между ними пренебрежимо мало. Полное описание —
|
||||
[per_point_switching.md](../device_firmware/per_point_switching.md).
|
||||
|
||||
Что нужно:
|
||||
|
||||
- прошить мастер образом `combined_per_point_switching.vnafw`; ведомые не трогать;
|
||||
- поставить контроллер ключей (плата WeAct STM32G431CB с прошивкой
|
||||
`device_firmware/switch_controller`) в разрыв петли триггера между
|
||||
`TRIGGER_OUT` мастера и `TRIGGER_IN` первого ведомого;
|
||||
- в профиле задать `radar.multi_device.stimulus_switch_positions: 2` и
|
||||
`radar.sweep.dwell_us: 2`. Ключами на портах мастера управляет контроллер, а не
|
||||
компьютер; разделы `switches` у многоприборной схемы программа сама ставит в `mock`.
|
||||
|
||||
| Вывод STM32 | Куда |
|
||||
| --- | --- |
|
||||
| PB0 (вход) | `TRIGGER_OUT` мастера |
|
||||
| PB1 (выход, повторяет PB0) | `TRIGGER_IN` первого ведомого |
|
||||
| PB2 | ключ порта 1 мастера |
|
||||
| PB6 | ключ порта 2 мастера |
|
||||
| PB5 | отладка: импульс 2 мкс на каждую поправку чётности |
|
||||
|
||||
Контроллер переключает оба ключа по каждому спаду сигнала запуска (положение A —
|
||||
логический 0, B — 1). Если пауза между стадиями в 1,5 раза длиннее наименьшей из
|
||||
последних 16 пауз, то есть на границе развёртки, он принудительно возвращает положение
|
||||
A и считает это поправкой чётности. Задержка, которую он вносит в петлю (около 50 нс),
|
||||
одинакова для всех
|
||||
приборов и на фазу не влияет. Через 200 мс простоя триггера контроллер печатает
|
||||
отчёт по USB (115 200 бод): `trigger cycles`, `resyncs (sweep ends)`, `parity corrections`
|
||||
(должно быть `0 (ok)`), `stages per resync` (ожидается число точек × число стадий),
|
||||
паузы и их гистограмма.
|
||||
|
||||
Проверка на железе — `python_app/scripts/per_point_switching_check.py --config <профиль>`:
|
||||
она отказывает, если медиана модуля S21 какого-то выхода ниже −80 дБ (признак старой
|
||||
прошивки мастера) или если два положения одного порта без подключённого ключа
|
||||
различаются больше чем на 6 дБ. С ключом нужен флаг `--switch-connected`.
|
||||
|
||||
> **Внимание.** Полоса ПЧ 1 кГц в этом режиме воспроизводимо роняет приборы
|
||||
> (`LIBUSB_ERROR_IO`). Работают 5, 10, 20 и 50 кГц; рабочее значение — 50 кГц.
|
||||
|
||||
## ВЧ-ключи H7992
|
||||
|
||||
Ключ подключает порт прибора к одной из до четырёх антенн. Положение задают две
|
||||
линии GPIO, A (`pin_a`) и B (`pin_b`); линии нужны обе даже при двух положениях.
|
||||
|
||||
| Положение | A | B |
|
||||
| --- | --- | --- |
|
||||
| 0 | 0 | 0 |
|
||||
| 1 | 0 | 1 |
|
||||
| 2 | 1 | 0 |
|
||||
| 3 | 1 | 1 |
|
||||
|
||||
Номера линий в профилях — номера линий GPIO-чипа (`gpio_chip`, обычно
|
||||
`/dev/gpiochip0`), а не номера контактов разъёма. Во всех профилях-примерах, кроме
|
||||
примера с платой RFG (там ключей нет), выходной ключ на линиях 17 (A) и 27 (B),
|
||||
входной — на 22 (A) и 23 (B). При открытии программа
|
||||
выставляет ключ в `default_position`. Ключ с `driver_mode: "mock"` никуда не
|
||||
переключается, программа только помнит его положение.
|
||||
|
||||
Кто управляет ключами:
|
||||
|
||||
| Радар | Кто переключает |
|
||||
| --- | --- |
|
||||
| одиночный LibreVNA | процесс сбора на Python |
|
||||
| многоприборная схема | процесс сбора, только если `multi_device.output_switch_positions` или `input_switch_positions` больше 1; в режиме переключения стимула — контроллер STM32 |
|
||||
| радиофотонный с E-502 | программа `rph_radar_collector`, если оба ключа `native`; иначе процесс сбора |
|
||||
|
||||
> **Внимание.** На Raspberry Pi 5 номер GPIO-чипа 40-контактного разъёма менялся от
|
||||
> версии ядра к версии. Перед работой проверьте его на самой машине: `gpiodetect` и
|
||||
> `gpioinfo -c gpiochip0`. Неверный чип означает ключи, которые молча не
|
||||
> переключаются.
|
||||
|
||||
## Радиофотонный радар
|
||||
|
||||
Плата лазеров меняет ток лазера LD1 ступенями, при этом ток второго лазера и
|
||||
температуры обоих постоянны. На каждой ступени оцифровываются основной сигнал и
|
||||
опорный. Опорное плечо имеет постоянную задержку, поэтому его фаза растёт с частотой
|
||||
линейно; по двум опорным точкам `phase_calibration` (фаза — частота) программа
|
||||
определяет частоту каждой ступени. S21 — основной сигнал, делённый на модуль
|
||||
опорного. Развёртки, которые не покрывают выходную полосу `radar.rph_radar.band`,
|
||||
отбрасываются, остальные пересчитываются на её равномерную сетку (по умолчанию
|
||||
2,1–5,5 ГГц, 2048 точек). Поля `radar.sweep` для этого радара не используются, в
|
||||
окне вместо секции LibreVNA показывается секция RPH.
|
||||
|
||||
### Плата лазеров
|
||||
|
||||
Плата подключается по последовательному порту (115 200 бод) и программируется
|
||||
кнопкой **Apply Radar**, если в профиле `laser_control.enabled: true`. Программа
|
||||
проверяет все значения до отправки первого байта, затем сбрасывает плату и отправляет
|
||||
постоянные токи и температуры, а в режиме `variation` — ещё задачу развёртки тока
|
||||
LD1. Каждая команда должна получить ответ «состояние 0»; иначе
|
||||
`Device rejected command: STATE 0x…`. После закрытия порта развёртка лазера
|
||||
продолжается.
|
||||
|
||||
| Величина | Пределы | По умолчанию |
|
||||
| --- | --- | --- |
|
||||
| температура лазеров | 15–40 °C | 24,0 и 24,9 °C |
|
||||
| ток лазеров | 15–110 мА | 97,3 и 102,0 мА |
|
||||
| развёртка тока LD1 | 15–110 мА, начало меньше конца | 97,3 → 107,0 мА |
|
||||
| шаг тока | 0,002–0,5 мА | 0,05 мА |
|
||||
| длительность ступени | 20–100 мкс | 50 мкс |
|
||||
| пауза между развёртками | 3–10 мс | 10 мс |
|
||||
|
||||
При запуске без экрана **Apply Radar** выполняется автоматически через 0,5 с после
|
||||
старта, а через 1 с после него — **Start**, даже если **Apply Radar** не удался.
|
||||
В веб-интерфейсе **Apply Radar** нет.
|
||||
|
||||
### АЦП L-Card E-502
|
||||
|
||||
Данные с АЦП читает отдельная программа `build/bin/rph_radar_collector` (собирается
|
||||
командой `make rph_radar_collector`, в обычный `make` не входит). Её запускает и
|
||||
останавливает процесс сбора; программа живёт, только пока жив её запустивший
|
||||
процесс. Библиотеки L-Card `libx502api.so` и `libe502api.so` она загружает при
|
||||
запуске, по умолчанию из `~/.local/lib`. Развёртки она передаёт процессу сбора через
|
||||
псевдотерминал `rph_radar.tty_path` (в примере `/tmp/ttyADC_data`). Устройство
|
||||
выбирается первое найденное. Подробности — в
|
||||
[README коллектора](../data_acq_and_processing/rph_radar_collector/README.md).
|
||||
|
||||
Параметры коллектора задаются списком `radar.rph_radar.args`:
|
||||
|
||||
| Параметр | Что задаёт | По умолчанию |
|
||||
| --- | --- | --- |
|
||||
| `range:<В>` | диапазон входа ±В: `10`, `5`, `2`, `1`, `0.5` или `0.2` | ±2 В |
|
||||
| `internal_ref_hz:<Гц>` | внутренняя опорная частота, она же наибольшая частота АЦП: `2000000` или `1500000` | 2 МГц |
|
||||
| `duration_ms:<мс>` | наибольшая длительность развёртки, больше 0 и не больше 60 000 мс; более длинная считается испорченной | 100 мс |
|
||||
| `do8_cycle_period:<n>` | период выхода DO8 в словах цифрового вывода, 4–262144, причём НОК(4, n) не больше 262144 (нечётные n больше 65 536 не подходят) | 8 |
|
||||
|
||||
Остальные слова в `args` (`profile:phase`, `clock:internal`, `start:di_syn2_rise`,
|
||||
`stop:di_syn2_fall`, `sample_clock_hz:max`, `packet_limit:0`, `do1_toggle_per_frame`,
|
||||
`do1_pair_subtract_avg`, `do8_freq_ref`) только подтверждают единственный
|
||||
поддерживаемый режим; любое другое слово отвергается.
|
||||
|
||||
Как коллектор использует цифровые линии АЦП:
|
||||
|
||||
| Линия | Что делает коллектор |
|
||||
| --- | --- |
|
||||
| DI_SYN2 | фронт открывает развёртку, спад закрывает |
|
||||
| DI1 | смена уровня отмечает новую ступень |
|
||||
| DI2 | делит отсчёты ступени на две группы; значение ступени — среднее разности групп |
|
||||
| DI8 | отсчёты с высоким DI8 идут в опорный канал |
|
||||
| DO1 | меняется на каждом кадре |
|
||||
| DO2 | высокий, пока идёт поток |
|
||||
| DO8 | высокий на двух последних словах каждого периода `do8_cycle_period` |
|
||||
|
||||
Если ключами управляет коллектор, он переключает их сам между развёртками, ждёт
|
||||
`run.settling_ms` (не меньше 1 мс) и открывает новый сеанс АЦП; первая развёртка
|
||||
после переключения отбрасывается.
|
||||
|
||||
### Плата RFG
|
||||
|
||||
Плата RFG шлёт по UDP точки развёртки на адрес `radar.rph_radar.udp_bind`. В примере
|
||||
это проводной адрес компьютера `192.168.2.1:5000`: привязка к проводному интерфейсу
|
||||
отсекает чужие широковещательные пакеты (например, из Wi-Fi), а любой непонятный пакет
|
||||
программа считает ошибкой. В каждой точке три канала; основной сигнал — разность
|
||||
каналов 0 и 1 (синхронный детектор), опорный — канал 2.
|
||||
|
||||
Требования профиля:
|
||||
|
||||
- `laser_control.mode: "variation"` — даже если лазер запускают из другой программы:
|
||||
по его настройкам вычисляется длина развёртки;
|
||||
- у обоих ключей одно положение, то есть одна пара антенн: без метки от источника
|
||||
нельзя доказать, что развёртка после переключения свежая.
|
||||
|
||||
Длина развёртки — ⌈(конец − начало) / шаг⌉ ступеней в кодах ЦАП тока; при настройках
|
||||
по умолчанию 199 ступеней по 50 мкс. Развёртка принимается, только если пришли все
|
||||
ступени подряд. Плата теряет несколько процентов точек, такие развёртки
|
||||
отбрасываются. Время каждой точки — метка ядра о приёме пакета.
|
||||
|
||||
### Фазовая калибровка
|
||||
|
||||
Фазовые якоря `radar.rph_radar.phase_calibration` измеряются программой
|
||||
`python_app/scripts/rph_radar_calibrate.py`. Перед ней примените настройки лазера
|
||||
кнопкой **Apply Radar** и остановите сбор.
|
||||
|
||||
```sh
|
||||
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200
|
||||
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200 --apply
|
||||
```
|
||||
|
||||
Первая команда снимает развёртки и печатает измеренные фазы начала и конца, вторая
|
||||
снимает заново и записывает в профиль только `phase0_rad` и `phase1_rad`, не трогая
|
||||
остальные байты файла. Частотные якоря `freq0_hz` и `freq1_hz` должны заранее
|
||||
описывать известные концы развёртки. `--warmup` задаёт, сколько первых развёрток
|
||||
отбросить (по умолчанию 10). Команды лазеру программа не отправляет. Если профиль
|
||||
изменился во время съёма, она его не перезаписывает.
|
||||
|
||||
## Симулятор
|
||||
|
||||
С `driver_mode: "mock"` процесс сбора не трогает железо и моделирует один точечный
|
||||
отражатель. Отражатель движется по восьмисекундному циклу: вдоль ряда антенн — вокруг
|
||||
его середины (размах — половина ширины ряда плюс 0,25 м, но не меньше 0,75 м), по
|
||||
дальности — между 3,25 и 4,25 м, на высоте 0,25 м. S21 каждой пары
|
||||
считается по настоящим расстояниям от антенн из профиля; коллекции идут не чаще чем
|
||||
раз в 50 мс. Шума, помех, переключения ключей и движения радара симулятор не
|
||||
моделирует; скорость радара при работе с ним должна быть нулевой. Предобработке
|
||||
по-прежнему нужны наборы; для симулятора подходят нейтральные (глава
|
||||
[Калибровка и предобработка](preprocessing.md)).
|
||||
|
||||
## GPIO-кнопка
|
||||
|
||||
К компьютеру можно подключить кнопку, которая снимает временный референс — то же,
|
||||
что **Capture Temporary Reference**. По умолчанию она выключена (раздел
|
||||
`control_button` профиля, ни в одном примере не включена). Схема, на которую
|
||||
рассчитаны умолчания: кнопка между линией GPIO и землёй, внутренняя подтяжка к
|
||||
питанию, нажатие — спад уровня. Дребезг подавляет ядро (`debounce_ms`, по умолчанию
|
||||
50 мс). Настройки кнопки читаются при запуске окна; после загрузки другого профиля
|
||||
нужно перезапустить программу.
|
||||
|
||||
## Тележка
|
||||
|
||||
`device_firmware/cart_firmware` — прошивка платы WeAct STM32G431CB, которая заменяет
|
||||
пульт тележки: читает джойстик и выдаёт команды движения в формате SBUS. С
|
||||
программой радара она не связана. Скорость для поправки на движение радар получает
|
||||
от клиента TCP-локатора (глава [Веб-интерфейс, кнопка и локатор](web.md)).
|
||||
Протокол пульта — [protocol.md](../device_firmware/cart_firmware/docs/protocol.md).
|
||||
|
||||
## Прошивки
|
||||
|
||||
| Где | Что | На чём работает |
|
||||
| --- | --- | --- |
|
||||
| `device_firmware/multi_libreVNA_firmware/combined.vnafw` | образ мастера: обычные стадии и аппаратный триггер | LibreVNA-мастер |
|
||||
| `device_firmware/multi_libreVNA_firmware/combined_per_point_switching.vnafw` | образ мастера с переключением стимула | LibreVNA-мастер |
|
||||
| `device_firmware/multi_libreVNA_firmware/combined_external_lo_slave.vnafw` | образ ведомого | LibreVNA-ведомые |
|
||||
| `device_firmware/multi_libreVNA_firmware/reproduce/` | полное дерево LibreVNA 1.6.5 с доработками и комплект воспроизводимой сборки | ПК разработчика |
|
||||
| `device_firmware/switch_controller/` | контроллер ключей для переключения стимула | WeAct STM32G431CB |
|
||||
| `device_firmware/cart_firmware/` | пульт тележки | WeAct STM32G431CB |
|
||||
|
||||
Образ `.vnafw` заливается в LibreVNA программой LibreVNA-GUI (Device → Firmware
|
||||
Update). Платы STM32 прошиваются через PlatformIO и ST-Link: `pio run -t upload`.
|
||||
|
||||
> **Внимание.** `top_master.bin` и `combined.vnafw` — единственные копии рабочего
|
||||
> битстрима мастера и рабочего образа прошивки, битстрим заново не воспроизводится.
|
||||
> Никогда их не перезаписывайте; новые сборки сохраняйте под новыми именами.
|
||||
|
||||
Подробно:
|
||||
|
||||
- [reproduce/README.md](../device_firmware/multi_libreVNA_firmware/reproduce/README.md) — как
|
||||
устроен комплект сборки, отличия от LibreVNA 1.6.5, что воспроизводится побайтово;
|
||||
- [install_windows/README.txt](../device_firmware/multi_libreVNA_firmware/reproduce/install_windows/README.txt)
|
||||
—
|
||||
сборка мастера с переключением стимула на Windows;
|
||||
- [per_point_switching.md](../device_firmware/per_point_switching.md) — переключение
|
||||
стимула: раскладка стадий, изменения FPGA и микроконтроллера, порядок ввода в
|
||||
строй;
|
||||
- [switch_controller/README.md](../device_firmware/switch_controller/README.md) —
|
||||
контроллер ключей: разводка, отчёт, отладка;
|
||||
- [librevna_protocol.md](../device_firmware/librevna_protocol.md) — USB-протокол
|
||||
LibreVNA и форматы кадров между процессами программы.
|
||||
@@ -0,0 +1,238 @@
|
||||
# Установка и запуск
|
||||
|
||||
Программа работает в Linux: на рабочей станции с экраном и на Raspberry Pi 5 без
|
||||
оператора. Python-часть ставится в виртуальное окружение `.venv` внутри каталога
|
||||
проекта, C++-часть собирается командой `make` в `build/bin/`.
|
||||
|
||||
## Что нужно
|
||||
|
||||
| Что | Зачем |
|
||||
| --- | --- |
|
||||
| Linux, bash, coreutils, `flock` | запуск через `start.sh` |
|
||||
| Python 3.10 или новее с модулем `venv` | программа и окружение |
|
||||
| компилятор C++20 (g++), `make` | процессы конвейера |
|
||||
| библиотеки `libusb-1.0` и `libatomic.so.1` | USB-доступ к LibreVNA и кольца разделяемой памяти |
|
||||
| `libxcb-cursor0`, `libxkbcommon-x11-0` (Debian/Ubuntu) | окно Qt на рабочем столе |
|
||||
| видеокарта NVIDIA с драйвером, NVRTC и заголовки CUDA | только для режима 3D SAR |
|
||||
| библиотеки L-Card `libx502api.so`, `libe502api.so` | только для радиофотонного радара с АЦП E-502 |
|
||||
|
||||
Python-пакеты перечислены в `requirements.txt`:
|
||||
|
||||
| Пакет | Для чего |
|
||||
| --- | --- |
|
||||
| `numpy` | массивы данных |
|
||||
| `scipy` | оптимизация в калибровке по объекту |
|
||||
| `libusb1` | USB-драйвер LibreVNA |
|
||||
| `pyserial` | последовательный порт платы лазеров |
|
||||
| `PyQt6` | окно программы (без экрана — тоже, в режиме `offscreen`) |
|
||||
| `pyqtgraph` | графики |
|
||||
| `fastapi`, `uvicorn[standard]` | веб-интерфейс |
|
||||
| `nvidia-cuda-nvrtc`, `nvidia-cuda-runtime` | NVRTC и заголовки CUDA для 3D SAR; ставятся только на x86_64 |
|
||||
|
||||
PyQt6 ставится только из готовых сборок (wheel), поэтому pip выбирает самую новую
|
||||
версию, которая работает с системной glibc.
|
||||
|
||||
## Установка
|
||||
|
||||
Из каталога проекта:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install --upgrade pip
|
||||
.venv/bin/python -m pip install -r requirements.txt
|
||||
make # build/bin/data_preprocessor и build/bin/data_processor
|
||||
make rph_radar_collector # только для радиофотонного радара с АЦП E-502
|
||||
```
|
||||
|
||||
`./start.sh` при обычном запуске делает это сам: создаёт `.venv`, если его нет, ставит
|
||||
недостающие пакеты, ставит системные библиотеки через менеджер пакетов (apt, dnf, yum,
|
||||
pacman, zypper или brew, через `sudo`), правило udev для LibreVNA и собирает C++. Без
|
||||
экрана (`--headless`) он ничего не ставит: всё должно быть установлено заранее.
|
||||
|
||||
## Запуск
|
||||
|
||||
```sh
|
||||
./start.sh --profile run_config_examples/run_config_librevna_multi_per_point.example.json
|
||||
```
|
||||
|
||||
Без `--profile` программа берёт последний использованный профиль, а если его нет —
|
||||
корневой `run_config.json` (симулятор). Порядок выбора профиля — в главе
|
||||
[Файл конфигурации](configuration.md). Но сам `start.sh` без `--profile` определяет
|
||||
модель радара, решает, собирать ли коллектор E-502, и проверяет программы по
|
||||
корневому `run_config.json`, а `--producer-only` берёт именно его. Поэтому с профилем
|
||||
радиофотонного радара и для `--producer-only` всегда указывайте `--profile`.
|
||||
|
||||
| Ключ `start.sh` | Что делает |
|
||||
| --- | --- |
|
||||
| `--profile ПУТЬ` | профиль для запуска; файл должен существовать. Относительный путь — от каталога проекта |
|
||||
| `--headless` | без окна: окно не показывается, через 0,5 с выполняется **Apply Radar**, ещё через 1 с — **Start**; если **Start** не удался, программа завершается с кодом 1 (`Headless auto-start did not bring the pipeline up`). Ничего не устанавливает |
|
||||
| `--auto-start` | с окном, но **Start** выполняется сам через 0,5 с после запуска |
|
||||
| `--producer-only` | запустить только процесс сбора для профиля, без окна; вывод — в терминал |
|
||||
| `--skip-build` | не собирать C++ (наличие программ всё равно проверяется) |
|
||||
| `--build-only` | подготовить окружение, собрать и выйти |
|
||||
| `--clean-shm` | удалить известные кольца в `/dev/shm` перед запуском (глава [Данные на диске](data.md)) |
|
||||
| `-h`, `--help` | справка |
|
||||
|
||||
Порядок работы `start.sh`: проверка окружения → определение модели радара по профилю
|
||||
→ системные библиотеки и правило udev (не для `--headless`) → Python-пакеты → сборка
|
||||
C++ (для радиофотонного радара — и коллектора) → проверка программ в `build/bin/` →
|
||||
остановка службы `radar.service`, если она работает → блокировка
|
||||
`/tmp/radar_system.lock` → запуск программы. Если блокировку держит другой
|
||||
экземпляр, `start.sh` завершается с
|
||||
`Another radar instance already holds /tmp/radar_system.lock; aborting.`
|
||||
|
||||
Для разработки окно можно запустить и напрямую:
|
||||
`.venv/bin/python -m python_app.gui.main`. Тогда нет ни сборки, ни блокировки.
|
||||
|
||||
### Первый запуск без железа
|
||||
|
||||
1. Запустите `./start.sh` (корневой профиль — симулятор).
|
||||
2. Нажмите **Preprocessing…**, затем **Create Neutral S21 Sets**: предобработке
|
||||
нужны наборы калибровки и референса, нейтральные данные не меняют.
|
||||
3. Нажмите **Start**. Симулятор показывает точечный отражатель, который движется
|
||||
по сцене; режим обработки меняется списком **Mode**.
|
||||
|
||||
Режим 3D SAR без видеокарты NVIDIA показывает только предупреждение.
|
||||
|
||||
### Первый запуск с железом
|
||||
|
||||
1. Скопируйте нужный профиль из `run_config_examples/` и проверьте в нём серийные
|
||||
номера, порты, линии GPIO и геометрию антенн (глава
|
||||
[Файл конфигурации](configuration.md)).
|
||||
2. Запустите `./start.sh --profile <профиль>`. При первом запуске он может спросить
|
||||
пароль `sudo`, чтобы поставить системные библиотеки и правило udev для LibreVNA.
|
||||
3. Нажмите **Apply Radar**, снимите калибровку и референс (глава
|
||||
[Калибровка и предобработка](preprocessing.md)) и нажмите **Start**.
|
||||
4. Нажмите **Save Config…**: выбор наборов и правки в окне сохраняются только так.
|
||||
|
||||
Порядок работы по шагам — в главе [Типовые сценарии](workflow.md).
|
||||
|
||||
## Видеокарта для 3D SAR
|
||||
|
||||
Ядра 3D SAR компилируются при запуске библиотекой NVRTC под установленную карту.
|
||||
`make` ищет `nvrtc.h`, `cuda.h` и `libnvrtc` в пакетах `nvidia-cuda-*` из `.venv` и в
|
||||
`/usr/local/cuda`, а `libcuda.so.1` (драйвер) — в системных каталогах. Если чего-то
|
||||
нет, `make` пишет `note: CUDA not found, selecting sar3d will show a warning` и
|
||||
собирает процессор без 3D SAR. После установки CUDA снова запустите `make`: он
|
||||
заметит изменение и пересоберёт нужную часть.
|
||||
|
||||
- На x86_64 CUDA 13 ставится из `requirements.txt`. Целевая карта — RTX 5070.
|
||||
- NVRTC 13 не компилирует под карты старее поколения sm_75 (например, GTX 1060,
|
||||
sm_61). Для них поставьте NVRTC 12 отдельно от `.venv` и соберите в отдельный
|
||||
каталог:
|
||||
|
||||
```sh
|
||||
pip install --target ~/.cache/radar_nvrtc12 nvidia-cuda-nvrtc-cu12 nvidia-cuda-runtime-cu12
|
||||
make BUILD_DIR=/tmp/build-nvrtc12 CUDA_ROOTS="$HOME/.cache/radar_nvrtc12/nvidia/cuda_nvrtc $HOME/.cache/radar_nvrtc12/nvidia/cuda_runtime"
|
||||
```
|
||||
|
||||
Программа запускает процессор только из `build/bin/`, поэтому такая сборка нужна
|
||||
для проверки на старой карте (глава [Разработка](development.md)).
|
||||
- На Jetson CUDA берётся из JetPack (`/usr/local/cuda`); pip-пакеты CUDA там не
|
||||
ставятся. PyQt6 на Jetson с glibc 2.35 — версии 6.7.
|
||||
|
||||
## Служба без экрана
|
||||
|
||||
На машине без оператора программу запускает служба systemd. Установка — один раз,
|
||||
от обычного пользователя через `sudo`:
|
||||
|
||||
```sh
|
||||
sudo bash deploy/install-daemon.sh
|
||||
```
|
||||
|
||||
Скрипт создаёт `/etc/systemd/system/radar.service`, разрешает пользователю без пароля
|
||||
`sudo systemctl stop|start|restart radar.service` (`/etc/sudoers.d/radar`) и включает
|
||||
службу при загрузке, но не запускает её. Служба работает от имени этого пользователя
|
||||
в каталоге проекта, запускает `start.sh --headless --skip-build`, перезапускается через
|
||||
3 с после сбоя, а при остановке ждёт программу до 25 с.
|
||||
|
||||
```sh
|
||||
sudo systemctl start radar.service # запустить
|
||||
systemctl status radar.service # состояние
|
||||
journalctl -u radar.service -f # журнал
|
||||
sudo systemctl disable --now radar.service && sudo rm /etc/systemd/system/radar.service /etc/sudoers.d/radar && sudo systemctl daemon-reload # удалить
|
||||
```
|
||||
|
||||
Служба берёт последний использованный профиль. Перед её запуском откройте окно
|
||||
программы на этой машине, загрузите профиль с железом, выберите наборы и нажмите
|
||||
**Save Config…**. Работа со службой — в разделе «Работа без экрана» главы
|
||||
[Веб-интерфейс, кнопка и локатор](web.md).
|
||||
|
||||
## Машина развёртывания
|
||||
|
||||
Raspberry Pi 5 Model B (Cortex-A76 ×4, 4 ГиБ), Debian 13, Python 3.13. Проект лежит в
|
||||
`~/Documents/radar_system`. На машине собираются `.venv` и C++; копировать их с
|
||||
рабочей станции нельзя: там другая архитектура (x86_64).
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv && .venv/bin/python -m pip install -r requirements.txt
|
||||
make -j4
|
||||
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
|
||||
```
|
||||
|
||||
На Pi нужна только часть проекта: `data_acq_and_processing`, `deploy`, `python_app`,
|
||||
`run_config_examples`, `Makefile`, `requirements.txt`, `run_config.json`, `start.sh`.
|
||||
Каталог `device_firmware/` и документация не нужны.
|
||||
|
||||
> **Внимание.** Номер GPIO-чипа 40-контактного разъёма на Pi 5 зависит от версии
|
||||
> ядра. После установки и обновления ядра проверьте его: `gpiodetect` и
|
||||
> `gpioinfo -c gpiochip0`. Неверный чип в профиле — ключи, которые молча не
|
||||
> переключаются.
|
||||
|
||||
## Обновление с флешки
|
||||
|
||||
На рабочей станции:
|
||||
|
||||
```sh
|
||||
deploy/pack-usb.sh /media/<пользователь>/<флешка>
|
||||
```
|
||||
|
||||
Без аргумента скрипт берёт единственную подключённую съёмную флешку. В каталог
|
||||
`radar_update/` на ней он кладёт архив `radar_system-<дата коммита>-<хеш>.tar.gz`
|
||||
с частью проекта для Pi из **последнего коммита** (незакоммиченные изменения в него не
|
||||
попадают) и скрипт `update.sh`.
|
||||
|
||||
На машине, от обычного пользователя (не через `sudo`), из каталога, где лежит
|
||||
`radar_update`:
|
||||
|
||||
```sh
|
||||
bash radar_update/update.sh # перенести текущий .venv
|
||||
bash radar_update/update.sh --python 3.13 # собрать .venv заново этим Python
|
||||
```
|
||||
|
||||
Скрипту можно передать путь к архиву: `bash radar_update/update.sh <архив>`; без него
|
||||
берётся последний по имени `radar_system-*.tar.gz` рядом со скриптом.
|
||||
|
||||
Перед обновлением закройте окно программы, иначе скрипт откажется и покажет, какие
|
||||
процессы работают в каталоге. Службу он остановит сам и запустит снова, если она
|
||||
работала.
|
||||
|
||||
Что делает `update.sh`:
|
||||
|
||||
1. проверяет архив и переименовывает текущую версию в `radar_system<N>` — следующий
|
||||
свободный номер; это копия для отката;
|
||||
2. разворачивает новую версию в `~/Documents/radar_system` (другой каталог задаёт
|
||||
переменная `RADAR_DIR`);
|
||||
3. переносит из прежней версии: `run_config.json`; профили из
|
||||
`run_config_examples/`, изменённые на машине (версия из архива ложится рядом с
|
||||
окончанием `.new`); `python_app/runtime/` без журналов; данные
|
||||
`python_app/data*` и чужие каталоги верхнего уровня — жёсткими ссылками, без
|
||||
копирования и места на диске; `.venv` — копией;
|
||||
4. ставит пакеты из `requirements.txt` и собирает C++ (`make all rph_radar_collector`);
|
||||
5. записывает имя архива в файл `RELEASE` и запускает службу, если она работала.
|
||||
|
||||
Если что-то пошло не так, скрипт печатает команду отката. Вручную:
|
||||
|
||||
```sh
|
||||
sudo systemctl stop radar.service
|
||||
mv ~/Documents/radar_system ~/Documents/radar_system_failed
|
||||
mv ~/Documents/radar_system<N> ~/Documents/radar_system
|
||||
sudo systemctl start radar.service
|
||||
```
|
||||
|
||||
Новую версию при откате лучше переименовать, а не удалять: в ней могут быть
|
||||
измерения, снятые после обновления.
|
||||
|
||||
На свежую машину (без прежней версии) `update.sh` спрашивает
|
||||
`Install a fresh one there? [y/N]` и ставит корневой `run_config.json` из архива, то есть
|
||||
симулятор: профиль с железом нужно выбрать после установки.
|
||||
@@ -0,0 +1,300 @@
|
||||
# Калибровка и предобработка
|
||||
|
||||
Препроцессор (`data_preprocessor`) по очереди применяет к каждой коллекции четыре
|
||||
шага и только потом отдаёт её на обработку. Зачем нужен каждый шаг, объяснено в
|
||||
главе [Как работает радар](principles.md).
|
||||
|
||||
## Цепочка предобработки
|
||||
|
||||
Для каждой пары и каждой частотной точки:
|
||||
|
||||
| Шаг | Формула | Набор |
|
||||
| --- | --- | --- |
|
||||
| 1. Калибровка | S21 ← S21 / S21_through | набор калибровки (through) |
|
||||
| 2. Вычитание референса | S21 ← S21 − S21_ref / S21_through | набор референса |
|
||||
| 3. Режекторный фильтр | S21 ← S21 · m(f) | настройки `preprocess.notch` в профиле |
|
||||
| 4. Калибровка по объекту | S21 ← S21 · F | набор калибровки по объекту, если выбран |
|
||||
|
||||
Референс делится на through один раз при загрузке, поэтому вычитается уже
|
||||
откалиброванный фон. Точки, где |S21_through|² не больше 10⁻¹², на шаге 1 остаются как
|
||||
есть, без деления. Окно съёма и номер коллекции не меняются.
|
||||
|
||||
У каждого набора должна быть каждая пара сбора, а сетка частот — совпадать с
|
||||
развёрткой: то же число точек, а каждая частота отличается от своей не больше чем на
|
||||
10⁻⁵ её значения, но допуск не меньше 1 мГц (на 6 ГГц — 60 кГц). Наборы проверяются при запуске
|
||||
препроцессора, и неподходящий набор его останавливает. Если это случилось, пока
|
||||
программа ждёт готовности процессов, **Start** отказывает с сообщением
|
||||
``Failed to start pipeline: RuntimeError: Process `data_preprocessor` exited with code 1.``
|
||||
и текстом ошибки. Если позже, в журнале окна появляется отчёт о завершении
|
||||
процесса, а программа перезапускает конвейер с нарастающей паузой.
|
||||
|
||||
Коллекцию, которая не прошла проверку уже во время
|
||||
работы, препроцессор отбрасывает и пишет об этом в свой журнал
|
||||
`python_app/runtime/logs/data_preprocessor.err.log`. В окне программы это видно
|
||||
только по тому, что изображение перестаёт обновляться.
|
||||
|
||||
## Наборы
|
||||
|
||||
| Набор | Каталог | Обязателен |
|
||||
| --- | --- | --- |
|
||||
| S21 Calibration — калибровка (through) | `python_app/data/s21_calibration/` | да |
|
||||
| S21 Reference — референс | `python_app/data/s21_reference/` | да |
|
||||
| Object Calibration — калибровка по объекту | `python_app/data/s21_object_calibration/` | нет |
|
||||
|
||||
Набор — архив NPZ с массивами частот и S21 по парам и JSON-файл рядом с описанием
|
||||
(формат — в главе [Данные на диске](data.md)). Наборы лежат в подкаталогах с именем
|
||||
`radar_key` — ключа конфигурации радара:
|
||||
|
||||
- у LibreVNA ключ складывается из модели, серийных номеров, начала и конца
|
||||
развёртки, числа точек, полосы ПЧ и мощности, а у многоприборной схемы ещё из
|
||||
положений ключей;
|
||||
- у радиофотонного радара ключ — хеш выходной полосы, фазовой калибровки, параметров
|
||||
источника, положений ключей и настроек лазера; лазер входит в ключ, если плата
|
||||
лазеров включена в профиле или источник — плата RFG.
|
||||
|
||||
Программа показывает только наборы текущего ключа. Если изменить любую из этих
|
||||
величин (например, число точек), старые наборы пропадают из списков, а выбор
|
||||
сбрасывается с записью `Preprocess selection reset: <старый ключ> -> <новый ключ>; cleared {...}`
|
||||
в журнале. Так программа не даёт применить калибровку, снятую при других
|
||||
настройках. Если вернуть прежние настройки, наборы снова появятся в списках, но
|
||||
выбрать их придётся заново.
|
||||
|
||||
Наборы применяются при запуске сбора: программа пересобирает из выбранных наборов
|
||||
файлы `python_app/runtime/*_bundle.bin` и заново запускает препроцессор. Запуск — это
|
||||
**Start**, а также продолжение сбора после приостановки: после съёма набора,
|
||||
временного референса или нейтральных наборов сбор, если он шёл, продолжается уже с
|
||||
новыми наборами. Смена выбора в списке во время сбора применится при следующем
|
||||
запуске.
|
||||
|
||||
Перед запуском программа проверяет наборы и, если что-то не так, отказывается
|
||||
запускаться, ничего не остановив. Окно ошибки показывает
|
||||
`Failed to start pipeline: <тип ошибки>: <текст>`, где текст — один из этих:
|
||||
|
||||
| Условие | Сообщение |
|
||||
| --- | --- |
|
||||
| не выбраны калибровка или референс | `Select all required preprocess sets in Preprocessing Panel before Start: S21 Calibration, S21 Reference` |
|
||||
| файла набора нет для текущего ключа | `Missing set files for <вид>/<radar_key>/<имя>` |
|
||||
| в наборе нет какой-то пары сбора | `Selected <набор> set does not cover requested run combos` |
|
||||
| архив повреждён или содержит не числа | `Corrupted preprocess set <вид>/<ключ>/<имя>: …` |
|
||||
| калибровка по объекту не подходит | см. раздел «Калибровка по объекту» |
|
||||
|
||||
## Окно Preprocessing Setup
|
||||
|
||||
Открывается кнопкой **Preprocessing…**. Окно не модальное: пока оно открыто, главным
|
||||
окном можно пользоваться.
|
||||
|
||||
Группа **Preprocess Sets**:
|
||||
|
||||
| Элемент | Что делает |
|
||||
| --- | --- |
|
||||
| **Set name** | имя нового набора; по умолчанию `set_001`. Для этого вида набора и этого ключа радара имя должно быть новым |
|
||||
| **Create Neutral S21 Sets** | создаёт нейтральные наборы (см. ниже). Кнопка есть только у радиофотонного радара, у многоприборной схемы и у одиночного LibreVNA в режиме `mock` |
|
||||
| **Refresh Sets** | перечитывает списки наборов с диска и каталог вариантов радара |
|
||||
| **Sweeps per combo (median)** | сколько развёрток снимать на каждую пару, от 1 до 99, по умолчанию 5. В набор идёт медиана по каждой частоте, отдельно по вещественной и мнимой части: при трёх развёртках и больше одиночный сбой развёртки в набор не попадает. Это же число развёрток берут временный референс и калибровка по объекту |
|
||||
| **Radar Config Variants** → **Directory**, **Browse…** | каталог с профилями JSON для съёма наборов сразу под несколько настроек развёртки (см. ниже). Под полем — итог просмотра: `Directory: <каталог> \| json=…, valid=…, skipped=…, duplicates=…` (файлов JSON, подходящих вариантов, пропущенных, повторов) или `No radar config directory selected.` |
|
||||
| **Capture current combo for all found radar configs** | включает съём по всем вариантам из каталога |
|
||||
| **S21 calibration**, **S21 reference** | выбор набора калибровки и референса для следующего **Start** |
|
||||
| **Object calibration** | выбор калибровки по объекту; **Off** — не применять |
|
||||
| **Create…** | открывает окно калибровки по объекту |
|
||||
|
||||
Группа **Capture All Antenna Pairs**:
|
||||
|
||||
| Элемент | Что делает |
|
||||
| --- | --- |
|
||||
| **Active type**, **Progress**, **Current combo** | вид снимаемого набора, сколько пар снято из скольких, какая пара следующая; без съёма — `<none>`, `0 / 0`, после последней пары — `<complete>` |
|
||||
| **Start S21 Calibration**, **Start S21 Reference** | начинают съём набора |
|
||||
| **Capture Current Combo** | снимает следующую пару |
|
||||
| **Capture All Remaining** | снимает подряд все оставшиеся пары |
|
||||
| **Undo Last Capture** | отменяет последний съём, чтобы переснять пару |
|
||||
| **Save Captured Set** | сохраняет набор и выбирает его; доступна, когда сняты все пары |
|
||||
| **Abort Sequence** | прерывает съём и отбрасывает снятое |
|
||||
|
||||
Ниже — журнал съёма (`S21 Calibration: 1/8 | input=0 output=0` на каждую пару),
|
||||
строка состояния (сначала `Ready`) и графики модуля и фазы последней снятой развёртки
|
||||
(до первого съёма — `Preview will appear after the first successful capture.`).
|
||||
Последняя развёртка рисуется и в главном окне.
|
||||
|
||||
## Съём калибровки и референса
|
||||
|
||||
1. Выберите профиль и настройки радара. Наборы привязаны к ключу радара, поэтому
|
||||
снимайте их при тех настройках развёртки, с которыми будете работать.
|
||||
2. Подготовьте установку. Для калибровки — эталонный тракт для текущей пары; для
|
||||
референса — пустую сцену: всё, что должно остаться в фоне, на месте, целей нет.
|
||||
3. Нажмите **Preprocessing…**, задайте **Set name** и
|
||||
**Sweeps per combo (median)**.
|
||||
4. Нажмите **Start S21 Calibration** или **Start S21 Reference**. Сбор
|
||||
приостанавливается, и на время съёма прибором управляет окно предобработки.
|
||||
5. Снимайте пары кнопкой **Capture Current Combo** или все сразу кнопкой
|
||||
**Capture All Remaining**. Проверяйте каждую развёртку по графикам; неудачную
|
||||
переснимите через **Undo Last Capture**. При ошибке съёма уже снятые пары
|
||||
сохраняются, и съём можно повторить.
|
||||
6. Когда сняты все пары, нажмите **Save Captured Set**. Набор сохраняется и
|
||||
выбирается, приостановленный сбор продолжается.
|
||||
|
||||
Съём всегда идёт по полной матрице положений ключей, а не только по парам сбора.
|
||||
|
||||
- Одиночный LibreVNA: для каждой пары программа выставляет ключи, ждёт время
|
||||
установления (`run.settling_ms`, в окне — **Settling (ms)**) и снимает нужное
|
||||
число развёрток этой пары.
|
||||
- Многоприборная схема при калибровке: прибор снимает всю матрицу, а в набор идёт
|
||||
только текущая пара, по одной за нажатие. **Capture All Remaining** здесь
|
||||
недоступна.
|
||||
- Многоприборная схема при съёме референса: одно нажатие снимает всю матрицу нужное
|
||||
число раз, и набор сразу готов.
|
||||
|
||||
### Варианты радара
|
||||
|
||||
Иногда наборы нужны сразу под несколько развёрток — например, под разные полосы
|
||||
частот. Для этого в **Directory** указывают каталог с профилями JSON. Профиль
|
||||
считается вариантом, если в его `radar.sweep` есть хотя бы одно из полей
|
||||
`start_hz`, `stop_hz`, `points`, `if_bandwidth_hz`, `stimulus_power_dbm`; эти поля
|
||||
накладываются на текущий профиль, повторы одного ключа пропускаются. С включённой
|
||||
**Capture current combo for all found radar configs** каждое нажатие снимает пару во
|
||||
всех вариантах подряд, с перенастройкой прибора. **Save Captured Set** сохраняет
|
||||
по набору на каждый вариант под одним именем, а картинки развёрток — в
|
||||
`python_app/data/preview_png/<вид>/<имя>/<radar_key>/i<вход>_o<выход>.png`.
|
||||
|
||||
## Временный референс
|
||||
|
||||
Кнопка **Capture Temporary Reference** в секции **Data Actions** снимает референс
|
||||
одним нажатием, не открывая окно предобработки. То же действие есть в
|
||||
веб-интерфейсе и на GPIO-кнопке.
|
||||
|
||||
Программа приостанавливает сбор, снимает полную матрицу с текущими настройками
|
||||
(медиана **Sweeps per combo (median)** развёрток) и сохраняет её как набор
|
||||
`tmp_reference`, заменяя прежний. Затем она выбирает этот набор, очищает историю
|
||||
обработки, рисует последнюю развёртку с подписью `Tmp S21 Reference last trace` и
|
||||
продолжает сбор. Процессор и локатор при этом не перезапускаются, клиенты локатора
|
||||
не отключаются.
|
||||
|
||||
Временный референс нужен, когда сцена или положение радара изменились: после
|
||||
перестановки, на новом месте, после заметного изменения обстановки.
|
||||
|
||||
## Нейтральные наборы
|
||||
|
||||
**Create Neutral S21 Sets** сохраняет под именем из **Set name** два набора:
|
||||
калибровку, равную 1, и референс, равный 0, для всех пар на текущей сетке частот.
|
||||
С ними данные проходят предобработку без изменений. Они нужны, чтобы запустить
|
||||
конвейер, пока настоящей калибровки нет: с симулятором, при первой проверке прибора.
|
||||
Если набор с таким именем уже есть, программа откажется. После создания наборы
|
||||
выбираются, история очищается, а процессор перезапускается: сразу, если сбор шёл,
|
||||
иначе при следующем **Start**.
|
||||
|
||||
## Режекторный фильтр
|
||||
|
||||
Фильтр настраивается только в профиле, в разделе `preprocess.notch`; в окне
|
||||
программы его нет.
|
||||
|
||||
| Ключ | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| `enabled` | `false` | включает фильтр |
|
||||
| `bands_hz` | `[]` | полосы подавления: список пар `[нижняя, верхняя]` в герцах, верхняя не меньше нижней |
|
||||
| `taper_width_hz` | `40e6` | ширина плавного края w, Гц, не меньше 0 |
|
||||
| `taper_type` | `"cosine"` | форма края: `"cosine"` — косинусный спад, `"hard"` — без спада |
|
||||
|
||||
Внутри полосы [L, H] множитель m(f) равен нулю. При `"cosine"` по краям полосы он
|
||||
плавно спадает от 1 до 0 по закону 0,5·(1 + cos(…)) на участках [L − w, L] и
|
||||
[H, H + w]. При `"hard"` (или w = 0) зануляется вся полоса [L − w, H + w]. Края
|
||||
соседних полос перемножаются.
|
||||
|
||||
Во всех профилях-примерах `enabled: true` и `bands_hz: []`, то есть фильтр ничего не
|
||||
подавляет. Изменение фильтра применяется при следующем **Start** и сбрасывает
|
||||
историю.
|
||||
|
||||
## Калибровка по объекту
|
||||
|
||||
Калибровка по объекту находит для каждой пары комплексную поправку по частоте так,
|
||||
чтобы отклик известного отражателя у всех пар совпадал по фазе. Эталон — ноутбук
|
||||
`MIMO_calibration.ipynb`, он хранится вне репозитория. Поправку можно строить по
|
||||
нескольким положениям отражателя: они уточняют друг друга.
|
||||
|
||||
Что нужно заранее:
|
||||
|
||||
- кольцо `rings.preprocessed_tap` в профиле: калибровка снимает данные из него;
|
||||
- таблицы геометрии антенн: в калибровку входят только пары, у которых есть
|
||||
геометрия обеих антенн, остальные получают поправку 1;
|
||||
- выбранный набор калибровки (through): поправка запоминает его имя и содержимое и
|
||||
годится только вместе с ним;
|
||||
- металлический шар известного радиуса (или другой точечный отражатель) и способ
|
||||
измерить положение его центра.
|
||||
|
||||
Окно **MIMO Object Calibration** открывается кнопкой **Create…** в окне
|
||||
предобработки. Вверху — подсказка о порядке работы
|
||||
(`Capture data, then enter the measured reflector centre: …`), внизу — строка
|
||||
состояния (сначала `Ready to capture`), а на месте карт фаз до расчёта —
|
||||
`Pair phases at the target will appear after calculation.`
|
||||
|
||||
| Элемент | Что делает |
|
||||
| --- | --- |
|
||||
| **Set name** | имя набора поправки |
|
||||
| таблица **Target**, **Sweeps**, **X (m)**, **Y (m)**, **Z (m)** | снятые положения отражателя: сколько коллекций снято и измеренный центр |
|
||||
| **Capture Data** | снимает данные для нового положения отражателя |
|
||||
| **Remove Selected Target** | удаляет выбранное положение |
|
||||
| **Sphere radius (m, 0 = point)** | радиус шара, по умолчанию 0,16 м; 0 — точечный отражатель |
|
||||
| **Antenna origin height above ground (m)** | высота начала осей антенн над землёй, по умолчанию 0,92 м |
|
||||
| **Radar downward tilt (°)** | наклон радара вниз, по умолчанию 10° |
|
||||
| **Calculation Settings** | раскрывает остальные параметры расчёта (таблица ниже) |
|
||||
| **Calculate and Save** | считает поправку в фоне и сохраняет набор |
|
||||
| **Cancel Operation** | прерывает съём или расчёт |
|
||||
| **Close** | закрывает окно |
|
||||
|
||||
Параметры в **Calculation Settings**:
|
||||
|
||||
| Параметр | По умолчанию | Что делает |
|
||||
| --- | --- | --- |
|
||||
| **Estimation band start (MHz)**, **Estimation band stop (MHz)** | 2000 и 6000 | полоса, по которой оценивается поправка; в ней должно быть не меньше 4 точек |
|
||||
| **Echo path gate half-width (m)** | 0,15 | полуширина окна вокруг ожидаемого пути до отражателя, в котором ищется его эхо |
|
||||
| **Gate taper width (m)** | 0,03 | ширина плавного края этого окна |
|
||||
| **Coarse path search (±m, 0 = off)** | 0,6 | предварительный поиск: на сколько можно сдвинуть отражатель по дальности до самого сильного эха |
|
||||
| **Coordinate refinement (±m)** | 0,1 | в каких пределах уточнять координаты отражателя |
|
||||
| **Refine coordinates** X, Y, Z | все три | по каким осям уточнять координаты |
|
||||
| **Antenna phase model** | `linear` | модель фазы антенны по частоте: `const` — постоянная, `linear` — линейная (задержка), `poly3` — многочлен третьей степени, `raw` — без сглаживания |
|
||||
| **Equalize amplitudes** | выключен | выравнивать и амплитуды, а не только фазы |
|
||||
|
||||
Порядок:
|
||||
|
||||
1. Поставьте отражатель в сцену и измерьте положение его центра в осях сцены: X —
|
||||
вдоль ряда антенн, Y — высота над землёй, Z — вдоль земли от радара.
|
||||
2. Нажмите **Capture Data**. Программа приостанавливает текущий сбор и запускает
|
||||
конвейер с этим же профилем, но без калибровки по объекту и по всем парам из
|
||||
таблиц геометрии (многоприборная схема — по полной матрице). Она собирает
|
||||
столько новых коллекций, сколько задано в
|
||||
**Sweeps per combo (median)**, и возвращает прежний режим работы. Предел ожидания —
|
||||
300 с.
|
||||
3. Введите X, Y, Z в строке нового положения.
|
||||
4. Повторите для других положений, если нужно. Все положения должны быть сняты при
|
||||
одних и тех же настройках радара, геометрии и предобработке.
|
||||
5. Нажмите **Calculate and Save**. Расчёт идёт в фоне. После него окно показывает
|
||||
карты фаз пар в точке отражателя до и после поправки (**Before**, **After**):
|
||||
строки — передающие антенны, столбцы — приёмные, фаза — относительно суммы по
|
||||
всем парам, по шкале ±180°, с разбросом в градусах.
|
||||
**Capture Data** отказывает, если идёт однократный съём или запись на диск
|
||||
(`Finish the single capture or disk recording before taking calibration data`), если
|
||||
нет таблиц геометрии (`Configure Tx/Rx antenna positions before taking calibration data`)
|
||||
или кольца копии
|
||||
(`Object calibration requires rings.preprocessed_tap; enable it in the run profile`).
|
||||
6. Если набор подходит к текущим настройкам, он выбирается и применяется при
|
||||
следующем **Start** (`Applies on the next Start.`). Иначе он сохраняется
|
||||
невыбранным с объяснением `Not selected for the current configuration: <причина>`.
|
||||
|
||||
Коллекции одного положения усредняются. По каждой частоте поправка раскладывается на
|
||||
множители передающих и приёмных антенн; фазы сглаживаются выбранной моделью и
|
||||
продолжаются на всю сетку частот. Поправка пары — сопряжённая фаза произведения
|
||||
множителей, а с **Equalize amplitudes** — обратное произведение.
|
||||
|
||||
Перед каждым **Start** программа проверяет, что поправка снята при тех же настройках
|
||||
радара, той же геометрии и с тем же набором калибровки (по имени и содержимому), что
|
||||
она и калибровка покрывают все пары сбора, не содержит нулей и имеет ту же сетку
|
||||
частот. Сообщения при отказе:
|
||||
|
||||
- `Object calibration has no capture provenance; capture it again`;
|
||||
- `Object calibration radar configuration differs from the captured state` (а также
|
||||
`antenna geometry`, `through calibration name`, `through calibration content`);
|
||||
- `Object calibration and through must cover input=i, output=o`;
|
||||
- `Object calibration corrections must be nonzero`;
|
||||
- `Object calibration frequency grid differs for input=i, output=o`.
|
||||
|
||||
Если изменить таблицу геометрии так, что она перестала совпадать с геометрией
|
||||
выбранной поправки, выбор поправки снимается, а работающий с ней сбор
|
||||
приостанавливается и продолжается уже без неё.
|
||||
|
||||
@@ -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).
|
||||
@@ -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 м/с.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Неполадки
|
||||
|
||||
Сначала посмотрите журнал окна (секция **Runtime Log**) и строку состояния. Если там
|
||||
пусто, а картинка не обновляется, причина обычно в журналах процессов в
|
||||
`python_app/runtime/logs/`:
|
||||
|
||||
| Файл | Что в нём искать |
|
||||
| --- | --- |
|
||||
| `acquisition.err.log` | приборы не открываются, ожидание приборов, ошибки коллектора E-502 |
|
||||
| `data_preprocessor.err.log` | отброшенные коллекции: не та сетка частот, нет пары в наборе |
|
||||
| `data_processor.err.log` | ошибки обработки, отклонённый live-файл, 3D SAR, локатор |
|
||||
| `radar.log` | всё, что было в окне журнала |
|
||||
|
||||
У каждого процесса есть ещё `<процесс>.out.log` (обычный вывод), а перед каждым
|
||||
запуском прежние журналы переименовываются в `*.prev`: после автоматического
|
||||
перезапуска причина падения лежит в `<процесс>.err.log.prev`. Без экрана журнал
|
||||
программы — `journalctl -u radar.service -f`.
|
||||
|
||||
## Программа не запускается
|
||||
|
||||
| Признак | Причина | Что делать |
|
||||
| --- | --- | --- |
|
||||
| окно не открывается, в выводе ошибка о LibreVNA (`No compatible LibreVNA USB device found…`) | профиль с `driver_mode: "native"`, а прибор не подключён: при запуске окно читает пределы прибора | подключите прибор или запустите с профилем симулятора: `./start.sh --profile run_config.json` |
|
||||
| окно открывается с ошибкой `Cannot switch processing mode to …` и закрывается, и так при каждом запуске | запомненный профиль читается, но его настройки нельзя применить (например, `SAR_Y_RANGE_M max` не больше min) | удалите `python_app/runtime/gui_session_state.json` и исправьте профиль |
|
||||
| `Another radar instance already holds /tmp/radar_system.lock; aborting.` | программа уже запущена: в другом окне или службой | закройте другой экземпляр или `sudo systemctl stop radar.service` |
|
||||
| служба перезапускается каждые 3 с | без экрана не удаётся **Start** (часто — не выбраны наборы) или нет программ в `build/bin/` | `journalctl -u radar.service`; выберите наборы и сохраните профиль, запустив окно на машине |
|
||||
| `[start.sh] … Run an interactive './start.sh' once (online)…` | без экрана не хватает Python-пакетов | запустите `./start.sh` один раз с экраном и сетью |
|
||||
|
||||
## Start отказывает
|
||||
|
||||
| Сообщение | Причина | Что делать |
|
||||
| --- | --- | --- |
|
||||
| `Select all required preprocess sets in Preprocessing Panel before Start: …` | не выбраны калибровка или референс | выберите или снимите их в **Preprocessing…**; для симулятора — **Create Neutral S21 Sets** |
|
||||
| наборы пропали из списков, в журнале `Preprocess selection reset: …` | изменилась развёртка, серийные номера или положения ключей — другой ключ радара | верните прежние настройки или снимите наборы заново |
|
||||
| `Selected … set does not cover requested run combos` | набор снят для другой матрицы | снимите набор заново при текущих ключах |
|
||||
| `rings.<имя>.slot_size_bytes must be at least N for this sweep` | слот кольца меньше коллекции | увеличьте `slot_size_bytes` в профиле |
|
||||
| `B-scan with native switches requires exactly one run combo (now N)` | B-scan с настоящими ключами показывает одну пару | задайте одну пару в **Single combo** |
|
||||
| `GPR requires at least 2 distinct run combos (now N)` и другие `GPR …` | для GPR и 3D SAR нужны пары и геометрия | проверьте пары сбора и таблицы геометрии |
|
||||
| `Object calibration … differs from the captured state` | калибровка по объекту снята при других настройках, геометрии или наборе калибровки | выберите **Off** или снимите калибровку по объекту заново |
|
||||
| `Stop all pipeline processes before loading a config profile` | профиль загружают при работающем сборе | нажмите **Stop** |
|
||||
|
||||
## Нет данных от приборов
|
||||
|
||||
| Признак | Причина | Что делать |
|
||||
| --- | --- | --- |
|
||||
| сбор идёт, приборы не опрашиваются, на графиках один движущийся точечный отражатель | профиль симулятора: `driver_mode: "mock"` (например, корневой `run_config.json`) | загрузите профиль с `driver_mode: "native"`; если в журнале `Failed to load the last selected config profile…`, запомненный профиль не прочитался — исправьте его и загрузите снова |
|
||||
| `No datapoints received from at least one device; hardware trigger sync did not start. Check Trigger Out/In loop and 10 MHz reference wiring.` | многоприборная схема: не работает петля триггера | проверьте кабели петли и контроллер STM32 в режиме переключения стимула |
|
||||
| `Packet not received from <серийный номер> within 1.0 s` | включена внешняя опора (`force_external_reference: true`), а 10 МГц не подана | выключите опору в профиле или проверьте, что 10 МГц доходит до всех приборов |
|
||||
| `<прибор> unavailable (attempt N); retrying until present` | прибор не открывается; процесс сбора ждёт его бесконечно | проверьте USB и серийный номер в профиле; правило udev ставит `./start.sh` при обычном запуске |
|
||||
| в режиме переключения стимула приборы падают с `LIBUSB_ERROR_IO` | полоса ПЧ 1 кГц | используйте 5–50 кГц, рабочее значение — 50 кГц |
|
||||
| ключи не переключаются, ошибок нет | неверный GPIO-чип или ключ в `mock` | проверьте `gpiodetect`, `gpioinfo -c gpiochip0` и `driver_mode` ключей |
|
||||
| `Failed to open GPIO chip '…'` или `Failed to request GPIO lines…` | чипа нет или линии заняты другой программой | проверьте чип и что линии свободны (`gpioinfo`) |
|
||||
|
||||
## Радиофотонный радар
|
||||
|
||||
| Сообщение | Причина | Что делать |
|
||||
| --- | --- | --- |
|
||||
| `…sweep rejected (count=n): covered [a, b] Hz does not span band [c, d] Hz…` | развёртка лазера не покрывает выходную полосу | проверьте фазовую калибровку (`rph_radar_calibrate`) и `radar.rph_radar.band` |
|
||||
| `Timed out waiting for a RadioPhotonic radar (RPH) sweep covering the configured band` | за **Read timeout (s)** не пришло ни одной пригодной развёртки | проверьте, что лазер запущен (**Apply Radar**), и сигналы на входах |
|
||||
| `Cannot load libx502api.so: …` | нет библиотек L-Card | положите `libx502api.so` и `libe502api.so` в `~/.local/lib` или задайте `LD_LIBRARY_PATH` в **Environment JSON** |
|
||||
| `Timed out waiting for RadioPhotonic radar (RPH) TTY …` | коллектор не запустился | смотрите строки `Collector: …` в `acquisition.err.log`; проверьте **Executable path** (`make rph_radar_collector`) |
|
||||
| `ADC source clock leads host monotonic clock; restart acquisition` | часы АЦП ушли вперёд часов компьютера | перезапустите сбор |
|
||||
| `Serial port '…' is busy — another program is already using the laser controller…` | порт платы лазеров занят другой программой | закройте её |
|
||||
| `Device rejected command: STATE 0x…` | плата лазеров отвергла команду | проверьте значения токов и температур и питание платы |
|
||||
| `RFG protocol violation: …` | на порт пришёл чужой пакет | привяжите `udp_bind` к проводному адресу, на котором работает плата |
|
||||
|
||||
## Картинка не обновляется или пустая
|
||||
|
||||
| Признак | Причина | Что делать |
|
||||
| --- | --- | --- |
|
||||
| сбор идёт, счётчик `History` растёт только у `raw` | препроцессор отбрасывает коллекции | `data_preprocessor.err.log`: обычно не совпадает сетка частот набора; снимите наборы заново |
|
||||
| GPR пустой | нет антенн с геометрией среди выбранных или **Min depth (m)** не меньше **Max depth (m)** | проверьте поля и таблицы; ошибки — в `data_processor.err.log` |
|
||||
| **Single Capture** в режиме GPR ждёт 300 с и останавливается | результат GPR пустой, и съём его не дождался | проверьте настройки GPR, как выше |
|
||||
| `SAR3D: warning — …` | нет видеокарты NVIDIA или программа собрана без CUDA | глава [Установка и запуск](installation.md), раздел о видеокарте |
|
||||
| `SAR3D: error — the views need N KiB but a results ring slot holds M KiB…` | изображения 3D SAR не помещаются в слот | увеличьте шаги вокселя, сузьте дальность или увеличьте `rings.results.slot_size_bytes` |
|
||||
| `SAR3D: no signal — No signal inside the selected depth range` | в пределах MIN_DEPTH–MAX_DEPTH нет сигнала | проверьте границы дальности и полосу |
|
||||
| кадр 3D SAR пустой, в строке `frame empty` | максимум ниже 6 фонов при включённой **BG_GATE_ENABLE** | это норма для пустой сцены; для проверки выключите **BG_GATE_ENABLE** |
|
||||
| `Stopping pipeline after 400 consecutive reader poll failures` | конвейер остановлен из-за ошибок чтения колец | нажмите **Start**; повторяется — проверьте `/dev/shm` |
|
||||
| `Status: result display error (acquisition continues)` | ошибка отрисовки; сбор продолжается | текст ошибки — в журнале |
|
||||
|
||||
## Прочее
|
||||
|
||||
| Признак | Причина | Что делать |
|
||||
| --- | --- | --- |
|
||||
| после перезапуска пропали настройки и выбор наборов | изменения в окне и вебе не пишутся в профиль сами | **Save Config…** |
|
||||
| отказ в доступе к кольцам (`Permission denied`) | программу или тесты запускали от root, и кольца в `/dev/shm` принадлежат root | при закрытой программе удалите их: `sudo rm -f /dev/shm/radar_*` |
|
||||
| веб-страница не открывается | порт занят или не тот адрес | проверьте вывод программы (`journalctl`) на `address already in use`; другой порт — `RADAR_SYSTEM_WEBUI_PORT` |
|
||||
| `Disk recording stopped because the writer could not keep up` | диск не успевает за записью | пишите на более быстрый диск или меньше коллекций; записанное сохранено |
|
||||
| `Snapshot directory already exists: …` | каталог набора данных уже есть | задайте другое **Name** |
|
||||
| `Log panel overflow: N record(s) dropped…` | журнал в окне переполнился | всё есть в `radar.log` |
|
||||
+226
@@ -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** в браузере.
|
||||
@@ -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. Закройте окно: запись на диск дописывается, процессы конвейера останавливаются.
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user