added docs

This commit is contained in:
Ayzen
2026-10-16 18:35:02 +03:00
parent b88d704126
commit ad2b3e555a
19 changed files with 3719 additions and 663 deletions
+203
View File
@@ -0,0 +1,203 @@
# Устройство программы
## Процессы и кольца
Программа — четыре процесса (с АЦП E-502 — пять: добавляется коллектор), связанные
кольцевыми буферами в разделяемой памяти POSIX:
```text
raw preprocessed results
железо ─► процесс сбора ─────► data_preprocessor ─────────► data_processor ─────► окно
│ │ │
│ raw_tap │ preprocessed_tap └─► TCP-локатор
└─────► окно └─────► окно
```
| Процесс | Что делает | Код |
| --- | --- | --- |
| окно программы (Python, PyQt6) | запускает и перезапускает остальные процессы, читает результаты и копии, рисует, обслуживает веб-интерфейс и GPIO-кнопку | `python_app/gui/` |
| процесс сбора (Python) | управляет железом, публикует коллекции в `raw` | `python_app/scripts/matrix_raw_producer.py`, `rph_radar_raw_producer.py` |
| препроцессор (C++) | калибровка, вычитание референса, режекторный фильтр, калибровка по объекту | `data_acq_and_processing/preprocessing/` |
| процессор (C++) | режимы обработки, локатор | `data_acq_and_processing/processing/` |
Процесс сбора выбирается по `radar.model` в
`python_app/orchestration/producer_selection.py`; той же функцией пользуются окно и
`start.sh --producer-only`. У радиофотонного радара с АЦП E-502 процесс сбора
запускает ещё и C++-коллектор `rph_radar_collector`, который передаёт развёртки через
псевдотерминал.
Кольца работают по правилу «побеждает новое»: писатель никогда не ждёт читателя, при
переполнении затирается самый старый непрочитанный кадр, а читатель, которого
обогнали во время чтения, продолжает с самого свежего. Окно читает только копии
`raw_tap` и `preprocessed_tap`: если бы оно читало основные кольца, оно отбирало бы
кадры у препроцессора и процессора. Пустое имя копии отключает её.
Раскладка кольца — `RDRRING2`, версия 1: заголовок 64 байта, у каждого слота заголовок
16 байт. Код: Python — `python_app/orchestration/shm/ring_layout.py`, C++ —
`common_cpp/ipc/` (`ShmRing`). Владелец заменяет несовместимый сегмент через unlink, не
обрезая отображения читателей.
## Форматы кадров
| Кадр | Где | Содержимое |
| --- | --- | --- |
| RAW3 | `raw`, `raw_tap`, файлы наборов `python_app/runtime/*_bundle.bin` | коллекция: окно съёма, затем для каждой трассы пара, окно съёма, частоты float32 и S21 complex64. Размер — 44 + пары × (28 + 12 × точки) байт |
| PRP3 | `preprocessed`, `preprocessed_tap` | то же после предобработки |
| RSL3 | `results` | заголовок 64 байта (номер коллекции, метка стадии, время обработки, `settings_revision`, окно съёма, использованная скорость, число результатов) и результаты: трасса, число, изображение или таблица, для всей коллекции и для пар |
Окно съёма обязательно; старые форматы RAW2, PRP2 и RSL2 отвергаются. Python и C++
обновляются только вместе. Подробное описание полей — §6
[librevna_protocol.md](../device_firmware/librevna_protocol.md).
## Идентичность и порядок коллекций
- Коллекция определяется парой `(collection_id, capture_start_ns)`. Номер после
**Stop** и **Start** снова начинается с 1, поэтому одного номера мало.
- Окно съёма ставит процесс сбора по часам CLOCK_MONOTONIC; дальше оно не меняется.
У каждой трассы своё окно: при переключении пар окно коллекции про отдельную пару
ничего не говорит.
- История упорядочена по `capture_start_ns`. `monotonic_ns` — диагностическая метка
стадии.
- Сопоставление стадий, **Single Capture**, **Remove Last Measurement**, кэш 3D SAR и
фильтр приближения GPR используют точный ключ (`python_app/models/collection_identity.py`),
без эвристик по близости времён.
Результаты штатно приходят не по порядку: процессор пересчитывает сохранённые
коллекции при смене настроек, а остановленное окно в режиме B-scan отправляет свои
коллекции обратно в `preprocessed`. Поэтому:
- коллекция не новее последней принятой обрабатывается и публикуется, но не
попадает в историю процессора и не уходит локатору;
- при новой ревизии процессор пересчитывает новейшую коллекцию, в B-scan — всю
историю; локатору уходит только новейшая;
- на время такого пересчёта окно пишет `reprocess_current_result: false`, чтобы
процессор не пересчитывал параллельно;
- история окна вставляет результат по времени съёма; на диск пишутся только новые
коллекции;
- удалённые коллекции (`history_clear_before_ns`, `history_removed`) отбрасываются и
процессором, и окном, даже если их результаты придут позже.
## Окно программы
`gui/app_window.py` только создаёт контроллеры, связывает их и закрывает окно.
Раскладку строит `gui/window_view.py`; секции `gui/controllers/sections/` — наборы
виджетов, сигналы подключают контроллеры. Миксинов и фасадов нет.
| Контроллер (`gui/controllers/`) | Отвечает за |
| --- | --- |
| `profile_controller` | профиль: выбор при запуске, загрузка, сохранение, профиль рядом с данными |
| `config_controller` | перевод полей окна в модели и обратно по метаданным; состояния не хранит |
| `radar_settings_controller` | развёртка, АЦП и лазер, пары сбора, пределы прибора |
| `live_settings_controller` | режим обработки, ревизия, live-файл, пакет настроек из веба |
| `pipeline_controller` | процессы, кольца, опрос и восстановление, **Single Capture**, приостановка, пересчёт B-scan |
| `history_controller` | истории стадий, удаления |
| `plot_controller` и `renderers/` | графики, выбор пары B-scan, проверка ревизии, счётчик изменений картинки для веба |
| `snapshot_controller`, `recording_controller` | сохранение данных, очистка и удаление, запись на диск |
| `preprocess_controller` | наборы, окно предобработки, съём, нейтральные наборы, временный референс |
| `object_calibration_controller`, `object_calibration_capture` | калибровка по объекту |
| `web_controller` | веб-сервер, очередь действий, снимки состояния и картинки |
| `control_button_controller`, `log_controller` | GPIO-кнопка; журнал и окна ошибок |
Поля настроек описаны один раз — метаданными dataclass-моделей в
`python_app/models/gui_profile_schema.py` (их разворачивает `setting_fields.py`).
Из этого описания берутся ключ профиля, виджет, ключ live-файла, поле веб-формы и
проверка при чтении. Подписи веб-формы берутся из подписей полей окна. Qt в моделях и
кодеках нет. Оформление окна — `gui/theme.py`, цвета серий и маркеров —
`gui/controllers/renderers/common.py`, оформление веба — `webui/static/style.css`.
### Правила жизненного цикла
- Приостановка (`gui/runtime/pipeline_pause.py`). Конвейер берёт владение до
остановки и дочитывания колец, потому что дочитывание прокачивает события Qt.
Асинхронный съём держит приостановку до конца. Автоперезапуск при удержанной
приостановке запрещён; продолжается только то, что шло до неё.
- **Stop** и закрытие окна. Пользовательский **Stop** сразу увеличивает счётчик остановок
и отменяет ожидающие продолжения; техническая остановка приостановку не отменяет.
Закрытие сразу запрещает новые запуски; если идёт дочитывание, его владелец
заканчивает и вызывает отложенное закрытие.
- **Start** проверяет всё до того, как что-то остановить. Отказ не трогает работающий
процессор, локатор и историю.
- Ошибка отрисовки не останавливает сбор: она пишется один раз и видна в строке
состояния. Сбор останавливают только ошибки чтения колец.
- Показывается новейшая коллекция: опрос рисует последний результат истории, даже
если последним пришёл пересчёт более старой.
- Графики меняются только через `PlotController`. Он считает изменения картинки, и
веб снимает кадр по этому счётчику. Сигнал `QGraphicsScene.changed` не подключать:
подписка переводит Qt в старый режим обновления, при котором на экране остаются
следы прежних кривых (это проверяет тест
`test_redrawn_views_leave_no_stale_pixels_on_screen`).
- Без экрана и при нажатии GPIO-кнопки окна ошибок не показываются; приостановка
освобождается до показа ошибки.
- Одно действие — один код для кнопки окна, веба и GPIO, с общей проверкой
занятости. Веб-поток трогает Qt только через сигналы с очередью и снимки состояния.
- Синхронный съём (калибровка, референс, нейтральные наборы) идёт в потоке окна
при приостановленном сборе; лишняя развёртка внутри такого нажатия — это зависшее
окно. Съём для калибровки по объекту асинхронный, через обычный конвейер, а расчёт
— в рабочем потоке без Qt.
## Состав кода
| Каталог | Что |
| --- | --- |
| `python_app/gui/` | окно, диалоги, контроллеры, отрисовка |
| `python_app/hardware_full/` | сервисы приборов: `librevna_service.py`, `multi_device_service.py`, `matrix_radar_service.py`, `switched_matrix_radar_service.py`, `point_target_mock_service.py` (симулятор), `switch_service.py` |
| `python_app/hardware_full/librevna_driver/` | USB-драйвер одиночного LibreVNA, протокол версии 14 |
| `python_app/hardware_full/librevna_multi_device_driver/` | синхронная многоприборная развёртка (`cycle_collection.py` — один цикл) |
| `python_app/hardware_full/rph_radar/`, `laser_control/` | радиофотонный радар: коллектор и плата RFG, плата лазеров |
| `python_app/hardware_full/switch_drivers/` | ВЧ-ключи H7992 через GPIO (`gpio_uapi.py` — ioctl ядра без внешних пакетов) |
| `python_app/models/` | профиль: схема, чтение и запись, проверки; модель данных (`dataset_model.py`) |
| `python_app/orchestration/` | запуск процессов (`process_supervisor.py`, `restart_policy.py`), рабочие файлы, кольца (`shm/`) |
| `python_app/storage/npz/` | наборы и наборы данных (`store.py`, `serialize.py`, `snapshot_numpy.py`, `vna_history_json.py`) |
| `python_app/webui/` | веб-сервер FastAPI в процессе окна, страница `static/` |
| `python_app/workflows/` | сценарии съёма и калибровки по объекту |
| `python_app/scripts/` | процессы сбора и утилиты |
| `python_app/tests/` | тесты Python |
| `data_acq_and_processing/common_cpp/` | общий C++: конфигурация (`config/`), кадры и кольца (`ipc/`), драйвер H7992 (`hardware/`) |
| `data_acq_and_processing/preprocessing/` | препроцессор и загрузка наборов |
| `data_acq_and_processing/processing/` | процессор, режимы (`processors/src/`), локатор (`locator/`) |
| `data_acq_and_processing/rph_radar_collector/` | коллектор L-Card E-502 |
| `data_acq_and_processing/tests/` | тесты C++ и эталоны 3D SAR и GPR |
| `data_acq_and_processing/third_party/` | nlohmann/json и Eigen |
| `deploy/` | служба и обновление с флешки |
| `device_firmware/` | прошивки и описания протоколов |
## Где что править
| Задача | Куда смотреть |
| --- | --- |
| ключи профиля и их проверка | `python_app/models/run_config_schema.py`, `run_config_codec.py`, `run_config_validation.py`; то же на стороне C++ — `data_acq_and_processing/common_cpp/config/` |
| поля окна, live-файла и веб-формы | `python_app/models/gui_profile_schema.py`, секции `gui/controllers/sections/`, C++ — `processing/data_processor/src/processing_live_config.cpp` |
| формат кадров | синхронно: C++ `common_cpp/ipc/` (`shared_types.hpp/.cpp`), Python-писатель `storage/npz/serialize.py`, Python-читатель `orchestration/shm/decoder.py`, описание — §6 `librevna_protocol.md` |
| модель данных | `python_app/models/dataset_model.py` |
| драйверы приборов | `python_app/hardware_full/` |
| запуск процессов | `orchestration/process_supervisor.py`, `restart_policy.py`; из окна — `pipeline_controller.py` |
| хранение данных | `python_app/storage/npz/` |
| режимы обработки | `data_acq_and_processing/processing/processors/src/`: `bscan`, `gpr`, `passthrough`, `sar3d`; общий код GPR и 3D SAR — `backprojection.hpp`; отрисовка — `gui/controllers/renderers/` |
| 3D SAR | `sar3d_processor.cpp` (алгоритм), `sar3d_clean.{hpp,cpp}` (фон кадра, CLEAN, рамки), `sar3d_gpu.{hpp,cpp}` (CUDA), `renderers/sar3d.py` |
| сценарии съёма | `gui/controllers/preprocess_controller.py`, `python_app/workflows/` |
| веб-интерфейс | `python_app/webui/`, мост к окну — `gui/controllers/web_controller.py` |
Изменив поведение, поправьте главу документации, где оно описано: ключи профиля —
[configuration.md](configuration.md), поля окна — [desktop.md](desktop.md) и
[processing.md](processing.md), форматы данных — [data.md](data.md), кадры колец и
USB-протокол — `librevna_protocol.md`, переключение стимула —
`per_point_switching.md`.
## 3D SAR и эталон
Алгоритм 3D SAR — порт исследовательского ноутбука `Multi_depthnorm_clean.ipynb`
(ячейки 1 и 5; хранится вне репозитория). Комментарии в `sar3d_processor.cpp` и
`sar3d_clean.cpp` называют функции и константы ноутбука, поля окна — ровно его имена.
Величины ноутбука, не вынесенные в окно, — константы в начале этих файлов.
Решения CLEAN и рамок зависят от точных равенств (углы сетки лежат на границе окна
дальности, полуразмеры окон кратны шагам), поэтому граница на видеокарте считается в
double без FMA (`__dmul_rn`, `__dadd_rn`), а хост-код 3D SAR собирается с
`-ffp-contract=off` (`SAR3D_HOST_FLAGS` в `Makefile`). Иначе на машинах с FMA (aarch64,
x86 с `-mfma`) результат расходился бы с ноутбуком.
Сверка с ноутбуком автоматизирована (глава [Разработка](development.md)): генератор
выполняет неизменённые функции ноутбука и пишет эталон `sar3d_reference.npz`, тест
гоняет настоящий CUDA-бэкенд и сравнивает с ним. Без видеокарты проверяются только
предупреждение и правила рамок, поэтому после правки 3D SAR сверку нужно прогнать на
настоящей карте. GPR сверяется с `gpr_reference.npz` на CPU, без видеокарты.
+428
View File
@@ -0,0 +1,428 @@
# Файл конфигурации
Все настройки программы хранятся в профиле — файле JSON. В профиле две части:
конфигурация сбора и обработки (разделы `radar`, `switches`, `control_button`,
`logging`, `run`, `preprocess`, `gpr`, `rings`) и состояние окна (раздел `gui`).
Незнакомые термины (полоса ПЧ, дБм, пара, коллекция) — в [словаре](glossary.md).
| Где | Что |
| --- | --- |
| `run_config.json` в корне | профиль для разработки: симулятор многоприборной схемы, `driver_mode: "mock"`, уровень журнала `debug` |
| `run_config_examples/` | готовые профили под каждое железо (таблица ниже) |
| `run_configs/` | профили, которые можно загрузить из веб-интерфейса |
| `python_app/runtime/run_config.json` | рабочая копия, которую программа пишет при каждом **Start**; по ней работают процессы конвейера |
> **Внимание.** Корневой `run_config.json` — профиль симулятора: с ним программа не
> обращается к железу. На машине с радаром работайте с профилем из
> `run_config_examples/`, скопированным и настроенным под установку.
## Как профиль загружается и сохраняется
При запуске программа берёт профиль по порядку:
1. путь из переменной окружения `RADAR_SYSTEM_PROFILE` (её задаёт
`./start.sh --profile ПУТЬ`);
2. последний использованный профиль из `python_app/runtime/gui_session_state.json`;
3. корневой `run_config.json`.
Относительный путь отсчитывается от каталога проекта. Если выбранный профиль не
читается, программа пишет
`Failed to load the last selected config profile; falling back to root run_config.json.`
и загружает корневой; если не читается файл сеанса —
`Failed to read GUI session-state; using root run_config.json.` Если не читается и
корневой профиль, программа не запускается. Профиль без раздела `gui` загружается со
значениями окна по умолчанию; при запуске (кроме корневого профиля) об этом пишется
`Loaded legacy run config without GUI defaults: <путь>`, при **Load Config…** —
`kind=legacy run config`
в строке `Config profile loaded: …`.
**Load Config…** загружает другой профиль; это возможно только при остановленном
сборе (`Stop all pipeline processes before loading a config profile`) и не во время
съёма наборов.
**Save Config…** записывает текущие значения всех полей окна, выбранные наборы и
геометрию антенн. Загруженный или сохранённый профиль становится стартовым для
следующего запуска.
> **Внимание.** Изменения в окне и в веб-интерфейсе, в том числе выбор наборов и
> временный референс, в профиль сами не записываются. После перезапуска программы они
> пропадут, если не нажать **Save Config…**.
Правила чтения:
- нет раздела или ключа — берётся значение по умолчанию. Умолчания схемы — минимальные
заглушки (`points: 1`, `if_bandwidth_hz: 1.0`, пустые имена колец), а не рабочие
настройки: начинайте с профиля-примера;
- типы строгие: целое число нельзя записать как `5.0`, логическое — как `1`; ошибка
называет ключ (`points must be a JSON integer`);
- неизвестные ключи пропускаются и при сохранении исчезают;
- раздел `gui` с неизвестным номером версии отвергается (`Unsupported gui.version: N`).
## Когда изменение вступает в силу
| Обозначение | Когда |
| --- | --- |
| сразу | сразу, в том числе во время сбора |
| Apply Radar | при нажатии **Apply Radar** (без экрана — автоматически при запуске) |
| Start | при следующем **Start** или **Single Capture**: программа заново пишет рабочую копию профиля и перезапускает процессы сбора и предобработки |
| Start, процессор | как Start, но перезапускается и процессор, а с ним TCP-локатор: его клиенты отключаются, история обработки очищается |
| профиль | ключа нет в окне: правьте файл и загрузите его через **Load Config…** при остановленном сборе |
Процессор перезапускается, если изменились: ключ радара (`radar_key`), опорная частота
многоприборной схемы, `driver_mode` радара и ключей, число положений ключей, список
пар, любой ключ раздела `rings`, любой ключ `run.locator_server`, путь к live-файлу
или `run.idle_sleep_ms`. Смена наборов предобработки процессор не перезапускает
(исключение — нейтральные наборы).
## Ключ радара
`radar_key` — строка, по которой программа находит наборы калибровки и референса:
они лежат в каталогах `python_app/data/<вид набора>/<radar_key>/`. Смена любой
величины, из которой строится ключ, скрывает наборы, снятые при старых значениях.
| Радар | Из чего строится | Пример (из профилей-примеров) |
| --- | --- | --- |
| `librevna` | модель, серийный номер (`no_serial`, если пуст), начало и конец развёртки, число точек, полоса ПЧ, мощность | `librevna_no_serial_st1000000_sp6000000000_p201_if50000_pw-10` |
| `librevna_multi` | то же для мастера и обоих ведомых, плюс положения ключей `_out`, `_in`, `_stim` | `librevna_multi_209E307A5532_20A1307D5532_2072306C5532_st500000000_sp6000000000_p1031_if50000_pw-3_out1_in1_stim2` |
| `rph_radar` | первые 16 знаков хеша SHA-256 от выходной полосы, фазовой калибровки, параметров коллектора (`args`) или источника `rfg_udp`, числа положений ключей и настроек лазера (если плата лазеров включена или источник — RFG) | `rph_radar_2afdaa32e154b122` |
`dwell_us`, путь к программам и тайм-ауты в ключ не входят.
## Профили-примеры
| Файл | Радар | Что в нём |
| --- | --- | --- |
| `run_config.json` (корневой) | `librevna_multi`, `mock` | симулятор схемы с переключением стимула: 16 пар, 1 МГц–6 ГГц, 201 точка, вертикальная база антенн 0,445 м, журнал `debug` |
| `run_config_librevna.example.json` | `librevna`, `native` | одиночный LibreVNA, выходной ключ на 2 положения (линии 17, 27), входной на 4 (линии 22, 23), 8 пар, 1 МГц–6 ГГц, 201 точка |
| `run_config_librevna_multi.example.json` | `librevna_multi`, `native` | приборы `209E307A5532` (мастер), `20A1307D5532`, `2072306C5532`; 8 пар, 1 МГц–6 ГГц, 201 точка |
| `run_config_librevna_multi.example2.json` | `librevna_multi`, `native` | то же для второго комплекта: `206830985532`, `206930965532`, `206930A15532` |
| `run_config_librevna_multi_per_point.example.json` | `librevna_multi`, `native` | переключение стимула, комплект `209E307A5532`; 16 пар, 0,5–6 ГГц, 1031 точка, −3 дБм, `dwell_us: 2`, измеренная геометрия антенн; режим `sar3d` |
| `run_config_librevna_multi_per_point_lowband.example.json` | `librevna_multi`, `native` | переключение стимула, комплект `206830985532`; 30–200 МГц, 201 точка — проверка ключей на низких частотах; у антенн y = z = 0, Tx2 и Tx3 совпадают с Tx0 и Tx1 |
| `run_config_rph_radar.example.json` | `rph_radar`, `e502` | АЦП E-502, плата лазеров на `/dev/ttyUSB0` в режиме `variation`, ключи на 2 и 4 положения, 8 пар |
| `run_config_rph_radar_rfg.example.json` | `rph_radar`, `rfg_udp` | плата RFG на `192.168.2.1:5000`, плата лазеров на `/dev/serial/by-id/usb-Silicon_Labs_CP2102_…`, одна пара, измеренные фазовые якоря |
| `run_config_simulator.example.json` | `librevna`, `mock` | симулятор одиночного LibreVNA: 8 пар, 1 МГц–6 ГГц, 201 точка; режим `gpr` |
`run_configs/run_config.json` — профиль симулятора одиночного LibreVNA для
веб-интерфейса, близкий к `run_config_simulator.example.json`.
## radar
| Ключ | Тип, по умолчанию | Что делает | Когда |
| --- | --- | --- | --- |
| `radar.model` | строка, `"librevna"` | тип радара: `"librevna"`, `"librevna_multi"` или `"rph_radar"`. Старое имя `"kamil_adc"` отвергается с подсказкой перейти на `rph_radar` | профиль; Start, процессор |
| `radar.serial` | строка, `""` | серийный номер прибора; у многоприборной схемы — мастера (обязателен). Пустой у одиночного — первый найденный прибор | профиль; Start, процессор |
| `radar.driver_mode` | строка, `"mock"` | `"native"` — настоящее железо, `"mock"` — симулятор. Радиофотонному радару нужен `"native"` | профиль; Start, процессор |
### radar.sweep
Развёртка LibreVNA. Радиофотонный радар эти ключи не использует (у него
`radar.rph_radar.band`), а `points` у него в профиль не записывается. Поля редактируются в секции
**Radar**; с подключённым прибором они подрезаются под его пределы.
| Ключ | Тип, по умолчанию | Поле | Что делает | Ключ радара |
| --- | --- | --- | --- | --- |
| `start_hz` | число, `1000000.0` | **Start (Hz)** | начало развёртки, Гц, не меньше 0 | да |
| `stop_hz` | число, `6000000000.0` | **Stop (Hz)** | конец развёртки, Гц, больше начала | да |
| `points` | целое, `1` | **Points** | число частотных точек | да |
| `if_bandwidth_hz` | число, `1.0` | **IF bandwidth (Hz)** | полоса ПЧ, Гц, больше 0 | да |
| `stimulus_power_dbm` | число, `-30.0` | **Stimulus power (dBm)** | мощность излучения, дБм | да |
| `dwell_us` | целое, `0` | — | дополнительная задержка на каждой стадии прибора, мкс; при отправке значение молча ограничивается диапазоном 0–65535, а прибор — 10 239 мкс. В режиме переключения стимула в это время контроллер переключает ключ; в примерах `2` | нет |
Изменения применяются при **Start**; у одиночного LibreVNA — ещё и при
**Apply Radar**.
### radar.multi_device
Только для `librevna_multi`; другие радары эти ключи игнорируют. В окне не
редактируются.
| Ключ | Тип, по умолчанию | Что делает |
| --- | --- | --- |
| `slave_serials` | список строк, `[]` | серийные номера двух ведомых, ровно два. Первый даёт входы 0 и 1, второй — 2 и 3; порядок важен. Можно записать строкой через запятую; старое имя `slave_serial_numbers` тоже читается |
| `force_external_reference` | логическое, `false` | внешняя опорная частота 10 МГц на всех трёх приборах. Включайте, только если опора подана на все приборы (глава [Оборудование](hardware.md)) |
| `recovery_attempts` | целое, `3` | сколько раз переподключать приборы после сбоя, прежде чем передать ошибку процессу сбора |
| `output_switch_positions` | целое, `1` | положения настоящего ВЧ-ключа на выходах мастера, которым управляет компьютер (линии из `switches.port1`). 1 — ключа нет. Ни один пример его не использует |
| `input_switch_positions` | целое, `1` | то же для входов ведомых (линии из `switches.port2`) |
| `stimulus_switch_positions` | целое, `1` | положения ключа переключения стимула внутри частотной точки; `2` — режим переключения стимула. Прошивка мастера поддерживает 1, 2 и 4, контроллер STM32 — 2 |
Все ключи, кроме `recovery_attempts`, входят в ключ радара или в условие перезапуска
процессора.
### radar.rph_radar
Только для `rph_radar`. Поля без пометки «—» редактируются в секции **Radar**.
| Ключ | Тип, по умолчанию | Поле | Что делает |
| --- | --- | --- | --- |
| `source` | строка, `"e502"` | — | источник: `"e502"` — АЦП L-Card через `rph_radar_collector`, `"rfg_udp"` — плата RFG по UDP |
| `udp_bind` | строка, `"0.0.0.0:5000"` | — | только RFG: адрес `IPv4:порт`, на котором принимаются пакеты |
| `project_dir` | строка, `""` | **Project dir** | рабочий каталог коллектора; пусто — каталог проекта |
| `executable_path` | строка, `""` | **Executable path** | путь к коллектору, обычно `build/bin/rph_radar_collector` |
| `tty_path` | строка, `""` | **TTY path** | путь псевдотерминала, через который коллектор передаёт развёртки; старый файл по этому пути удаляется перед запуском |
| `args` | список строк, `[]` | **Arguments** (по строке на параметр) | параметры коллектора (глава [Оборудование](hardware.md)); `tty:` здесь задавать нельзя |
| `env` | объект строк, `{}` | **Environment JSON** | переменные окружения коллектора; `~` и `$ПЕРЕМЕННАЯ` раскрываются. Если `env` не задаёт `LD_LIBRARY_PATH`, в начало унаследованного `LD_LIBRARY_PATH` добавляется `~/.local/lib` |
| `startup_timeout_s` | число, `5.0` | **Startup timeout (s)** | сколько ждать появления псевдотерминала после запуска коллектора |
| `sweep_timeout_s` | число, `5.0` | **Read timeout (s)** | сколько ждать пригодной развёртки, покрывающей полосу, прежде чем переподключиться |
| `stop_timeout_s` | число, `2.0` | **Stop timeout (s)** | ожидание остановки коллектора; на деле не больше 0,5 с, затем он завершается принудительно |
| `phase_calibration.phase0_rad`, `freq0_hz` | `0.0`, `2046000000.0` | — | первая опорная точка «фаза опорного сигнала — частота» |
| `phase_calibration.phase1_rad`, `freq1_hz` | `300.0`, `5612000000.0` | — | вторая опорная точка; фазы и частоты точек должны различаться |
| `band.start_hz`, `band.stop_hz` | `2100000000.0`, `5500000000.0` | — | выходная полоса: развёртка пересчитывается на её сетку; развёртка, не покрывающая полосу, отбрасывается |
| `band.points` | `2048` | — | число точек выходной сетки, не меньше 2 |
Для `source: "e502"` поля `executable_path` и `tty_path` обязательны, а `project_dir`, если
задан, должен быть каталогом. Все три тайм-аута должны быть конечными положительными
числами (`radar.rph_radar.<ключ> must be finite and positive`).
Фазы `phase0_rad` и `phase1_rad` записывает программа фазовой калибровки
(`rph_radar_calibrate --apply`). Полоса и якоря входят в ключ радара, поэтому после
новой фазовой калибровки старые наборы не видны.
### radar.laser_control
Плата лазеров радиофотонного радара. Поля — в секции **Radar**, применяются кнопкой
**Apply Radar**.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `enabled` | `false` | программировать плату при **Apply Radar** |
| `port` | `""` | последовательный порт платы, 115 200 бод |
| `mode` | `"manual"` | `"manual"` — постоянные токи и температуры, `"variation"` — развёртка тока LD1 (плате RFG нужен этот режим) |
| `pi_coeff1_p`, `pi_coeff1_i` | `1280`, `25` | коэффициенты ПИ-регулятора первого канала |
| `pi_coeff2_p`, `pi_coeff2_i` | `1280`, `179` | коэффициенты ПИ-регулятора второго канала |
| `manual.temp1`, `manual.temp2` | `24.0`, `24.9` | температуры лазеров, °C, 15–40 |
| `manual.current1`, `manual.current2` | `97.3`, `102.0` | токи лазеров, мА, 15–110 |
| `variation.variation_type` | `"CHANGE_CURRENT_LD1"` | что меняется; другого варианта нет |
| `variation.static_temp1`, `static_temp2` | `24.0`, `24.9` | температуры во время развёртки |
| `variation.static_current1`, `static_current2` | `97.3`, `102.0` | постоянные токи во время развёртки |
| `variation.min_value`, `max_value` | `97.3`, `107.0` | пределы развёртки тока LD1, мА, начало меньше конца |
| `variation.step` | `0.05` | шаг тока, мА, 0,002–0,5 |
| `variation.time_step` | `50` | длительность ступени, мкс, 20–100 |
| `variation.delay_time` | `10` | пауза между развёртками, мс, 3–10 |
## switches
Два ВЧ-ключа: `port1` — выходной (передающие антенны), `port2` — входной (приёмные).
Таблица положений — в главе [Оборудование](hardware.md).
| Ключ | Тип, по умолчанию | Что делает |
| --- | --- | --- |
| `name` | строка, `""` | имя ключа в сообщениях и в GPIO (`radar_<имя>`) |
| `driver_mode` | `"mock"` | `"native"` — управлять линиями GPIO, `"mock"` — только помнить положение |
| `radar_port` | целое, `0` | порт прибора, к которому подключён ключ: 1 или 2. Задайте его явно: `0` не проходит проверку при **Start** (`switches.<имя>.radar_port must be 1 or 2`) |
| `positions` | `1` | число положений, у настоящего ключа 1–4. Задаёт число выходов или входов матрицы |
| `default_position` | `0` | положение после открытия ключа, меньше `positions` |
| `gpio_chip` | `""` | GPIO-чип, например `/dev/gpiochip0`; для `native` обязателен |
| `pin_a`, `pin_b` | `-1` | номера линий A и B на чипе; для `native` — разные и неотрицательные |
Поле `driver` допускается только со значением `"h7992"`, `invert_logic` — только
`false`; оба при сохранении убираются. Другие значения отвергаются ещё до обращения к
GPIO. У многоприборной схемы программа сама переписывает в этих разделах `driver_mode`
(`"mock"`), `radar_port`, `positions` (положения настоящего ключа × выходы или входы
приборов) и `default_position` (`0`): значения из файла не используются. Читаются
только `name`, `gpio_chip`, `pin_a` и `pin_b` — для настоящих ключей на выходах мастера
или входах ведомых, если `multi_device.output_switch_positions` или
`input_switch_positions` больше 1; их режим берётся из `radar.driver_mode`. Изменения
применяются при **Start**;
число положений и режим перезапускают процессор.
## control_button
GPIO-кнопка, которая снимает временный референс. Настройки читаются один раз при
запуске программы.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `enabled` | `false` | включить кнопку |
| `gpio_chip` | `"/dev/gpiochip0"` | GPIO-чип |
| `pin` | `-1` | номер линии на чипе (не номер контакта разъёма); отрицательный — кнопка не включается |
| `active_low` | `true` | `true` — нажатие опускает линию в 0 (кнопка на землю), `false` — поднимает в 1 |
| `bias` | `""` | подтяжка: `"pull_up"`, `"pull_down"`, `"disabled"`; пусто — к питанию при `active_low`, к земле иначе |
| `debounce_ms` | `50` | подавление дребезга средствами ядра, мс; 0 — без подавления |
| `action` | `"capture_tmp_reference"` | действие по нажатию; другого нет |
## logging
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `level` | `"info"` | наименьший уровень записей журнала: `debug`, `info`, `warning` или `error`. Меняется списком **Level** над журналом окна; записывается при **Save Config…** |
На уровне `debug` в журнал попадают трассы тайминга переключений; корневой профиль
работает с ним.
## run
| Ключ | Тип, по умолчанию | Что делает | Когда |
| --- | --- | --- | --- |
| `settling_ms` | целое, `0` | пауза после переключения ВЧ-ключей перед развёрткой, мс. Поле **Settling (ms)** | Start |
| `idle_sleep_ms` | целое, `2` | сколько процессы предобработки и обработки спят, когда нет входных данных, мс | Start, процессор |
| `continuous` | логическое, `false` | непрерывный сбор или одна коллекция. Программа сама ставит `true` при **Start** и `false` при **Single Capture**; значение из профиля важно только для `./start.sh --producer-only` | — |
| `processing_live_config_path` | строка, `""` | путь к live-файлу. Программа всегда подставляет `python_app/runtime/processing_live.json` своего каталога | — |
| `combos` | список `{"input", "output"}`, `[]` | пары сбора в порядке обхода (ниже) | Start, процессор |
| `locator_server` | объект | TCP-локатор (ниже) | Start, процессор |
### run.combos
Список пар `{"input": вход, "output": выход}`; пустой список — полная матрица:
выходы по порядку, внутри каждого — все входы. Пар не больше 4096, повторы и номера
вне матрицы отвергаются.
- Одиночный LibreVNA и радиофотонный радар: при **Start** список берётся из окна —
**Run combos** (`вход:выход` через запятую, пусто — полная матрица) или
**Single combo**. В режимах pass-through и B-scan, если оба ключа `mock` или у обоих
не больше одного положения, программа снимает только пару `0:0`.
- Многоприборная схема: список всегда равен полной матрице; в окне он показан, но не
редактируется.
### run.locator_server
TCP-сервер, который отдаёт объекты GPR внешним программам и принимает от них
скорость (протокол — в главе [Веб-интерфейс, кнопка и локатор](web.md)). В окне не
редактируется. Старое имя раздела `run.locator` тоже читается.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `host` | `"0.0.0.0"` | адрес, на котором сервер ждёт клиентов; `0.0.0.0` — все интерфейсы |
| `port` | `8888` | TCP-порт, 1–65535 |
| `device_id` | `3` | номер устройства в заголовке каждого пакета |
| `protocol_version` | `1` | поле `ver` в пакетах |
| `max_payload_bytes` | `65536` | наибольший размер входящего пакета; клиент с большим пакетом отключается |
| `client_queue_size` | `32` | очередь пакетов на клиента; при переполнении выбрасываются старые |
| `logger_name` | `"locator_runtime"` | не используется |
Ключ `enabled` в этом разделе программа не сохраняет: локатор включён всегда.
## preprocess
| Ключ | Что делает |
| --- | --- |
| `s21.calibration.set_name` | имя выбранного набора калибровки (through) |
| `s21.reference.set_name` | имя выбранного набора референса |
| `s21.object_calibration.set_name` | имя выбранной калибровки по объекту; пусто — не применять |
| `s21.*.bundle_path` | путь к файлу набора для препроцессора; программа заполняет его сама при **Start**, в профиле — пусто |
| `notch.enabled` | включить режекторный фильтр, по умолчанию `false` |
| `notch.bands_hz` | полосы подавления `[нижняя, верхняя]`, Гц, по умолчанию `[]` |
| `notch.taper_width_hz` | ширина плавного края, Гц, не меньше 0, по умолчанию `40000000.0` |
| `notch.taper_type` | форма края: `"cosine"` (по умолчанию) или `"hard"`; как работает фильтр — в главе [Калибровка и предобработка](preprocessing.md) |
Наборы выбираются в окне **Preprocessing Setup** и записываются в профиль при
**Save Config…**. Изменения применяются при **Start**.
## gpr
Геометрия антенн для GPR и 3D SAR и для симулятора. Редактируется таблицами
**Tx geometry** и **Rx geometry** на страницах GPR и 3D SAR; правка таблицы сразу
уходит процессору.
| Ключ | Запись | Что делает |
| --- | --- | --- |
| `tx_geometry` | `{"output_pos", "x_m", "y_m", "z_m"}` | положения передающих антенн: номер выхода и координаты в осях радара, м |
| `rx_geometry` | `{"input_pos", "x_m", "y_m", "z_m"}` | положения приёмных антенн: номер входа и координаты, м |
Записей может быть меньше, чем выходов или входов: антенны без записи в GPR и 3D SAR
не участвуют. Номер должен быть меньше числа выходов (входов), без повторов, иначе
`gpr.tx_geometry output_pos is out of range [0, N)`. При сужении матрицы лишние
записи нужно удалить. Отсутствующие `y_m` и `z_m` считаются нулями. Оси — в главе
[Как работает радар](principles.md).
## rings
Пять колец разделяемой памяти, через которые процессы передают данные
(глава [Устройство программы](architecture.md)):
| Кольцо | Кто пишет | Кто читает |
| --- | --- | --- |
| `raw` | процесс сбора | препроцессор |
| `raw_tap` | процесс сбора, копия | окно программы |
| `preprocessed` | препроцессор | процессор |
| `preprocessed_tap` | препроцессор, копия | окно программы |
| `results` | процессор | окно программы |
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `name` | `""` | имя сегмента разделяемой памяти, например `/radar_raw`. Пустое имя у `raw_tap` или `preprocessed_tap` отключает копию и соответствующую историю окна |
| `capacity` | `1` | сколько кадров помещается в кольцо; при переполнении затирается самый старый |
| `slot_size_bytes` | `4096` | наибольший размер кадра, байт, кратный 8 |
Слоты `raw`, `preprocessed` и включённых копий должны вмещать коллекцию:
44 + пары × (28 + 12 × точки) байт. Например, 16 пар по 1031 точке требуют 198 444
байт. Программа проверяет это до запуска
(`rings.raw.slot_size_bytes must be at least N for this sweep`). Слот `results` должен
вмещать результат; изображения 3D SAR при настройках по умолчанию занимают около
1 МБ. Во всех примерах слоты по 2 МиБ (`2097152`) и ёмкость 50.
Ёмкость влияет и на историю: процессор хранит до 50 коллекций, но не больше
ёмкости `preprocessed` и `results`, а B-scan во время сбора показывает столько же. В
GPR и 3D SAR процессор при этом хранит не меньше, чем нужно обработке: 1 +
**Mean count** (если включено вычитание среднего) + **SAR_FRAME_RADIUS** (в 3D SAR),
независимо от ёмкости колец.
Калибровке по объекту нужно кольцо `preprocessed_tap`.
## gui
Состояние окна: значения всех полей, которые программа запоминает. Раздел
записывается целиком при **Save Config…**. Если его нет, программа берёт значения по
умолчанию, а часть вычисляет из профиля: номера антенн — из геометрии и списка пар,
окно видимости GPR — из положения антенн, каталог сохранения —
`python_app/data/snapshots`.
| Ключ | Поле в окне | Смысл |
| --- | --- | --- |
| `version` | — | версия раздела, всегда `1` |
| `switches.combo_mode` | кнопки **Select** у **Run combos** и **Single combo** | откуда брать пары сбора: `"text"` или `"single"` |
| `switches.combos_text` | **Run combos** | список пар `вход:выход` |
| `switches.single_input`, `single_output` | **Input**, **Output** в строке **Single combo** | одна пара |
| `processing.selected_mode` | **Mode** | режим обработки: `pass_through`, `bscan`, `gpr`, `sar3d` |
| `processing.pass_through.*` | страница pass-through | `show_magnitude`, `show_phase`, `unwrap_phase`, `combo_filter`, `fixed_y_enabled`, `y_min_db`, `y_max_db` |
| `processing.bscan.*` | страница B-scan | `axis`, `cut_m`, `max_depth_m`, `gain`, `start_freq_mhz`, `stop_freq_mhz`, `subtract_mean_ascan`, `history_window_scans` |
| `processing.gpr.*` | страница GPR | `input_positions`, `output_positions`, `min_depth_m`, `max_depth_m`, `range_comp_power`, `object_min_frac`, `score_mode`, `motion_mode`, `look_angle_deg`, `direction_sign`, `speed_m_s`, `ignore_socket_speed_enabled`, `max_detected_objects_to_draw`, `draw_top_m_objects`, `object_approach_min_frames`, `start_freq_mhz`, `stop_freq_mhz`, `background_subtract_enabled`, `background_mean_count`, `remove_sidelobe_objects_enabled`, `bp_ground_depth_m`, `pair_depth_equalize`, `render_mode`, `visible_x_min_m`, `visible_x_max_m`, `visible_z_min_m`, `visible_z_max_m` |
| `processing.sar3d.*` | страница 3D SAR | `input_positions`, `output_positions`, `min_depth_m`, `max_depth_m`, `start_freq_mhz`, `stop_freq_mhz`, `speed_m_s`, `ignore_socket_speed_enabled`, `look_angle_deg`, `frame_radius`, `voxel_x_m`, `voxel_y_m`, `voxel_z_m`, `bp_ground_depth_m`, `comp_range_power`, `use_compensation`, `normalize_each_frame`, `pair_depth_equalize`, `background_subtract_enabled`, `background_mean_count`, `bg_gate_enabled`, `clean_enabled`, `x_size_m`, `y_min_m`, `y_max_m`, `z_size_m`, `cloud_threshold`, `zoom_percent`, `mark_enabled` |
| `data_actions.save_path` | **Path** | каталог для сохранения данных; относительный путь — от рабочего каталога процесса |
| `data_actions.save_name` | **Name** | имя сохраняемого набора данных, по умолчанию `snapshot_manual` |
| `data_actions.record_count` | **Sweeps to record** | сколько коллекций записать на диск, по умолчанию 100 |
| `preprocess_dialog.set_name` | **Set name** | имя нового набора, по умолчанию `set_001` |
| `preprocess_dialog.radar_config_dir` | **Directory** | каталог вариантов радара |
| `preprocess_dialog.use_all_radar_configs` | **Capture current combo for all found radar configs** | съём по всем вариантам |
| `preprocess_dialog.median_sweep_count` | **Sweeps per combo (median)** | развёрток на пару, по умолчанию 5 |
Ключи страниц обработки названы по полям окна; что делает каждое поле, сколько оно по
умолчанию и в каких пределах — в главе [Режимы обработки](processing.md). Типы,
варианты выбора, объявленные в схеме границы и версия проверяются при чтении, и
нарушение отвергает весь профиль: `gui.processing.gpr.score_mode must be one of: peak, combined`,
`gui.processing.sar3d.zoom_percent must be <= 1000`. Значение в пределах схемы, но вне
пределов поля окна поле молча подрезает к своей границе (например, `frame_radius: 1000`
станет 24), и подрезанное значение запишется при следующем **Save Config…**.
`preprocess_dialog.median_sweep_count` меньше 1 поднимается до 1.
## processing_live.json
Живые настройки процессора. Файл `python_app/runtime/processing_live.json` пишет
программа при каждом изменении настройки обработки, смене режима, правке геометрии,
очистке истории и удалении последнего измерения; читает его только процессор. Править
его вручную не нужно: программа перезаписывает его целиком.
В файле 53 ключа. Ключи настроек обработки соответствуют полям окна:
| Ключ | Поле |
| --- | --- |
| `processor_mode` | **Mode** |
| `bscan_axis`, `bscan_cut_m`, `bscan_max_depth_m`, `bscan_gain`, `bscan_start_freq_mhz`, `bscan_stop_freq_mhz` | поля B-scan |
| `gpr_input_positions`, `gpr_output_positions`, `gpr_min_depth_m`, `gpr_max_depth_m`, `gpr_start_freq_mhz`, `gpr_stop_freq_mhz`, `gpr_speed_m_s`, `gpr_look_angle_deg`, `ignore_socket_speed`, `gpr_background_subtract_enabled`, `gpr_background_mean_count`, `pair_depth_equalize` | общие поля GPR и 3D SAR — со страницы выбранного режима |
| `gpr_range_comp_power`, `gpr_object_min_frac`, `gpr_score_mode`, `gpr_motion_mode`, `gpr_direction_sign`, `gpr_max_detected_objects_to_draw`, `gpr_draw_top_m_objects`, `gpr_object_approach_min_frames`, `gpr_remove_sidelobe_objects_enabled`, `gpr_bp_ground_depth_m`, `gpr_visible_x_min_m`, `gpr_visible_x_max_m`, `gpr_visible_z_min_m`, `gpr_visible_z_max_m` | поля GPR |
| `sar3d_frame_radius`, `sar3d_voxel_x_m`, `sar3d_voxel_y_m`, `sar3d_voxel_z_m`, `sar3d_bp_ground_depth_m`, `sar3d_comp_range_power`, `sar3d_use_compensation`, `sar3d_normalize_each_frame`, `sar3d_x_size_m`, `sar3d_y_min_m`, `sar3d_y_max_m`, `sar3d_z_size_m`, `sar3d_bg_gate_enabled`, `sar3d_clean_enabled` | поля 3D SAR |
| `tx_geometry`, `rx_geometry` | таблицы геометрии; заменяют `gpr` профиля в процессоре |
Служебные ключи:
| Ключ | Что делает |
| --- | --- |
| `settings_revision` | номер ревизии настроек (время записи по монотонным часам, нс). Процессор возвращает его в каждом результате, и окно рисует GPR и 3D SAR только текущей ревизии |
| `reprocess_current_result` | пересчитывать ли сохранённые коллекции при новой ревизии; `false` — пока окно само пересчитывает остановленный B-scan |
| `history_clear_before_ns` | граница **Clear Runtime History**: коллекции, начатые не позже этого момента, удаляются из истории процессора |
| `history_removed` | пары `{"collection_id", "capture_start_ns"}`, удалённые **Remove Last Measurement**; список очищается при **Clear Runtime History** |
Процессор отвергает файл целиком, если в нём ошибка JSON, число не того типа или вне
float32, неизвестное значение `processor_mode`, `gpr_score_mode` или
`gpr_motion_mode`, геометрия с номером вне матрицы или с повтором, отрицательные
`sar3d_x_size_m` или `sar3d_z_size_m`, `sar3d_y_max_m` не больше `sar3d_y_min_m`.
Тогда он работает с последними принятыми настройками и пишет
`data_processor warning: failed to refresh live processing config: …` в
`python_app/runtime/logs/data_processor.err.log`. Неизвестные ключи пропускаются.
+225
View File
@@ -0,0 +1,225 @@
# Данные на диске
Программа хранит данные в трёх местах: наборы предобработки и наборы данных — в
`python_app/data/` (или в каталоге, выбранном в **Path**), своё рабочее состояние — в
`python_app/runtime/`, обмен между процессами — в разделяемой памяти `/dev/shm`.
Каталоги `python_app/data*` и `python_app/runtime` в git не попадают.
> **Внимание.** Наборы калибровки и референса в `python_app/data/` — незаменимые
> измерения. При обновлении программы их переносит `update.sh`; при ручном переносе
> копируйте этот каталог целиком.
## Где что лежит
| Путь | Что | Кто пишет |
| --- | --- | --- |
| `python_app/data/s21_calibration/<radar_key>/<имя>.npz`, `.json` | наборы калибровки | окно предобработки |
| `python_app/data/s21_reference/<radar_key>/<имя>.npz`, `.json` | наборы референса, в том числе `tmp_reference` | окно предобработки, временный референс |
| `python_app/data/s21_object_calibration/<radar_key>/<имя>.npz`, `.json` | калибровки по объекту | окно калибровки по объекту |
| `python_app/data/preview_png/<вид>/<имя>/<radar_key>/i<вход>_o<выход>.png` | картинки развёрток при съёме по вариантам радара | окно предобработки |
| `<Path>/<набор данных>/` | снимки и записи на диск; по умолчанию `python_app/data/snapshots` | **Save Last 1000**, **Save S21 JSON**, **Start + Record to Disk** |
| `python_app/runtime/` | рабочее состояние программы (ниже) | программа |
| `/dev/shm/<имя кольца>` | кольца разделяемой памяти | процессы конвейера |
| `/tmp/radar_system.lock` | блокировка единственного экземпляра | `start.sh` |
| `radar.rph_radar.tty_path`, например `/tmp/ttyADC_data` | ссылка на псевдотерминал коллектора E-502 | `rph_radar_collector` |
| `/etc/udev/rules.d/99-radar-librevna.rules` | доступ к LibreVNA по USB | `start.sh` |
| `/etc/systemd/system/radar.service`, `/etc/sudoers.d/radar` | служба и право её перезапускать | `deploy/install-daemon.sh` |
Все файлы JSON и наборы программа записывает через временный файл и замену, а важные
ещё и сбрасывает на диск (fsync). Оборванная запись оставляет прежний файл целым;
от неё может остаться временный файл вида `.<имя>.<32 знака>.tmp`, а от оборванного
сохранения набора данных — каталог `.<имя набора>.<32 знака>`; их можно удалить.
## Наборы предобработки
Набор лежит в каталоге своего вида и ключа радара (глава
[Файл конфигурации](configuration.md)) и состоит из двух файлов:
| Файл | Содержимое |
| --- | --- |
| `<имя>.npz` | архив NumPy без сжатия: для каждой пары `freq_i<вход>_o<выход>` (частоты, float32) и `s21_i<вход>_o<выход>` (S21, complex64), плюс `metadata_json` — описание набора |
| `<имя>.json` | то же описание в читаемом виде |
Описание: `collection_id`, `monotonic_ns`, `capture_start_ns`, `capture_end_ns`,
список `combos` (вход, выход, окно съёма, имена массивов) и `metadata`. У калибровки
по объекту в `metadata` записано, при каких условиях она снята: ключ радара,
геометрия, имя и хеш набора калибровки, настройки расчёта, координаты отражателя,
параметры антенн, диагностика фаз и пары без поправки (`unchanged_combos`).
Старые наборы без `metadata_json` читаются по файлу `.json`; лишние массивы (например,
S11) пропускаются.
Имя набора — любая строка, кроме пустой, `.`, `..` и строк с `/` или `\`. Существующее
имя программа не перезаписывает; исключение — `tmp_reference`, который заменяется при
каждом съёме. Перед записью проверяется, что пары не повторяются, массивы одной длины
и значения конечны.
## Наборы данных
Набор данных — каталог со снимком истории (**Save Last 1000**) или записью на диск
(**Start + Record to Disk**). Имя каталога — `<префикс>_<Name>`, например
`librevna_multi_0.001-6GHz_201pts_-10dBm_run_A`; символы кроме латиницы, цифр, `.`,
`_` и `-` заменяются на `_`. Префикс: модель, полоса в единицах большей границы,
число точек (у радиофотонного радара — `adc`) и мощность. Пустое имя —
`snapshot_<ГГГГММДД>_<ЧЧММСС>` по UTC. Существующий каталог программа не трогает.
```text
<набор данных>/
config_profile.json профиль, с которым сняты данные
processing_settings.json ревизии настроек обработки
raw/ сырые коллекции
0000_id<номер>_ns<время>/
preprocessed/ предобработанные коллекции
results/ результаты обработки
```
Коллекции внутри стадии нумеруются с нуля. Время в имени каталога — метка своей
стадии, поэтому у одной коллекции оно разное в `raw`, `preprocessed` и `results`.
Сопоставляйте стадии по паре `collection_id` и `capture_start_ns` из `meta.json`.
Каталог коллекции в `raw/` и `preprocessed/`:
| Файл | Содержимое |
| --- | --- |
| `i<вход>_o<выход>_freq.npy` | частоты, float32 |
| `i<вход>_o<выход>_s21.npy` | S21, complex64 |
| `meta.json` | `collection_id`, `monotonic_ns`, `capture_start_ns`, `capture_end_ns`, `trace_count`, список трасс с их окнами съёма и именами файлов |
Каталог коллекции в `results/` содержит `meta.json` (номер, время обработки,
`settings_revision`, окно съёма, использованная скорость `speed_m_s`, список
результатов) и массивы результатов. Результаты всей коллекции — файлы
`collection_<NNN>_<имя>_kind<вид>_<поле>.npy`, результаты пары — в подкаталоге
`block_<NNN>_i<вход>_o<выход>/`.
| Вид | Поля |
| --- | --- |
| 1 — трасса | `freq` float32, `trace` complex64 |
| 2 — число | `scalar` float32 |
| 3 — изображение | `x_axis`, `y_axis` float32, `image` float32 размером (строки по y, столбцы по x) |
| 4 — таблица | `table` float32 (строки, столбцы) |
| Режим | Что сохраняется |
| --- | --- |
| pass-through | для каждой пары `pass_through` (вид 1): частота и S21 |
| B-scan | для каждой пары `bscan` (вид 1): в поле `freq` — дальность в метрах, в `trace` — значение (мнимая часть 0) |
| GPR | `gpr_accumulator` (вид 3): карта, оси X и Z; `gpr_points` (вид 4): строки `[x_m, z_m, score]` |
| 3D SAR | `sar3d_front`, `sar3d_side`, `sar3d_top`, `sar3d_oblique` (вид 3); `sar3d_oblique_basis`; `sar3d_peak` `[уровень, X, Y, Z]`; `sar3d_background` `[отношение, 6, 8.5]`; `sar3d_objects` `[X, Y, Z, уровень]` и `sar3d_marks` `[класс, X0, X1, Y0, Y1, Z0, Z1]` или `sar3d_target_width` `[X, Y, Z]`; `sar3d_status` `[код, байты текста UTF-8]` |
Классы рамок 3D SAR: 0 — очень низко, 1 — на земле, 2 — над землёй, 3 — у края. Коды
состояния 3D SAR: 0 — посчитано, 1 — нет сигнала, 2 — ошибка, 3 — нет CUDA.
`processing_settings.json` — словарь «номер ревизии → настройки обработки» в формате
`processing_live.json`: по `settings_revision` из `meta.json` результата видно, с
какими настройками он посчитан. При ручном сохранении туда попадают все ревизии
текущего сеанса, при записи — только использованные. `config_profile.json` — полный
профиль; разделы радара, ключей, пар, предобработки и геометрии взяты из конфигурации,
с которой шёл сбор, даже если потом загрузили другой профиль.
### Запись на диск
**Start + Record to Disk** создаёт каталог сразу и пишет в него коллекции, снятые
после нажатия; счётчик считает результаты. Данные копятся порциями по 200 коллекций и
уходят в очередь записи на 8 порций; каждая коллекция появляется на диске целиком.
Если диск не успевает и очередь заполнилась, запись останавливается с
`Disk recording stopped because the writer could not keep up`; записанное остаётся.
Если сбор так и не начался и ничего не пришло, каталог удаляется.
### Экспорт S21 JSON
**Save S21 JSON** создаёт в `<Path>/<набор данных>/` по файлу на пару
`<имя набора>_i<вход>_o<выход>_s21_vna_bscan_history.json` и профиль
`vna_config_profile.json`. Основные поля файла:
| Поле | Что |
| --- | --- |
| `format` | `"vna-system-history-v1"` |
| `input_index`, `output_index`, `channel` | пара и канал (`"s21"`) |
| `primary_stage` | `"preprocessed"` |
| `sweep_history` | список коллекций: `timestamp`, `sweep_points` (сырая развёртка, пары `[re, im]`), `calibrated_points` (предобработанная), `reference_points` (всегда пусто), `vna_config` (начало, конец, число точек) |
`timestamp` — время конца съёма развёртки этой пары (если его нет — коллекции) в
секундах монотонных часов компьютера (отсчёт от его загрузки), а не дата и время. Сырая и
предобработанная развёртки
сопоставляются по паре номер — начало съёма; если пары нет, массив пуст.
### Как открыть данные в Python
```python
import json
from pathlib import Path
import numpy as np
# Набор предобработки
with np.load("python_app/data/s21_reference/<radar_key>/<имя>.npz", allow_pickle=False) as archive:
meta = json.loads(str(archive["metadata_json"].item()))
for combo in meta["combos"]:
frequency, s21 = archive[combo["freq_key"]], archive[combo["s21_key"]]
# Предобработанные коллекции набора данных
from python_app.storage.npz.vna_history_json import collection_directories
for directory in collection_directories(Path("<набор данных>") / "preprocessed"):
meta = json.loads((directory / "meta.json").read_text())
for trace in meta["traces"]:
frequency = np.load(directory / trace["freq_file"])
s21 = np.load(directory / trace["s21_file"])
# Результаты (например, карта GPR)
for directory in collection_directories(Path("<набор данных>") / "results"):
meta = json.loads((directory / "meta.json").read_text())
for payload in meta["collection_payloads"]:
if payload["name"] == "gpr_accumulator":
image = np.load(directory / payload["image_file"])
```
`collection_directories` возвращает каталоги коллекций по порядку номеров. Загрузить
набор данных обратно в окно программы нельзя: он предназначен для обработки
отдельно.
## Рабочие файлы
Каталог `python_app/runtime/` программа создаёт и ведёт сама.
| Файл | Что | Можно удалить |
| --- | --- | --- |
| `gui_session_state.json` | путь последнего профиля: с него программа стартует | да; программа стартует с корневого профиля. Это и способ выйти из ситуации, когда запомненный профиль не даёт программе запуститься |
| `processing_live.json` | живые настройки процессора (глава [Файл конфигурации](configuration.md)) | да; программа запишет его заново |
| `run_config.json` | рабочая копия профиля последнего **Start** | при остановленном сборе |
| `s21_calibration_bundle.bin`, `s21_reference_bundle.bin`, `s21_object_calibration_bundle.bin` | выбранные наборы в формате для препроцессора, пересобираются при каждом **Start** | при остановленном сборе |
| `supervisor_children.pids` | номера процессов конвейера: если программа упала, следующий запуск завершает оставшиеся | когда процессы конвейера не работают |
| `logs/` | журналы (ниже) | при закрытой программе |
## Журналы
| Где | Что |
| --- | --- |
| `python_app/runtime/logs/radar.log` и `radar.log.1` … `radar.log.5` | журнал программы: то же, что в окне журнала, строки `дата время \| уровень \| источник \| сообщение`. Файл до 2 МиБ, хранятся пять предыдущих |
| `python_app/runtime/logs/acquisition.out.log`, `acquisition.err.log` | вывод процесса сбора, в том числе ожидание приборов и сообщения коллектора E-502 (`Collector: …`) |
| `python_app/runtime/logs/data_preprocessor.*.log`, `data_processor.*.log` | вывод препроцессора и процессора: отброшенные коллекции, отклонённый live-файл, сообщения 3D SAR (`sar3d: …`) и локатора (`locator: …`) |
| `*.prev` рядом | вывод прошлого запуска процесса или начало текущего, если файл превысил 8 МиБ |
| `journalctl -u radar.service` | при работе службой: вывод программы, включая строки `[start.sh] …` |
Уровень записей задаёт `logging.level` профиля или список **Level** в окне.
Подробности ошибки, показанной окном (например, трассировка вызовов), видны в окне
ошибки и в окне журнала, а в `radar.log` попадает само сообщение. Если процесс конвейера
завершился сам,
отчёт с последними 16 КиБ его вывода попадает в журнал программы. Раз в 50 замеров
программа пишет строку `metrics: <стадия> n=… avg=…ms min=…ms max=…ms` со временем
сбора, обработки и отрисовки.
## Разделяемая память
Кольца лежат в `/dev/shm` под именами из раздела `rings` профиля (без начальной `/`).
При ёмкости 50 и слоте 2 МиБ кольцо занимает около 100 МиБ виртуальной памяти, но
страницы выделяются только по мере записи. Кольца создаёт первый запустивший их
процесс с правами 0660 и при выходе не удаляет. `./start.sh --clean-shm` удаляет
кольца `/radar_raw`, `/radar_raw_tap`, `/radar_preprocessed`, `/radar_preprocessed_tap`,
`/radar_results` и те же имена с окончанием `_rph_radar`. Кольца с окончанием
`_simulator` он не трогает, а их используют корневой профиль, оба профиля симулятора и
оба примера с переключением стимула (`run_config_librevna_multi_per_point*.example.json`).
Их удаляйте вручную при закрытой программе: `rm /dev/shm/radar_*_simulator`.
> **Внимание.** Если программу хоть раз запустили от root (например, тесты через
> `sudo`), кольца станут принадлежать root, и обычный пользователь получит отказ в
> доступе. Удалите их при закрытой программе: `sudo rm -f /dev/shm/radar_*`.
+341
View File
@@ -0,0 +1,341 @@
# Окно программы
Окно называется **Radar System Control** и открывается развёрнутым на весь экран.
Слева — графики выбранного режима обработки, справа — панель управления. Работу ведут
несколько процессов — конвейер: процесс сбора управляет железом, препроцессор
калибрует данные, процессор их обрабатывает и держит TCP-локатор для внешних программ;
между собой они передают данные через кольца в разделяемой памяти (главы
[Устройство программы](architecture.md) и [Словарь](glossary.md)).
```text
┌──────────────────────────────────────┬─┬──────────────────────────────────┐
│ │ │ Actions (не прокручивается)│
│ │ │ ┌──────────────────────────────┐ │
│ графики │›│ │ Data Actions │ │
│ (вид зависит от режима) │ │ │ Switches │ │
│ │ │ │ Selected Preprocess Sets │ │
│ │ │ │ Processing │ │
│ │ │ │ Radar │ │
│ │ │ └──────────────── прокрутка ───┘ │
│ │ │ Status: … History: … │
│ │ │ Runtime Log Level ▾ │
│ │ │ [журнал] │
└──────────────────────────────────────┴─┴──────────────────────────────────┘
```
Стрелка между графиками и панелью (подсказка **Hide settings panel** или
**Show settings panel**) прячет и показывает всю панель; графики тогда занимают всю ширину.
Положение
стрелки не запоминается. Секция **Actions** со списком **Mode** всегда на месте,
остальные секции прокручиваются. Меню, панели инструментов и сочетаний клавиш нет.
Многие действия приостанавливают сбор: останавливают процесс сбора и препроцессор, но
оставляют работать процессор и локатор, а по окончании продолжают сбор, если он шёл.
Так работают **Apply Radar**, **Clear Runtime History**, **Remove Last Measurement**,
**Capture Temporary Reference** и съём наборов. Приостановка завершает и запись на
диск. **Stop** отменяет ожидающее продолжение. Если в это время идёт другое такое
действие, программа отвечает `Wait for the current pipeline operation to finish`.
## Actions
| Кнопка | Что делает |
| --- | --- |
| **Start** | запускает непрерывный сбор и обработку |
| **Single Capture** | снимает одну коллекцию, показывает результат и останавливает сбор |
| **Stop** | останавливает сбор |
| **Apply Radar** | отправляет настройки радара в железо, не запуская сбор |
| **Load Config…** | загружает профиль из файла |
| **Save Config…** | сохраняет текущие настройки в профиль |
| **Preprocessing…** | открывает окно **Preprocessing Setup**: наборы калибровки и референса, калибровка по объекту ([Калибровка и предобработка](preprocessing.md)) |
| **Clear Runtime History** | очищает историю в памяти |
| **Mode** | режим обработки: `pass_through`, `bscan`, `gpr`, `sar3d` ([Режимы обработки](processing.md)) |
### Start
Перед запуском программа проверяет всё и при любой ошибке отказывает, ничего не
остановив; ошибка показывается окном `Failed to start pipeline: <тип>: <текст>`.
Проверяются:
- значения полей (например, буквы в поле **Points**: `invalid literal for int()…`);
- профиль сбора: режим драйвера, модель, ключи, линии GPIO, размер слотов колец;
- требования режима обработки (B-scan, GPR, 3D SAR — в главе
[Режимы обработки](processing.md));
- выбранные наборы калибровки и референса (глава
[Калибровка и предобработка](preprocessing.md)).
Если проверка прошла, программа:
1. перезапускает процессор, если изменились настройки, от которых он зависит
(глава [Файл конфигурации](configuration.md));
2. записывает файлы наборов и рабочую копию профиля в `python_app/runtime/`;
3. очищает историю, если процессор запускается заново (первый **Start**, перезапуск на
шаге 1) или изменились профиль сбора, наборы или режекторный фильтр (тогда в
журнале `History reset because run settings changed`);
4. запускает процесс сбора и препроцессор и ждёт до 3 с, пока они поднимутся;
5. начинает читать кольца; строка состояния — **Status: running**.
Если во время работы процесс конвейера завершился, программа пишет отчёт с хвостом
его вывода и перезапускает конвейер: первый раз сразу, следующие подряд — не раньше
чем через 6, 12, 24, 48, затем 60 с после предыдущей попытки, пока не получится. Повторное
нажатие **Start** при работающем сборе отвечает
`Pipeline is already running`.
### Single Capture
Делает то же, что **Start**, но процесс сбора снимает одну коллекцию и завершается.
Если шёл непрерывный сбор, он сначала останавливается. Программа ждёт результат
коллекции, снятой после нажатия, рисует его и останавливает сбор
(`Single capture completed`). Строка состояния — **Status: single capture running**.
Если результата нет 300 с, сбор останавливается с
`Single capture timed out after 300s with no result; stopping.` В режиме 3D SAR, если
результат не посчитался, строка состояния показывает **Status: no signal**,
**Status: error** или **Status: warning**.
### Stop
Останавливает процесс сбора, дочитывает кольца (до 0,35 с), останавливает
препроцессор и дочитывает результаты (до 0,25 с). Процессор и локатор продолжают
работать: так остановленный B-scan можно пересчитать, а клиенты локатора остаются
подключёнными. Запись на диск завершается. **Stop** отменяет и ожидающие действия:
автозапуск, продолжение после приостановки, съём для калибровки по объекту,
пересчёт B-scan.
### Apply Radar
Приостанавливает сбор, проверяет профиль и:
- одиночный LibreVNA (`native`) — открывает прибор, отправляет настройки развёртки,
закрывает;
- многоприборная схема — приборы не трогает (их настраивает процесс сбора), только
перечитывает пределы мастера;
- радиофотонный радар — программирует плату лазеров, если она включена в профиле;
- режим `mock` у любой модели — ничего (`Radar pre-configuration skipped (mock mode)`).
Если работает только процессор, а настройки, от которых он зависит, изменились,
**Apply Radar** перезапускает процессор и очищает историю.
В журнал пишется `Radar settings applied: start=…, stop=…, points=…, ifbw=…, power=…`.
Профиль кнопка не меняет.
### Load Config… и Save Config…
**Load Config…** открывает выбор файла (**Load Config Profile**) и загружает профиль:
все поля, таблицы геометрии, выбранные наборы, режим. Сбор должен быть остановлен,
иначе — `Stop all pipeline processes before loading a config profile`. Ошибка чтения
показывается как `Failed to load config profile: …`, текущий профиль при этом не
меняется.
**Save Config…** (**Save Config Profile**) записывает профиль; имя без расширения
получает `.json`, существующий файл перезаписывается. Записываются все поля окна,
выбранные наборы и геометрия. Загруженный или сохранённый профиль становится
стартовым для следующего запуска.
### Clear Runtime History
Приостанавливает сбор, отбрасывает непрочитанные данные колец, очищает историю
окна и процессора, очищает графики и продолжает сбор. Файлы на диске не трогает;
подтверждения не спрашивает. Поздние результаты коллекций, снятых до очистки,
отбрасываются.
## Data Actions
| Элемент | Что делает |
| --- | --- |
| **Save Last 1000** | сохраняет до 1000 последних коллекций каждой стадии (сырые, предобработанные, результаты) в новый каталог набора данных |
| **Save S21 JSON** | сохраняет S21 предобработанной стадии в файлы JSON, по файлу на пару |
| **Remove Last Measurement** | удаляет новейшую коллекцию из истории всех стадий |
| **Capture Temporary Reference** | снимает временный референс и сразу его применяет ([Калибровка и предобработка](preprocessing.md)) |
| **Start + Record to Disk** | запускает сбор, если он остановлен, и записывает на диск следующие **Sweeps to record** коллекций |
| **Sweeps to record** | сколько коллекций записать, от 1 до 1 000 000 |
| **Path**, **Browse…** | каталог, в котором создаются наборы данных |
| **Name** | имя набора данных; пусто — `snapshot_<дата>_<время>` по UTC |
Каталог набора данных — `<Path>/<префикс>_<Name>`. Префикс описывает снятое:
модель, полосу, число точек (у радиофотонного радара — `adc`) и мощность. Каталог
не должен существовать (`Snapshot directory already exists`). Рядом с данными
кладутся профиль, с которым они сняты (`config_profile.json`), и все ревизии
настроек обработки (`processing_settings.json`). Состав файлов — в главе
[Данные на диске](data.md).
Перед сохранением программа дочитывает данные из колец; идущий сбор и запись при
этом не прерываются. Если в памяти нет ни одной коллекции, сохранение отказывает:
`No runtime data is available for save`. Окно хранит в памяти до 1000 коллекций каждой
стадии; что старее,
не сохранить. Без колец-копий (`raw_tap`, `preprocessed_tap`) сохраняются только
результаты.
**Save S21 JSON** пишет по файлу на пару: для каждой коллекции — сырая и
предобработанная развёртка, рядом профиль `vna_config_profile.json`. Его можно
направить в существующий каталог набора данных: существующие файлы данных JSON не
перезаписываются, а `vna_config_profile.json` заменяется.
**Remove Last Measurement** приостанавливает сбор и удаляет из истории окна и
процессора самую новую коллекцию; если её результат ещё в пути, процессор его не
выдаст. Повторное нажатие удаляет следующую.
**Start + Record to Disk** записывает коллекции, которые придут после нажатия. Запись
заканчивается, когда записано заданное число, по **Stop**, при приостановке сбора
(очистка истории, временный референс, съём) или если диск не успевает и очередь
записи переполнилась; причина пишется в журнал. Одновременно идёт только одна запись
(`A recording is already in progress…`), а каталог должен быть новым. Закрытие окна
дописывает принятые данные.
## Switches
| Элемент | Что делает |
| --- | --- |
| **Settling (ms)** | пауза после переключения ВЧ-ключей перед развёрткой, мс (`run.settling_ms`) |
| **Run combos** и **Select** | список пар сбора `вход:выход` через запятую; пусто — полная матрица. **Select** делает этот список источником пар |
| **Single combo**: **Input**, **Output** и **Select** | одна пара сбора; **Select** делает её источником пар |
| **B-scan pair** | какую пару показывать в B-scan; доступно в режиме B-scan |
| **Next Combination** | следующая пара B-scan; доступно, когда пар в истории хотя бы две |
Пары сбора применяются при **Start**. У многоприборной схемы список всегда равен
полной матрице и не редактируется. В режимах pass-through и B-scan, если оба ключа
`mock` или у обоих одно положение, снимается только пара `0:0`.
## Selected Preprocess Sets
Показывает выбранные наборы: **S21 calibration**, **S21 reference**,
**Object calibration**; `<not selected>` — набор не выбран. Выбираются наборы в окне
**Preprocessing Setup**.
## Processing
Страница настроек выбранного режима. Каждое поле описано в главе
[Режимы обработки](processing.md). В режимах GPR и 3D SAR над настройками — таблицы
геометрии антенн.
### Таблицы геометрии
**Tx geometry** — передающие антенны: **Output** (номер выхода), **X (m)**, **Y (m)**,
**Z (m)**. **Rx geometry** — приёмные: **Input**, **X (m)**, **Y (m)**, **Z (m)**.
Координаты — в осях радара (глава [Как работает радар](principles.md)).
- **Add Antenna** добавляет строку со следующим свободным номером; строка начинает
действовать после ввода X, и до этого кнопка недоступна. Недоступна она и тогда,
когда строк столько же, сколько выходов или входов.
- **Remove Selected** удаляет выделенные строки.
- Каждая правка проверяется: номер — целый, в пределах матрицы, без повторов;
координаты — числа; требования GPR и 3D SAR выполняются. Неверная правка
откатывается с сообщением **Invalid Antenna Geometry**.
- Принятая правка сразу уходит процессору и пересчитывает результат. Если она не
совпадает с геометрией выбранной калибровки по объекту, выбор этой калибровки
снимается.
## Radar
Для LibreVNA и симулятора секция показывает развёртку:
| Поле | Ключ профиля |
| --- | --- |
| **Start (Hz)** | `radar.sweep.start_hz` |
| **Stop (Hz)** | `radar.sweep.stop_hz` |
| **Points** | `radar.sweep.points` |
| **IF bandwidth (Hz)** | `radar.sweep.if_bandwidth_hz` |
| **Stimulus power (dBm)** | `radar.sweep.stimulus_power_dbm` |
Под полями — пределы прибора: `Limits: freq=…, points=1..N, ifbw=…, power=…`. В режиме
`mock` и у радиофотонного радара пределов нет: `Device limits are not available.` (с
`driver_mode: "native"` без подключённого прибора окно не открывается — глава
[Неполадки](troubleshooting.md)). С известными пределами подпись поля дополняется
диапазоном, а значения вне его подрезаются с предупреждением
`Radar fields were adjusted to satisfy device limits.` Поле применяется, когда из него
уходит фокус или нажат Enter. Если
изменение меняет ключ радара, выбор наборов сбрасывается, а границы частот обработки
подрезаются под новую развёртку.
Для радиофотонного радара вместо развёртки показаны поля коллектора и платы
лазеров:
| Поле | Ключ профиля |
| --- | --- |
| **Project dir**, **Executable path**, **TTY path** | `radar.rph_radar.project_dir`, `executable_path`, `tty_path` |
| **Arguments** (по строке на параметр) | `radar.rph_radar.args` |
| **Environment JSON** | `radar.rph_radar.env` |
| **Startup timeout (s)**, **Read timeout (s)**, **Stop timeout (s)** | `startup_timeout_s`, `sweep_timeout_s`, `stop_timeout_s` |
| **Board enabled** | `radar.laser_control.enabled` |
| **Board port**, **Board mode** | `port`, `mode` (`manual` или `variation`) |
| **PI 1 P**, **PI 1 I**, **PI 2 P**, **PI 2 I** | `pi_coeff1_p`, `pi_coeff1_i`, `pi_coeff2_p`, `pi_coeff2_i` |
| **Temperature 1**, **Temperature 2**, **Current 1**, **Current 2** (режим `manual`) | `manual.temp1`, `temp2`, `current1`, `current2` |
| **Static temp 1**, **Static temp 2**, **Static current 1**, **Static current 2**, **Min value**, **Max value**, **Step**, **Time step**, **Delay time** (режим `variation`) | `variation.static_temp1` … `delay_time` |
Поля платы доступны при включённой **Board enabled**; видны поля только выбранного
режима. Единицы и пределы — в главе [Файл конфигурации](configuration.md). Поля
коллектора применяются при **Start**, поля лазера — кнопкой **Apply Radar**.
## Строка состояния и журнал
Под секциями — строка состояния и счётчик истории
`History: raw=0, preprocessed=0, results=0` (сколько коллекций каждой стадии в памяти).
| Состояние | Что значит |
| --- | --- |
| **Status: idle** | сбор остановлен |
| **Status: running** | идёт непрерывный сбор |
| **Status: single capture running** | идёт однократный съём |
| **Status: error** | в журнал записана ошибка; состояние держится до следующего изменения |
| **Status: result display error (acquisition continues)** | не удалось нарисовать результат; сбор продолжается |
| **Status: runtime display/recording error (acquisition continues)** | ошибка отображения или записи на диск; сбор продолжается |
| **Status: no signal**, **Status: warning** | итог однократного съёма в режиме 3D SAR |
**Runtime Log** — журнал программы. Список **Level** (**Debug**, **Info**,
**Warning**, **Error**) задаёт наименьший уровень записей; он применяется сразу и
сохраняется в профиль при **Save Config…**. Стрелка справа прячет и показывает журнал.
В окне хранится до 2000 последних записей; при переполнении появляется
`Log panel overflow: N record(s) dropped (they are still in the log file).` Полный
журнал — в `python_app/runtime/logs/radar.log` (глава [Данные на диске](data.md)).
Повторяющиеся ошибки отрисовки показываются один раз.
## Графики
| Режим | Что на экране |
| --- | --- |
| pass-through | **Pass-Through S21**: модуль (**Magnitude (dB)**) и фаза (**Phase (deg)**) по частоте, кривая на пару, легенда `in<вход>/out<выход>` |
| B-scan | **B-scan S21 in<вход>/out<выход> \| sweeps=N**: номер развёртки — дальность |
| GPR | **GPR coherent BP — ground plane**: карта плоскости земли и объекты |
| 3D SAR | **Front (X-Y)**, **Side (Z-Y)**, **Top (X-Z)**, **3D Projection** и строка состояния 3D SAR над ними |
Подробно виды описаны в главе [Режимы обработки](processing.md). Приставку единиц на
осях (Гц, МГц, ГГц, м, мм) график подбирает сам.
Графики управляются мышью так, как принято в библиотеке pyqtgraph: колесо меняет
масштаб, перетаскивание сдвигает, правая кнопка открывает меню (**View All** —
вернуть весь график, экспорт картинки). Щелчок по строке легенды прячет и показывает
кривую. Графики окна видны и в веб-интерфейсе.
Последняя развёртка снятого набора (калибровка, референс, временный референс)
рисуется на графике pass-through с подписью вида `Tmp S21 Reference last trace`.
## Сообщения
Ошибка действия открывает окно с ошибкой и пишется в журнал. Без экрана, для
действий из веб-интерфейса и для GPIO-кнопки окон нет: ошибка уходит в журнал, а
веб-интерфейсу — в ответ на запрос. Причины частых ошибок — в главе
[Неполадки](troubleshooting.md).
Окно ошибки называется по действию и обычно показывает
`<заголовок>: <тип ошибки>: <текст>` (у записи на диск — `<заголовок>: <текст>`):
| Заголовок | Действие |
| --- | --- |
| `Failed to start pipeline` | **Start**, **Single Capture** |
| `Failed to stop pipeline` | **Stop** |
| `Failed to apply radar settings` | **Apply Radar** |
| `Failed to load config profile`, `Failed to save config profile` | **Load Config…**, **Save Config…** |
| `Failed to open preprocessing panel` | **Preprocessing…** |
| `Failed to clear runtime history`, `Failed to remove last runtime measurement` | **Clear Runtime History**, **Remove Last Measurement** |
| `Failed to save snapshot`, `Failed to save VNA history JSON` | **Save Last 1000**, **Save S21 JSON** |
| `Failed to start disk recording` | **Start + Record to Disk** |
| `Failed to capture tmp reference` | **Capture Temporary Reference** |
| `Cannot switch processing mode to <режим>` | **Mode** |
| `Failed to update live processing settings`, `Failed to update plot display settings`, `Failed to render processing settings` | правка настроек обработки и вида |
| `Failed to recompute the B-scan` | пересчёт остановленного B-scan |
| `Failed to start <вид> sequence`, `Failed to capture combo; previous captures kept, retry when ready`, `Failed to undo last preprocess capture`, `Failed to save preprocess set`, `Failed to abort capture sequence`, `Failed to refresh preprocessing sets`, `Failed to create neutral S21 sets` | окно **Preprocessing Setup** |
| `Failed to capture object-calibration data`, `Failed to calculate object calibration`, `Failed to save object calibration`, `Failed to update object calibration after geometry change` | калибровка по объекту |
| `Cannot perform data action`, `Cannot modify runtime history during active capture sequence` | действие с данными во время съёма или остановки |
| `Shutdown operation failed` | закрытие окна |
Пока идёт съём наборов, часть действий недоступна, и программа объясняет почему:
`Finish the active capture sequence first`, `Wait for preprocessing capture to finish`,
`Wait for object-calibration capture to finish`, `Wait for pipeline shutdown to finish`.
+160
View File
@@ -0,0 +1,160 @@
# Разработка
## Сборка
C++ собирается командой `make` из корня проекта; Eigen и nlohmann/json лежат в
`data_acq_and_processing/third_party/`, других внешних библиотек нет.
| Цель | Что собирает или делает |
| --- | --- |
| `all` (по умолчанию) | `build/bin/data_preprocessor`, `build/bin/data_processor` |
| `rph_radar_collector` | `build/bin/rph_radar_collector`; в `all` не входит |
| `check-native` | тесты C++: кадры, кольца, конфигурация, наборы, режекторный фильтр, локатор, псевдотерминал коллектора (на поддельной библиотеке L-Card) |
| `check-sar3d` | 3D SAR на настоящем CUDA-бэкенде против эталона |
| `check-gpr` | GPR на CPU, без видеокарты, против эталона |
| `clean` | удаляет каталог сборки |
| Переменная | По умолчанию | Что задаёт |
| --- | --- | --- |
| `BUILD_DIR` | `build` | каталог сборки. Программа запускает процессы только из `build/bin/`, другой каталог нужен для проверок |
| `CUDA_ROOTS` | пакеты `nvidia/*` в `.venv` и `/usr/local/cuda` | где искать `nvrtc.h`, `cuda.h`, `libnvrtc` |
| `PYTHON` | `.venv/bin/python` | интерпретатор для `check-sar3d` и `check-gpr` |
| `NATIVE_SANITIZERS` | не задана | санитайзеры для тестов C++, например `address,undefined` |
| `CXX`, `CXXFLAGS`, `LDFLAGS` | `g++`, `-std=c++20 -O2 …`, `-pthread -lrt` | компилятор и флаги |
`BUILD_DIR`, `CUDA_ROOTS`, `CXX`, `CXXFLAGS` и `LDFLAGS` задаются в командной строке
`make`, а не через окружение; `PYTHON` и `NATIVE_SANITIZERS` можно задать и переменной
окружения. `CXXFLAGS` в командной строке заменяет весь набор флагов, вместе с
`-std=c++20` и отслеживанием зависимостей `-MMD -MP`. `make` сам пересобирает нужное, если
изменились найденная CUDA или
санитайзеры. Сборка с CUDA и без неё описана в главе [Установка и запуск](installation.md).
## Тесты
```sh
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
make check-native
ASAN_OPTIONS=detect_leaks=0 make check-native NATIVE_SANITIZERS=address,undefined
make check-sar3d
make check-gpr
```
| Набор | Что нужно |
| --- | --- |
| Python, `python_app/tests` | `.venv` с пакетами, `QT_QPA_PLATFORM=offscreen` (дисплея у тестов нет), запись в `/dev/shm`, псевдотерминалы, UDP на loopback. Железо не нужно |
| C++, `make check-native` | g++ с C++20 |
| 3D SAR, `make check-sar3d` | сборка с CUDA и пригодная видеокарта; без неё проверяются только предупреждение и правила рамок, остальное пропускается |
| GPR, `make check-gpr` | numpy |
Особые случаи:
- `test_processor_history` запускает настоящий `build/bin/data_processor` и
пропускается, если он не собран.
- Тесты окна строят настоящее окно на временном профиле из
`run_config_examples/run_config_simulator.example.json` и не зависят от корневого
`run_config.json` (`python_app/tests/gui_test_support.py`).
- `test_deploy_scripts` гоняет `deploy/update.sh` во временном каталоге с заглушками
`systemctl` и `make`.
- `test_cart_firmware` компилирует прошивку тележки на компьютере и пропускается без
g++.
- `data_acq_and_processing/tests/test_backprojection.py` в общий прогон не входит:
его запускают `make check-sar3d` и `make check-gpr`.
- `test_documentation` проверяет документацию: `docs/configuration.md` называет каждый
ключ профиля и `processing_live.json`, главы называют каждую подпись окна и диалогов
(жирным, кодом или заголовком), а относительные ссылки ведут на существующие файлы.
Новый ключ или кнопка без описания роняет этот тест.
> **Внимание.** Не запускайте тесты от root: созданные ими кольца в `/dev/shm`
> станут принадлежать root, и программа от обычного пользователя получит отказ в
> доступе.
Проверка на старой видеокарте (NVRTC 13 не компилирует под карты старее sm_75):
```sh
make check-sar3d BUILD_DIR=/tmp/radar_build_nvrtc12 CUDA_ROOTS="$HOME/.cache/radar_nvrtc12/nvidia/cuda_nvrtc $HOME/.cache/radar_nvrtc12/nvidia/cuda_runtime"
```
## Эталоны 3D SAR и GPR
`data_acq_and_processing/tests/sar3d_reference.npz` и `gpr_reference.npz` строит
генератор, который выполняет неизменённые функции ноутбука
`Multi_depthnorm_clean.ipynb` (ячейки 1 и 5) и скрипта `Horns_motion_4x4_ground.py` с
заменёнными параметрами. В эталоне 3D SAR десять случаев: шесть объёмов (ширина
отклика, вычитание среднего предыдущих коллекций для каждого кадра апертуры,
отношение к фону) и четыре сцены точечных отражателей в шуме для CLEAN с объектами и
рамками. Правила рамок проверяются по одному на синтетических объектах. Архивы
записывают хеши SHA-256 исходников и версии NumPy и SciPy.
Перегенерировать эталоны нужно только при правке алгоритма; ноутбук и скрипт
хранятся вне репозитория:
```sh
.venv/bin/python data_acq_and_processing/tests/generate_backprojection_reference.py \
--notebook /путь/к/Multi_depthnorm_clean.ipynb \
--gpr-script /путь/к/Horns_motion_4x4_ground.py
```
Другую сборку можно проверить так: `make check-sar3d BUILD_DIR=/путь/к/сборке` или
`BACKPROJECTION_FIXTURE=/путь/к/сборке/native_tests/backprojection_fixture` при запуске
`test_backprojection.py Sar3dTest`.
## Утилиты
| Команда | Что делает |
| --- | --- |
| `.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config <профиль> --sweeps 200 [--warmup 10] [--apply]` | фазовая калибровка радиофотонного радара (глава [Оборудование](hardware.md)) |
| `.venv/bin/python -m python_app.scripts.per_point_switching_check --config <профиль> [--collections 3] [--switch-connected]` | проверка переключения стимула на железе; выход 0 — норма, 1 — сбой |
| `./start.sh --producer-only --profile <профиль>` | только процесс сбора с выводом в терминал — для отладки железа |
## Правила кода
- Обязателен только функциональный контракт: программа в правильном порядке
собирает коллекции, предобрабатывает, обрабатывает и показывает результат в окне и в
вебе, даёт выбрать настройки предобработки и выполнить действия пользователя.
Внутреннее устройство можно менять под этот контракт.
- Код — читаемый и без лишнего: понятные имена, без мёртвых опций и «умных» трюков.
- Наименьшая версия Python — 3.10 (код использует `dataclass(slots=True)` и `match`).
- `.venv/bin/pyright python_app` — 0 ошибок, без подавлений и расширения исключений
(режим `basic`).
- Нумерацию антенн и портов задаёт профиль: она уточняется по железу, менять её
самостоятельно нельзя.
- Корневой `run_config.json` остаётся профилем симулятора.
- Изменив поведение, исправьте документацию в том же изменении (таблица «Где что
править» в главе [Устройство программы](architecture.md)); полноту ключей и подписей
проверяет `test_documentation`. Документ описывает, как устроено сейчас; числа
перепроверяются по коду.
## Прошивки
> **Внимание.** `top_master.bin` и `combined.vnafw` — единственные копии рабочего
> битстрима мастера и рабочего образа; битстрим заново не воспроизводится. Не
> перезаписывайте их, новые сборки кладите под новыми именами.
- VHDL — только ASCII: любой другой символ, даже длинное тире в комментарии, ломает
GHDL (`invalid character, even in a comment`).
- В `VNA.xise` закреплено `Starting Placer Cost Table (1-100) Map spartan6 = 2`; не
сбрасывайте его в 1: ограничение `MCU_MOSI OFFSET hold` и в стоковой прошивке
проходит с запасом всего +0,458 нс.
- После правки FPGA прогоните тестбенч и мутационный тест — внесите ошибку и
убедитесь, что тестбенч падает:
```sh
ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep --assert-level=error
```
- Изменение объёма логики FPGA сравнивайте по Timing Score с контрольной сборкой
стоковой прошивки.
Порядок сборки прошивок —
[reproduce/README.md](../device_firmware/multi_libreVNA_firmware/reproduce/README.md).
## Git
- Основная ветка — `main`.
- Правила `build*` и `build/` в корневом `.gitignore` скрывают и файлы комплекта сборки
прошивок (в том числе `build.sh` и эталонные бинарники STM32); для них добавлены
исключения, а часть файлов скрывает ещё и вложенный `.gitignore` LibreVNA. Добавляя
файлы в `device_firmware/`, проверяйте `git status --ignored` и при необходимости
`git add -f`.
- В репозиторий не входят: `.venv`, `build/`, `python_app/data*`, `python_app/runtime`,
ноутбуки `*.ipynb`, локальные инструменты разработчиков.
+64
View File
@@ -0,0 +1,64 @@
# Словарь
Русские термины — по алфавиту, за ними термины, которые пишутся латиницей.
| Термин | Значение |
| --- | --- |
| А-скан | профиль дальности одной пары: амплитуда отражения в зависимости от расстояния; получается обратным преобразованием Фурье развёртки S21 |
| апертура | набор точек, из которых снята сцена. В 3D SAR — текущая коллекция и до `SAR_FRAME_RADIUS` предыдущих |
| боковой лепесток | ложный слабый пик рядом с настоящим на А-скане или изображении; появляется из-за того, что полоса частот ограничена |
| ВАЦ (векторный анализатор цепей, VNA) | прибор, который выдаёт синусоиду заданной частоты и измеряет амплитуду и фазу прошедшего сигнала; в системе — LibreVNA |
| ведомый прибор (slave) | LibreVNA многоприборной схемы, который только принимает |
| воксель | ячейка трёхмерной сетки 3D SAR |
| временный референс | референс, снятый одной кнопкой во время работы; хранится как набор `tmp_reference` |
| вход (input) | приёмная сторона пары: номер приёмной антенны |
| ВЧ-ключ | коммутатор H7992: подключает порт прибора к одной из антенн (их до четырёх) по двум логическим линиям |
| выход (output) | передающая сторона пары: номер передающей антенны; у LibreVNA совпадает с номером стадии прибора |
| геометрия антенн | координаты передающих и приёмных антенн в осях радара, таблицы **Tx geometry** и **Rx geometry** |
| дальность | половина пути волны от передающей антенны через отражатель к приёмной; в GPR и 3D SAR — расстояние вдоль оси обзора радара |
| дБ | логарифмическая мера отношения: для амплитуд 20·lg, для мощностей 10·lg |
| дБм | мощность в децибелах относительно 1 мВт |
| калибровка (through) | эталонное измерение тракта; каждая развёртка делится на него |
| калибровка по объекту | поправка пар по известному отражателю, выравнивающая их по фазе |
| ключ радара (`radar_key`) | строка из настроек железа; по ней находятся наборы, снятые при тех же настройках |
| когерентное суммирование | сложение комплексных значений с учётом фазы: вклады настоящего отражателя складываются, остальные гасятся |
| коллекция (collection) | все пары, снятые за один обход; единица данных в программе |
| кольцо (ring) | кольцевой буфер в разделяемой памяти, через который процессы передают кадры |
| копия кольца (tap) | кольца `raw_tap` и `preprocessed_tap`: копии для окна программы |
| локатор | TCP-сервер процессора: отдаёт объекты GPR и принимает скорость |
| мастер (master) | LibreVNA многоприборной схемы, который излучает и задаёт запуск |
| набор (предобработки) | сохранённое измерение для предобработки: калибровка, референс или калибровка по объекту |
| набор данных | каталог с сохранёнными коллекциями и результатами |
| нейтральные наборы | калибровка, равная 1, и референс, равный 0: данные проходят предобработку без изменений |
| обратная проекция | построение изображения: для каждой точки сумма значений А-сканов всех пар на задержке до этой точки |
| обратное преобразование Фурье | пересчёт значений по частотам в значения по времени (задержке); переводит развёртку S21 в А-скан |
| окно Ханна | плавное взвешивание частот перед преобразованием Фурье, снижающее боковые лепестки |
| окно съёма | время начала и конца измерения по монотонным часам компьютера; есть у коллекции и у каждой трассы |
| пара (combo) | сочетание передающей и приёмной антенны, записывается `вход:выход` |
| переключение стимула внутри частотной точки | режим многоприборной схемы, в котором каждый порт мастера на каждой частоте излучает через оба положения ключа; даёт матрицу 4 × 4 |
| полоса ПЧ (IF bandwidth) | полоса фильтра приёмника ВАЦ; чем уже, тем меньше шум и медленнее развёртка |
| предельная дальность | дальность, после которой А-скан повторяется: c / (2Δf) |
| препроцессор | процесс `data_preprocessor`: калибровка, вычитание референса, режекторный фильтр, калибровка по объекту |
| проекция максимума | плоская картинка объёма: в каждой её точке — наибольшее значение вдоль направления взгляда |
| профиль | файл JSON со всеми настройками программы |
| процесс сбора | процесс, который управляет железом и публикует коллекции |
| процессор | процесс `data_processor`: режимы обработки и локатор |
| развёртка (sweep) | один проход прибора по списку частот |
| разрешение по дальности | наименьшее расстояние между двумя отражателями, при котором они видны раздельно: c / (2B) |
| ревизия настроек | номер набора настроек обработки; процессор возвращает его в каждом результате |
| режекторный фильтр (notch) | обнуление заданных полос частот |
| режим обработки | что процессор делает с данными: pass-through, B-scan, GPR, 3D SAR |
| референс | измерение пустой сцены; вычитается как фон |
| симулятор | работа без железа (`driver_mode: "mock"`): программа моделирует один движущийся отражатель |
| стадия прибора | одно излучение на частоте: на каждой частоте прибор проходит все стадии по очереди |
| угол обзора | наклон радара вниз от горизонтали |
| фон кадра | медиана рабочей части объёма 3D SAR (ненулевые воксели опорных плоскостей); без объектов CLEAN кадр показывается, когда его максимум превышает фон в 6 раз |
| частотная точка | одна частота развёртки |
| ширина на половине максимума | ширина отклика на уровне половины его пика; её размер имеет квадрат, которым 3D SAR отмечает максимум |
| B-scan | изображение «номер развёртки — дальность» для одной пары |
| CLEAN | алгоритм, который по шагам вычитает самый сильный отклик, чтобы разделить объекты |
| GPR | режим обработки: изображение плоскости земли перед радаром и поиск объектов на нём |
| live-файл | `python_app/runtime/processing_live.json`: настройки обработки, которые программа передаёт процессору на ходу |
| RPH (радиофотонный радар) | радар, в котором СВЧ-сигнал формируется лазерами; данные оцифровывает АЦП E-502 или плата RFG |
| S21 | комплексный коэффициент передачи из порта 1 в порт 2: отношение принятой волны к поданной, то есть ослабление и сдвиг фазы |
| SAR (3D SAR) | радар с синтезированной апертурой; режим, строящий трёхмерное изображение сцены |
+382
View File
@@ -0,0 +1,382 @@
# Оборудование
Тип радара задаёт поле `radar.model` профиля, а `radar.driver_mode` — работать с
настоящим железом (`"native"`) или с симулятором (`"mock"`). Ключи профиля описаны
в главе [Файл конфигурации](configuration.md), принцип измерения — в главе
[Как работает радар](principles.md), термины — в [словаре](glossary.md).
| Радар | `radar.model` | Что подключается | Процесс сбора |
| --- | --- | --- | --- |
| Одиночный LibreVNA | `librevna` | один LibreVNA по USB, два ВЧ-ключа на GPIO | `python_app.scripts.matrix_raw_producer` |
| Многоприборная схема | `librevna_multi` | три LibreVNA по USB, петля триггера; в режиме переключения стимула — контроллер STM32 и ключи на портах мастера | `python_app.scripts.matrix_raw_producer` |
| Радиофотонный радар с АЦП | `rph_radar`, `radar.rph_radar.source: "e502"` | АЦП L-Card E-502 по USB, плата лазеров по последовательному порту, два ВЧ-ключа на GPIO | `python_app.scripts.rph_radar_raw_producer` и программа `rph_radar_collector` |
| Радиофотонный радар с платой RFG | `rph_radar`, `radar.rph_radar.source: "rfg_udp"` | плата RFG по Ethernet, плата лазеров по последовательному порту; одна пара антенн | `python_app.scripts.rph_radar_raw_producer` |
| Симулятор | `librevna` или `librevna_multi` с `driver_mode: "mock"` | ничего | `python_app.scripts.matrix_raw_producer` |
Для радиофотонного радара симулятора нет: он работает только с `driver_mode: "native"`.
Процесс сбора открывает приборы и при неудаче повторяет попытки бесконечно, с паузой
от 1 до 10 с: прибор можно подключить и после запуска. Его сообщения пишутся в
`python_app/runtime/logs/acquisition.err.log`, а в журнал окна попадают, только если
процесс завершился. Ошибки сбора и их признаки собраны в главе
[Неполадки](troubleshooting.md).
## LibreVNA
LibreVNA — двухпортовый векторный анализатор цепей (ВАЦ) с USB. В системе используются платы
ревизии B. Прошивки — изменённая версия 1.6.5, протокол версии 14 (программа
проверяет версию при подключении одиночного прибора).
| Предел | Значение |
| --- | --- |
| Частота | 0–6 ГГц |
| Полоса ПЧ | 6 Гц–50 кГц |
| Число точек | до 4501 |
| Мощность | от −40 до 0 дБм |
| Задержка на стадии `dwell_us` | до 10 239 мкс |
Ниже 25 МГц генератор прибора переключается на другую микросхему, и развёртка идёт
с неравномерными длинными паузами: если развёртка должна быть быстрой, начинайте её не
ниже 30 МГц (так сделано в `run_config_librevna_multi_per_point_lowband.example.json`).
С `driver_mode: "native"` программа при запуске окна, загрузке профиля и нажатии
**Apply Radar** читает пределы прибора и подрезает поля секции **Radar** под них
(`Radar fields were adjusted to satisfy device limits.` в журнале). У многоприборной
схемы пределы читаются с мастера. Если прибор с `driver_mode: "native"` не
подключён, окно программы не открывается: чтение пределов при старте завершается
ошибкой.
USB-доступ к прибору даёт правило udev `/etc/udev/rules.d/99-radar-librevna.rules`
(группа `plugdev`). Его ставит `./start.sh` при обычном запуске с профилем LibreVNA;
при запуске без экрана (`--headless`) правило не ставится.
## Одиночный LibreVNA
Прибор подключается по USB; если `radar.serial` пуст, берётся первый найденный.
Порт 1 излучает, порт 2 принимает. Выходной ВЧ-ключ (`switches.port1`) подключает
порт 1 к одной из передающих антенн, входной (`switches.port2`) — порт 2 к одной из
приёмных. Ключами управляет компьютер через GPIO.
Сбор одной коллекции:
1. Для каждой пары из списка сбора программа выставляет выходной ключ, затем входной,
и ждёт время установления `run.settling_ms`.
2. Запускает развёртку и принимает все точки. Если какая-то точка не пришла,
коллекция не публикуется, а прибор открывается заново.
3. Публикует коллекцию в кольцо `raw` и начинает следующую.
S21 одиночного прибора — отношение сигнала порта 2 к сигналу опорного приёмника.
**Apply Radar** открывает прибор, отправляет настройки развёртки и закрывает его.
## Многоприборная схема
Три LibreVNA: мастер (`radar.serial`) и два ведомых
(`radar.multi_device.slave_serials`, ровно два). Мастер на каждой частоте излучает по
очереди с порта 1 и порта 2, ведомые не излучают и принимают каждый на оба порта.
S21 — сигнал приёмника ведомого, делённый на сигнал опорного приёмника мастера на той
же стадии.
| Номер | Что это |
| --- | --- |
| вход 0, 1 | первый ведомый из `slave_serials`, порт 1 и порт 2 |
| вход 2, 3 | второй ведомый, порт 1 и порт 2 |
| выход 0, 1 (без переключения стимула) | мастер, порт 1 и порт 2 |
| выход 0, 1, 2, 3 (переключение стимула, два положения) | порт 1 положение A, порт 1 положение B, порт 2 положение A, порт 2 положение B |
Номер выхода совпадает с номером стадии прибора. Многоприборная схема всегда снимает
полную матрицу: список пар `run.combos` у неё заменяется всеми парами, и в окне он не
редактируется.
Приборы синхронизированы аппаратным сигналом запуска. Петля триггера:
`TRIGGER_OUT` мастера → первый ведомый → второй ведомый → `TRIGGER_IN` мастера.
Вернувшийся сигнал означает «все готовы». Мастер измеряет по вернувшемуся сигналу,
поэтому задержки в петле одинаковы для всех приборов и в отношении «приёмник/опорный»
сокращаются.
Опорная частота: поле `radar.multi_device.force_external_reference` включает внешнюю
опору 10 МГц сразу на всех трёх приборах (раздельно задать нельзя). Во всех профилях
оно выключено. Включайте его, только если опора 10 МГц действительно подана на все
приборы. Признак отсутствующей опоры при включённом поле — мастер не отдаёт точки:
`Packet not received from <серийный номер> within 1.0 s`.
Сбор: перед первой коллекцией программа настраивает опору на всех приборах,
затем отправляет настройки развёртки сначала ведомым, потом мастеру, чтобы приёмники
были готовы раньше, чем мастер выдаст запуск. Развёртка идёт непрерывно. Каждый
прибор передаёт свои точки по отдельному USB; программа ждёт начала развёртки на всех
трёх и принимает цикл, только если начала совпали по времени. Если пропало не больше
5 % точек трассы, они заменяются нулями (`Zero-filled dropped sweep point(s) to keep the cycle…`),
иначе цикл отбрасывается.
| Сбой | Предел |
| --- | --- |
| от прибора нет ни одной точки | 1,5 с |
| нет начала развёртки | 6 с |
| цикл не закончился | 8 с |
| повторные попытки без переподключения | 2 |
| переподключение всех приборов | `radar.multi_device.recovery_attempts` раз (по умолчанию 3), не дольше 10 с |
После этого процесс сбора пересоздаёт подключение и ждёт приборы бесконечно. Если
не пришло ни одной точки ни от одного прибора, в журнале процесса сбора будет
`No datapoints received from at least one device; hardware trigger sync did not start. Check Trigger Out/In loop and 10 MHz reference wiring.`
**Apply Radar** в этой схеме приборы не трогает: их настраивает процесс сбора
(`Multi-device raw producer will configure all LibreVNA devices`), кнопка только
перечитывает пределы мастера.
Прошивки: мастер работает с образом `combined.vnafw` (обычная раскладка стадий с
аппаратным триггером) или `combined_per_point_switching.vnafw` (переключение стимула),
ведомые — с `combined_external_lo_slave.vnafw`. Образы и правила их сборки — в
разделе «Прошивки» ниже.
## Переключение стимула внутри частотной точки
В этом режиме одна развёртка даёт матрицу 4 × 4 вместо 2 × 4: на каждом порту
мастера стоит ВЧ-ключ, и на каждой частоте каждый порт излучает дважды, через оба
положения ключа. Все 16 пар одной частоты измеряются за доли миллисекунды, поэтому
движение радара между ними пренебрежимо мало. Полное описание —
[per_point_switching.md](../device_firmware/per_point_switching.md).
Что нужно:
- прошить мастер образом `combined_per_point_switching.vnafw`; ведомые не трогать;
- поставить контроллер ключей (плата WeAct STM32G431CB с прошивкой
`device_firmware/switch_controller`) в разрыв петли триггера между
`TRIGGER_OUT` мастера и `TRIGGER_IN` первого ведомого;
- в профиле задать `radar.multi_device.stimulus_switch_positions: 2` и
`radar.sweep.dwell_us: 2`. Ключами на портах мастера управляет контроллер, а не
компьютер; разделы `switches` у многоприборной схемы программа сама ставит в `mock`.
| Вывод STM32 | Куда |
| --- | --- |
| PB0 (вход) | `TRIGGER_OUT` мастера |
| PB1 (выход, повторяет PB0) | `TRIGGER_IN` первого ведомого |
| PB2 | ключ порта 1 мастера |
| PB6 | ключ порта 2 мастера |
| PB5 | отладка: импульс 2 мкс на каждую поправку чётности |
Контроллер переключает оба ключа по каждому спаду сигнала запуска (положение A —
логический 0, B — 1). Если пауза между стадиями в 1,5 раза длиннее наименьшей из
последних 16 пауз, то есть на границе развёртки, он принудительно возвращает положение
A и считает это поправкой чётности. Задержка, которую он вносит в петлю (около 50 нс),
одинакова для всех
приборов и на фазу не влияет. Через 200 мс простоя триггера контроллер печатает
отчёт по USB (115 200 бод): `trigger cycles`, `resyncs (sweep ends)`, `parity corrections`
(должно быть `0 (ok)`), `stages per resync` (ожидается число точек × число стадий),
паузы и их гистограмма.
Проверка на железе — `python_app/scripts/per_point_switching_check.py --config <профиль>`:
она отказывает, если медиана модуля S21 какого-то выхода ниже −80 дБ (признак старой
прошивки мастера) или если два положения одного порта без подключённого ключа
различаются больше чем на 6 дБ. С ключом нужен флаг `--switch-connected`.
> **Внимание.** Полоса ПЧ 1 кГц в этом режиме воспроизводимо роняет приборы
> (`LIBUSB_ERROR_IO`). Работают 5, 10, 20 и 50 кГц; рабочее значение — 50 кГц.
## ВЧ-ключи H7992
Ключ подключает порт прибора к одной из до четырёх антенн. Положение задают две
линии GPIO, A (`pin_a`) и B (`pin_b`); линии нужны обе даже при двух положениях.
| Положение | A | B |
| --- | --- | --- |
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 2 | 1 | 0 |
| 3 | 1 | 1 |
Номера линий в профилях — номера линий GPIO-чипа (`gpio_chip`, обычно
`/dev/gpiochip0`), а не номера контактов разъёма. Во всех профилях-примерах, кроме
примера с платой RFG (там ключей нет), выходной ключ на линиях 17 (A) и 27 (B),
входной — на 22 (A) и 23 (B). При открытии программа
выставляет ключ в `default_position`. Ключ с `driver_mode: "mock"` никуда не
переключается, программа только помнит его положение.
Кто управляет ключами:
| Радар | Кто переключает |
| --- | --- |
| одиночный LibreVNA | процесс сбора на Python |
| многоприборная схема | процесс сбора, только если `multi_device.output_switch_positions` или `input_switch_positions` больше 1; в режиме переключения стимула — контроллер STM32 |
| радиофотонный с E-502 | программа `rph_radar_collector`, если оба ключа `native`; иначе процесс сбора |
> **Внимание.** На Raspberry Pi 5 номер GPIO-чипа 40-контактного разъёма менялся от
> версии ядра к версии. Перед работой проверьте его на самой машине: `gpiodetect` и
> `gpioinfo -c gpiochip0`. Неверный чип означает ключи, которые молча не
> переключаются.
## Радиофотонный радар
Плата лазеров меняет ток лазера LD1 ступенями, при этом ток второго лазера и
температуры обоих постоянны. На каждой ступени оцифровываются основной сигнал и
опорный. Опорное плечо имеет постоянную задержку, поэтому его фаза растёт с частотой
линейно; по двум опорным точкам `phase_calibration` (фаза — частота) программа
определяет частоту каждой ступени. S21 — основной сигнал, делённый на модуль
опорного. Развёртки, которые не покрывают выходную полосу `radar.rph_radar.band`,
отбрасываются, остальные пересчитываются на её равномерную сетку (по умолчанию
2,1–5,5 ГГц, 2048 точек). Поля `radar.sweep` для этого радара не используются, в
окне вместо секции LibreVNA показывается секция RPH.
### Плата лазеров
Плата подключается по последовательному порту (115 200 бод) и программируется
кнопкой **Apply Radar**, если в профиле `laser_control.enabled: true`. Программа
проверяет все значения до отправки первого байта, затем сбрасывает плату и отправляет
постоянные токи и температуры, а в режиме `variation` — ещё задачу развёртки тока
LD1. Каждая команда должна получить ответ «состояние 0»; иначе
`Device rejected command: STATE 0x…`. После закрытия порта развёртка лазера
продолжается.
| Величина | Пределы | По умолчанию |
| --- | --- | --- |
| температура лазеров | 15–40 °C | 24,0 и 24,9 °C |
| ток лазеров | 15–110 мА | 97,3 и 102,0 мА |
| развёртка тока LD1 | 15–110 мА, начало меньше конца | 97,3 → 107,0 мА |
| шаг тока | 0,002–0,5 мА | 0,05 мА |
| длительность ступени | 20–100 мкс | 50 мкс |
| пауза между развёртками | 3–10 мс | 10 мс |
При запуске без экрана **Apply Radar** выполняется автоматически через 0,5 с после
старта, а через 1 с после него — **Start**, даже если **Apply Radar** не удался.
В веб-интерфейсе **Apply Radar** нет.
### АЦП L-Card E-502
Данные с АЦП читает отдельная программа `build/bin/rph_radar_collector` (собирается
командой `make rph_radar_collector`, в обычный `make` не входит). Её запускает и
останавливает процесс сбора; программа живёт, только пока жив её запустивший
процесс. Библиотеки L-Card `libx502api.so` и `libe502api.so` она загружает при
запуске, по умолчанию из `~/.local/lib`. Развёртки она передаёт процессу сбора через
псевдотерминал `rph_radar.tty_path` (в примере `/tmp/ttyADC_data`). Устройство
выбирается первое найденное. Подробности — в
[README коллектора](../data_acq_and_processing/rph_radar_collector/README.md).
Параметры коллектора задаются списком `radar.rph_radar.args`:
| Параметр | Что задаёт | По умолчанию |
| --- | --- | --- |
| `range:<В>` | диапазон входа ±В: `10`, `5`, `2`, `1`, `0.5` или `0.2` | ±2 В |
| `internal_ref_hz:<Гц>` | внутренняя опорная частота, она же наибольшая частота АЦП: `2000000` или `1500000` | 2 МГц |
| `duration_ms:<мс>` | наибольшая длительность развёртки, больше 0 и не больше 60 000 мс; более длинная считается испорченной | 100 мс |
| `do8_cycle_period:<n>` | период выхода DO8 в словах цифрового вывода, 4–262144, причём НОК(4, n) не больше 262144 (нечётные n больше 65 536 не подходят) | 8 |
Остальные слова в `args` (`profile:phase`, `clock:internal`, `start:di_syn2_rise`,
`stop:di_syn2_fall`, `sample_clock_hz:max`, `packet_limit:0`, `do1_toggle_per_frame`,
`do1_pair_subtract_avg`, `do8_freq_ref`) только подтверждают единственный
поддерживаемый режим; любое другое слово отвергается.
Как коллектор использует цифровые линии АЦП:
| Линия | Что делает коллектор |
| --- | --- |
| DI_SYN2 | фронт открывает развёртку, спад закрывает |
| DI1 | смена уровня отмечает новую ступень |
| DI2 | делит отсчёты ступени на две группы; значение ступени — среднее разности групп |
| DI8 | отсчёты с высоким DI8 идут в опорный канал |
| DO1 | меняется на каждом кадре |
| DO2 | высокий, пока идёт поток |
| DO8 | высокий на двух последних словах каждого периода `do8_cycle_period` |
Если ключами управляет коллектор, он переключает их сам между развёртками, ждёт
`run.settling_ms` (не меньше 1 мс) и открывает новый сеанс АЦП; первая развёртка
после переключения отбрасывается.
### Плата RFG
Плата RFG шлёт по UDP точки развёртки на адрес `radar.rph_radar.udp_bind`. В примере
это проводной адрес компьютера `192.168.2.1:5000`: привязка к проводному интерфейсу
отсекает чужие широковещательные пакеты (например, из Wi-Fi), а любой непонятный пакет
программа считает ошибкой. В каждой точке три канала; основной сигнал — разность
каналов 0 и 1 (синхронный детектор), опорный — канал 2.
Требования профиля:
- `laser_control.mode: "variation"` — даже если лазер запускают из другой программы:
по его настройкам вычисляется длина развёртки;
- у обоих ключей одно положение, то есть одна пара антенн: без метки от источника
нельзя доказать, что развёртка после переключения свежая.
Длина развёртки — ⌈(конец − начало) / шаг⌉ ступеней в кодах ЦАП тока; при настройках
по умолчанию 199 ступеней по 50 мкс. Развёртка принимается, только если пришли все
ступени подряд. Плата теряет несколько процентов точек, такие развёртки
отбрасываются. Время каждой точки — метка ядра о приёме пакета.
### Фазовая калибровка
Фазовые якоря `radar.rph_radar.phase_calibration` измеряются программой
`python_app/scripts/rph_radar_calibrate.py`. Перед ней примените настройки лазера
кнопкой **Apply Radar** и остановите сбор.
```sh
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200
.venv/bin/python -m python_app.scripts.rph_radar_calibrate --config run_config.json --sweeps 200 --apply
```
Первая команда снимает развёртки и печатает измеренные фазы начала и конца, вторая
снимает заново и записывает в профиль только `phase0_rad` и `phase1_rad`, не трогая
остальные байты файла. Частотные якоря `freq0_hz` и `freq1_hz` должны заранее
описывать известные концы развёртки. `--warmup` задаёт, сколько первых развёрток
отбросить (по умолчанию 10). Команды лазеру программа не отправляет. Если профиль
изменился во время съёма, она его не перезаписывает.
## Симулятор
С `driver_mode: "mock"` процесс сбора не трогает железо и моделирует один точечный
отражатель. Отражатель движется по восьмисекундному циклу: вдоль ряда антенн — вокруг
его середины (размах — половина ширины ряда плюс 0,25 м, но не меньше 0,75 м), по
дальности — между 3,25 и 4,25 м, на высоте 0,25 м. S21 каждой пары
считается по настоящим расстояниям от антенн из профиля; коллекции идут не чаще чем
раз в 50 мс. Шума, помех, переключения ключей и движения радара симулятор не
моделирует; скорость радара при работе с ним должна быть нулевой. Предобработке
по-прежнему нужны наборы; для симулятора подходят нейтральные (глава
[Калибровка и предобработка](preprocessing.md)).
## GPIO-кнопка
К компьютеру можно подключить кнопку, которая снимает временный референс — то же,
что **Capture Temporary Reference**. По умолчанию она выключена (раздел
`control_button` профиля, ни в одном примере не включена). Схема, на которую
рассчитаны умолчания: кнопка между линией GPIO и землёй, внутренняя подтяжка к
питанию, нажатие — спад уровня. Дребезг подавляет ядро (`debounce_ms`, по умолчанию
50 мс). Настройки кнопки читаются при запуске окна; после загрузки другого профиля
нужно перезапустить программу.
## Тележка
`device_firmware/cart_firmware` — прошивка платы WeAct STM32G431CB, которая заменяет
пульт тележки: читает джойстик и выдаёт команды движения в формате SBUS. С
программой радара она не связана. Скорость для поправки на движение радар получает
от клиента TCP-локатора (глава [Веб-интерфейс, кнопка и локатор](web.md)).
Протокол пульта — [protocol.md](../device_firmware/cart_firmware/docs/protocol.md).
## Прошивки
| Где | Что | На чём работает |
| --- | --- | --- |
| `device_firmware/multi_libreVNA_firmware/combined.vnafw` | образ мастера: обычные стадии и аппаратный триггер | LibreVNA-мастер |
| `device_firmware/multi_libreVNA_firmware/combined_per_point_switching.vnafw` | образ мастера с переключением стимула | LibreVNA-мастер |
| `device_firmware/multi_libreVNA_firmware/combined_external_lo_slave.vnafw` | образ ведомого | LibreVNA-ведомые |
| `device_firmware/multi_libreVNA_firmware/reproduce/` | полное дерево LibreVNA 1.6.5 с доработками и комплект воспроизводимой сборки | ПК разработчика |
| `device_firmware/switch_controller/` | контроллер ключей для переключения стимула | WeAct STM32G431CB |
| `device_firmware/cart_firmware/` | пульт тележки | WeAct STM32G431CB |
Образ `.vnafw` заливается в LibreVNA программой LibreVNA-GUI (Device → Firmware
Update). Платы STM32 прошиваются через PlatformIO и ST-Link: `pio run -t upload`.
> **Внимание.** `top_master.bin` и `combined.vnafw` — единственные копии рабочего
> битстрима мастера и рабочего образа прошивки, битстрим заново не воспроизводится.
> Никогда их не перезаписывайте; новые сборки сохраняйте под новыми именами.
Подробно:
- [reproduce/README.md](../device_firmware/multi_libreVNA_firmware/reproduce/README.md) — как
устроен комплект сборки, отличия от LibreVNA 1.6.5, что воспроизводится побайтово;
- [install_windows/README.txt](../device_firmware/multi_libreVNA_firmware/reproduce/install_windows/README.txt)
—
сборка мастера с переключением стимула на Windows;
- [per_point_switching.md](../device_firmware/per_point_switching.md) — переключение
стимула: раскладка стадий, изменения FPGA и микроконтроллера, порядок ввода в
строй;
- [switch_controller/README.md](../device_firmware/switch_controller/README.md) —
контроллер ключей: разводка, отчёт, отладка;
- [librevna_protocol.md](../device_firmware/librevna_protocol.md) — USB-протокол
LibreVNA и форматы кадров между процессами программы.
+238
View File
@@ -0,0 +1,238 @@
# Установка и запуск
Программа работает в Linux: на рабочей станции с экраном и на Raspberry Pi 5 без
оператора. Python-часть ставится в виртуальное окружение `.venv` внутри каталога
проекта, C++-часть собирается командой `make` в `build/bin/`.
## Что нужно
| Что | Зачем |
| --- | --- |
| Linux, bash, coreutils, `flock` | запуск через `start.sh` |
| Python 3.10 или новее с модулем `venv` | программа и окружение |
| компилятор C++20 (g++), `make` | процессы конвейера |
| библиотеки `libusb-1.0` и `libatomic.so.1` | USB-доступ к LibreVNA и кольца разделяемой памяти |
| `libxcb-cursor0`, `libxkbcommon-x11-0` (Debian/Ubuntu) | окно Qt на рабочем столе |
| видеокарта NVIDIA с драйвером, NVRTC и заголовки CUDA | только для режима 3D SAR |
| библиотеки L-Card `libx502api.so`, `libe502api.so` | только для радиофотонного радара с АЦП E-502 |
Python-пакеты перечислены в `requirements.txt`:
| Пакет | Для чего |
| --- | --- |
| `numpy` | массивы данных |
| `scipy` | оптимизация в калибровке по объекту |
| `libusb1` | USB-драйвер LibreVNA |
| `pyserial` | последовательный порт платы лазеров |
| `PyQt6` | окно программы (без экрана — тоже, в режиме `offscreen`) |
| `pyqtgraph` | графики |
| `fastapi`, `uvicorn[standard]` | веб-интерфейс |
| `nvidia-cuda-nvrtc`, `nvidia-cuda-runtime` | NVRTC и заголовки CUDA для 3D SAR; ставятся только на x86_64 |
PyQt6 ставится только из готовых сборок (wheel), поэтому pip выбирает самую новую
версию, которая работает с системной glibc.
## Установка
Из каталога проекта:
```sh
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements.txt
make # build/bin/data_preprocessor и build/bin/data_processor
make rph_radar_collector # только для радиофотонного радара с АЦП E-502
```
`./start.sh` при обычном запуске делает это сам: создаёт `.venv`, если его нет, ставит
недостающие пакеты, ставит системные библиотеки через менеджер пакетов (apt, dnf, yum,
pacman, zypper или brew, через `sudo`), правило udev для LibreVNA и собирает C++. Без
экрана (`--headless`) он ничего не ставит: всё должно быть установлено заранее.
## Запуск
```sh
./start.sh --profile run_config_examples/run_config_librevna_multi_per_point.example.json
```
Без `--profile` программа берёт последний использованный профиль, а если его нет —
корневой `run_config.json` (симулятор). Порядок выбора профиля — в главе
[Файл конфигурации](configuration.md). Но сам `start.sh` без `--profile` определяет
модель радара, решает, собирать ли коллектор E-502, и проверяет программы по
корневому `run_config.json`, а `--producer-only` берёт именно его. Поэтому с профилем
радиофотонного радара и для `--producer-only` всегда указывайте `--profile`.
| Ключ `start.sh` | Что делает |
| --- | --- |
| `--profile ПУТЬ` | профиль для запуска; файл должен существовать. Относительный путь — от каталога проекта |
| `--headless` | без окна: окно не показывается, через 0,5 с выполняется **Apply Radar**, ещё через 1 с — **Start**; если **Start** не удался, программа завершается с кодом 1 (`Headless auto-start did not bring the pipeline up`). Ничего не устанавливает |
| `--auto-start` | с окном, но **Start** выполняется сам через 0,5 с после запуска |
| `--producer-only` | запустить только процесс сбора для профиля, без окна; вывод — в терминал |
| `--skip-build` | не собирать C++ (наличие программ всё равно проверяется) |
| `--build-only` | подготовить окружение, собрать и выйти |
| `--clean-shm` | удалить известные кольца в `/dev/shm` перед запуском (глава [Данные на диске](data.md)) |
| `-h`, `--help` | справка |
Порядок работы `start.sh`: проверка окружения → определение модели радара по профилю
→ системные библиотеки и правило udev (не для `--headless`) → Python-пакеты → сборка
C++ (для радиофотонного радара — и коллектора) → проверка программ в `build/bin/` →
остановка службы `radar.service`, если она работает → блокировка
`/tmp/radar_system.lock` → запуск программы. Если блокировку держит другой
экземпляр, `start.sh` завершается с
`Another radar instance already holds /tmp/radar_system.lock; aborting.`
Для разработки окно можно запустить и напрямую:
`.venv/bin/python -m python_app.gui.main`. Тогда нет ни сборки, ни блокировки.
### Первый запуск без железа
1. Запустите `./start.sh` (корневой профиль — симулятор).
2. Нажмите **Preprocessing…**, затем **Create Neutral S21 Sets**: предобработке
нужны наборы калибровки и референса, нейтральные данные не меняют.
3. Нажмите **Start**. Симулятор показывает точечный отражатель, который движется
по сцене; режим обработки меняется списком **Mode**.
Режим 3D SAR без видеокарты NVIDIA показывает только предупреждение.
### Первый запуск с железом
1. Скопируйте нужный профиль из `run_config_examples/` и проверьте в нём серийные
номера, порты, линии GPIO и геометрию антенн (глава
[Файл конфигурации](configuration.md)).
2. Запустите `./start.sh --profile <профиль>`. При первом запуске он может спросить
пароль `sudo`, чтобы поставить системные библиотеки и правило udev для LibreVNA.
3. Нажмите **Apply Radar**, снимите калибровку и референс (глава
[Калибровка и предобработка](preprocessing.md)) и нажмите **Start**.
4. Нажмите **Save Config…**: выбор наборов и правки в окне сохраняются только так.
Порядок работы по шагам — в главе [Типовые сценарии](workflow.md).
## Видеокарта для 3D SAR
Ядра 3D SAR компилируются при запуске библиотекой NVRTC под установленную карту.
`make` ищет `nvrtc.h`, `cuda.h` и `libnvrtc` в пакетах `nvidia-cuda-*` из `.venv` и в
`/usr/local/cuda`, а `libcuda.so.1` (драйвер) — в системных каталогах. Если чего-то
нет, `make` пишет `note: CUDA not found, selecting sar3d will show a warning` и
собирает процессор без 3D SAR. После установки CUDA снова запустите `make`: он
заметит изменение и пересоберёт нужную часть.
- На x86_64 CUDA 13 ставится из `requirements.txt`. Целевая карта — RTX 5070.
- NVRTC 13 не компилирует под карты старее поколения sm_75 (например, GTX 1060,
sm_61). Для них поставьте NVRTC 12 отдельно от `.venv` и соберите в отдельный
каталог:
```sh
pip install --target ~/.cache/radar_nvrtc12 nvidia-cuda-nvrtc-cu12 nvidia-cuda-runtime-cu12
make BUILD_DIR=/tmp/build-nvrtc12 CUDA_ROOTS="$HOME/.cache/radar_nvrtc12/nvidia/cuda_nvrtc $HOME/.cache/radar_nvrtc12/nvidia/cuda_runtime"
```
Программа запускает процессор только из `build/bin/`, поэтому такая сборка нужна
для проверки на старой карте (глава [Разработка](development.md)).
- На Jetson CUDA берётся из JetPack (`/usr/local/cuda`); pip-пакеты CUDA там не
ставятся. PyQt6 на Jetson с glibc 2.35 — версии 6.7.
## Служба без экрана
На машине без оператора программу запускает служба systemd. Установка — один раз,
от обычного пользователя через `sudo`:
```sh
sudo bash deploy/install-daemon.sh
```
Скрипт создаёт `/etc/systemd/system/radar.service`, разрешает пользователю без пароля
`sudo systemctl stop|start|restart radar.service` (`/etc/sudoers.d/radar`) и включает
службу при загрузке, но не запускает её. Служба работает от имени этого пользователя
в каталоге проекта, запускает `start.sh --headless --skip-build`, перезапускается через
3 с после сбоя, а при остановке ждёт программу до 25 с.
```sh
sudo systemctl start radar.service # запустить
systemctl status radar.service # состояние
journalctl -u radar.service -f # журнал
sudo systemctl disable --now radar.service && sudo rm /etc/systemd/system/radar.service /etc/sudoers.d/radar && sudo systemctl daemon-reload # удалить
```
Служба берёт последний использованный профиль. Перед её запуском откройте окно
программы на этой машине, загрузите профиль с железом, выберите наборы и нажмите
**Save Config…**. Работа со службой — в разделе «Работа без экрана» главы
[Веб-интерфейс, кнопка и локатор](web.md).
## Машина развёртывания
Raspberry Pi 5 Model B (Cortex-A76 ×4, 4 ГиБ), Debian 13, Python 3.13. Проект лежит в
`~/Documents/radar_system`. На машине собираются `.venv` и C++; копировать их с
рабочей станции нельзя: там другая архитектура (x86_64).
```sh
python3 -m venv .venv && .venv/bin/python -m pip install -r requirements.txt
make -j4
QT_QPA_PLATFORM=offscreen .venv/bin/python -m unittest discover -s python_app/tests
```
На Pi нужна только часть проекта: `data_acq_and_processing`, `deploy`, `python_app`,
`run_config_examples`, `Makefile`, `requirements.txt`, `run_config.json`, `start.sh`.
Каталог `device_firmware/` и документация не нужны.
> **Внимание.** Номер GPIO-чипа 40-контактного разъёма на Pi 5 зависит от версии
> ядра. После установки и обновления ядра проверьте его: `gpiodetect` и
> `gpioinfo -c gpiochip0`. Неверный чип в профиле — ключи, которые молча не
> переключаются.
## Обновление с флешки
На рабочей станции:
```sh
deploy/pack-usb.sh /media/<пользователь>/<флешка>
```
Без аргумента скрипт берёт единственную подключённую съёмную флешку. В каталог
`radar_update/` на ней он кладёт архив `radar_system-<дата коммита>-<хеш>.tar.gz`
с частью проекта для Pi из **последнего коммита** (незакоммиченные изменения в него не
попадают) и скрипт `update.sh`.
На машине, от обычного пользователя (не через `sudo`), из каталога, где лежит
`radar_update`:
```sh
bash radar_update/update.sh # перенести текущий .venv
bash radar_update/update.sh --python 3.13 # собрать .venv заново этим Python
```
Скрипту можно передать путь к архиву: `bash radar_update/update.sh <архив>`; без него
берётся последний по имени `radar_system-*.tar.gz` рядом со скриптом.
Перед обновлением закройте окно программы, иначе скрипт откажется и покажет, какие
процессы работают в каталоге. Службу он остановит сам и запустит снова, если она
работала.
Что делает `update.sh`:
1. проверяет архив и переименовывает текущую версию в `radar_system<N>` — следующий
свободный номер; это копия для отката;
2. разворачивает новую версию в `~/Documents/radar_system` (другой каталог задаёт
переменная `RADAR_DIR`);
3. переносит из прежней версии: `run_config.json`; профили из
`run_config_examples/`, изменённые на машине (версия из архива ложится рядом с
окончанием `.new`); `python_app/runtime/` без журналов; данные
`python_app/data*` и чужие каталоги верхнего уровня — жёсткими ссылками, без
копирования и места на диске; `.venv` — копией;
4. ставит пакеты из `requirements.txt` и собирает C++ (`make all rph_radar_collector`);
5. записывает имя архива в файл `RELEASE` и запускает службу, если она работала.
Если что-то пошло не так, скрипт печатает команду отката. Вручную:
```sh
sudo systemctl stop radar.service
mv ~/Documents/radar_system ~/Documents/radar_system_failed
mv ~/Documents/radar_system<N> ~/Documents/radar_system
sudo systemctl start radar.service
```
Новую версию при откате лучше переименовать, а не удалять: в ней могут быть
измерения, снятые после обновления.
На свежую машину (без прежней версии) `update.sh` спрашивает
`Install a fresh one there? [y/N]` и ставит корневой `run_config.json` из архива, то есть
симулятор: профиль с железом нужно выбрать после установки.
+300
View File
@@ -0,0 +1,300 @@
# Калибровка и предобработка
Препроцессор (`data_preprocessor`) по очереди применяет к каждой коллекции четыре
шага и только потом отдаёт её на обработку. Зачем нужен каждый шаг, объяснено в
главе [Как работает радар](principles.md).
## Цепочка предобработки
Для каждой пары и каждой частотной точки:
| Шаг | Формула | Набор |
| --- | --- | --- |
| 1. Калибровка | S21 ← S21 / S21_through | набор калибровки (through) |
| 2. Вычитание референса | S21 ← S21 − S21_ref / S21_through | набор референса |
| 3. Режекторный фильтр | S21 ← S21 · m(f) | настройки `preprocess.notch` в профиле |
| 4. Калибровка по объекту | S21 ← S21 · F | набор калибровки по объекту, если выбран |
Референс делится на through один раз при загрузке, поэтому вычитается уже
откалиброванный фон. Точки, где |S21_through|² не больше 10⁻¹², на шаге 1 остаются как
есть, без деления. Окно съёма и номер коллекции не меняются.
У каждого набора должна быть каждая пара сбора, а сетка частот — совпадать с
развёрткой: то же число точек, а каждая частота отличается от своей не больше чем на
10⁻⁵ её значения, но допуск не меньше 1 мГц (на 6 ГГц — 60 кГц). Наборы проверяются при запуске
препроцессора, и неподходящий набор его останавливает. Если это случилось, пока
программа ждёт готовности процессов, **Start** отказывает с сообщением
``Failed to start pipeline: RuntimeError: Process `data_preprocessor` exited with code 1.``
и текстом ошибки. Если позже, в журнале окна появляется отчёт о завершении
процесса, а программа перезапускает конвейер с нарастающей паузой.
Коллекцию, которая не прошла проверку уже во время
работы, препроцессор отбрасывает и пишет об этом в свой журнал
`python_app/runtime/logs/data_preprocessor.err.log`. В окне программы это видно
только по тому, что изображение перестаёт обновляться.
## Наборы
| Набор | Каталог | Обязателен |
| --- | --- | --- |
| S21 Calibration — калибровка (through) | `python_app/data/s21_calibration/` | да |
| S21 Reference — референс | `python_app/data/s21_reference/` | да |
| Object Calibration — калибровка по объекту | `python_app/data/s21_object_calibration/` | нет |
Набор — архив NPZ с массивами частот и S21 по парам и JSON-файл рядом с описанием
(формат — в главе [Данные на диске](data.md)). Наборы лежат в подкаталогах с именем
`radar_key` — ключа конфигурации радара:
- у LibreVNA ключ складывается из модели, серийных номеров, начала и конца
развёртки, числа точек, полосы ПЧ и мощности, а у многоприборной схемы ещё из
положений ключей;
- у радиофотонного радара ключ — хеш выходной полосы, фазовой калибровки, параметров
источника, положений ключей и настроек лазера; лазер входит в ключ, если плата
лазеров включена в профиле или источник — плата RFG.
Программа показывает только наборы текущего ключа. Если изменить любую из этих
величин (например, число точек), старые наборы пропадают из списков, а выбор
сбрасывается с записью `Preprocess selection reset: <старый ключ> -> <новый ключ>; cleared {...}`
в журнале. Так программа не даёт применить калибровку, снятую при других
настройках. Если вернуть прежние настройки, наборы снова появятся в списках, но
выбрать их придётся заново.
Наборы применяются при запуске сбора: программа пересобирает из выбранных наборов
файлы `python_app/runtime/*_bundle.bin` и заново запускает препроцессор. Запуск — это
**Start**, а также продолжение сбора после приостановки: после съёма набора,
временного референса или нейтральных наборов сбор, если он шёл, продолжается уже с
новыми наборами. Смена выбора в списке во время сбора применится при следующем
запуске.
Перед запуском программа проверяет наборы и, если что-то не так, отказывается
запускаться, ничего не остановив. Окно ошибки показывает
`Failed to start pipeline: <тип ошибки>: <текст>`, где текст — один из этих:
| Условие | Сообщение |
| --- | --- |
| не выбраны калибровка или референс | `Select all required preprocess sets in Preprocessing Panel before Start: S21 Calibration, S21 Reference` |
| файла набора нет для текущего ключа | `Missing set files for <вид>/<radar_key>/<имя>` |
| в наборе нет какой-то пары сбора | `Selected <набор> set does not cover requested run combos` |
| архив повреждён или содержит не числа | `Corrupted preprocess set <вид>/<ключ>/<имя>: …` |
| калибровка по объекту не подходит | см. раздел «Калибровка по объекту» |
## Окно Preprocessing Setup
Открывается кнопкой **Preprocessing…**. Окно не модальное: пока оно открыто, главным
окном можно пользоваться.
Группа **Preprocess Sets**:
| Элемент | Что делает |
| --- | --- |
| **Set name** | имя нового набора; по умолчанию `set_001`. Для этого вида набора и этого ключа радара имя должно быть новым |
| **Create Neutral S21 Sets** | создаёт нейтральные наборы (см. ниже). Кнопка есть только у радиофотонного радара, у многоприборной схемы и у одиночного LibreVNA в режиме `mock` |
| **Refresh Sets** | перечитывает списки наборов с диска и каталог вариантов радара |
| **Sweeps per combo (median)** | сколько развёрток снимать на каждую пару, от 1 до 99, по умолчанию 5. В набор идёт медиана по каждой частоте, отдельно по вещественной и мнимой части: при трёх развёртках и больше одиночный сбой развёртки в набор не попадает. Это же число развёрток берут временный референс и калибровка по объекту |
| **Radar Config Variants** → **Directory**, **Browse…** | каталог с профилями JSON для съёма наборов сразу под несколько настроек развёртки (см. ниже). Под полем — итог просмотра: `Directory: <каталог> \| json=…, valid=…, skipped=…, duplicates=…` (файлов JSON, подходящих вариантов, пропущенных, повторов) или `No radar config directory selected.` |
| **Capture current combo for all found radar configs** | включает съём по всем вариантам из каталога |
| **S21 calibration**, **S21 reference** | выбор набора калибровки и референса для следующего **Start** |
| **Object calibration** | выбор калибровки по объекту; **Off** — не применять |
| **Create…** | открывает окно калибровки по объекту |
Группа **Capture All Antenna Pairs**:
| Элемент | Что делает |
| --- | --- |
| **Active type**, **Progress**, **Current combo** | вид снимаемого набора, сколько пар снято из скольких, какая пара следующая; без съёма — `<none>`, `0 / 0`, после последней пары — `<complete>` |
| **Start S21 Calibration**, **Start S21 Reference** | начинают съём набора |
| **Capture Current Combo** | снимает следующую пару |
| **Capture All Remaining** | снимает подряд все оставшиеся пары |
| **Undo Last Capture** | отменяет последний съём, чтобы переснять пару |
| **Save Captured Set** | сохраняет набор и выбирает его; доступна, когда сняты все пары |
| **Abort Sequence** | прерывает съём и отбрасывает снятое |
Ниже — журнал съёма (`S21 Calibration: 1/8 | input=0 output=0` на каждую пару),
строка состояния (сначала `Ready`) и графики модуля и фазы последней снятой развёртки
(до первого съёма — `Preview will appear after the first successful capture.`).
Последняя развёртка рисуется и в главном окне.
## Съём калибровки и референса
1. Выберите профиль и настройки радара. Наборы привязаны к ключу радара, поэтому
снимайте их при тех настройках развёртки, с которыми будете работать.
2. Подготовьте установку. Для калибровки — эталонный тракт для текущей пары; для
референса — пустую сцену: всё, что должно остаться в фоне, на месте, целей нет.
3. Нажмите **Preprocessing…**, задайте **Set name** и
**Sweeps per combo (median)**.
4. Нажмите **Start S21 Calibration** или **Start S21 Reference**. Сбор
приостанавливается, и на время съёма прибором управляет окно предобработки.
5. Снимайте пары кнопкой **Capture Current Combo** или все сразу кнопкой
**Capture All Remaining**. Проверяйте каждую развёртку по графикам; неудачную
переснимите через **Undo Last Capture**. При ошибке съёма уже снятые пары
сохраняются, и съём можно повторить.
6. Когда сняты все пары, нажмите **Save Captured Set**. Набор сохраняется и
выбирается, приостановленный сбор продолжается.
Съём всегда идёт по полной матрице положений ключей, а не только по парам сбора.
- Одиночный LibreVNA: для каждой пары программа выставляет ключи, ждёт время
установления (`run.settling_ms`, в окне — **Settling (ms)**) и снимает нужное
число развёрток этой пары.
- Многоприборная схема при калибровке: прибор снимает всю матрицу, а в набор идёт
только текущая пара, по одной за нажатие. **Capture All Remaining** здесь
недоступна.
- Многоприборная схема при съёме референса: одно нажатие снимает всю матрицу нужное
число раз, и набор сразу готов.
### Варианты радара
Иногда наборы нужны сразу под несколько развёрток — например, под разные полосы
частот. Для этого в **Directory** указывают каталог с профилями JSON. Профиль
считается вариантом, если в его `radar.sweep` есть хотя бы одно из полей
`start_hz`, `stop_hz`, `points`, `if_bandwidth_hz`, `stimulus_power_dbm`; эти поля
накладываются на текущий профиль, повторы одного ключа пропускаются. С включённой
**Capture current combo for all found radar configs** каждое нажатие снимает пару во
всех вариантах подряд, с перенастройкой прибора. **Save Captured Set** сохраняет
по набору на каждый вариант под одним именем, а картинки развёрток — в
`python_app/data/preview_png/<вид>/<имя>/<radar_key>/i<вход>_o<выход>.png`.
## Временный референс
Кнопка **Capture Temporary Reference** в секции **Data Actions** снимает референс
одним нажатием, не открывая окно предобработки. То же действие есть в
веб-интерфейсе и на GPIO-кнопке.
Программа приостанавливает сбор, снимает полную матрицу с текущими настройками
(медиана **Sweeps per combo (median)** развёрток) и сохраняет её как набор
`tmp_reference`, заменяя прежний. Затем она выбирает этот набор, очищает историю
обработки, рисует последнюю развёртку с подписью `Tmp S21 Reference last trace` и
продолжает сбор. Процессор и локатор при этом не перезапускаются, клиенты локатора
не отключаются.
Временный референс нужен, когда сцена или положение радара изменились: после
перестановки, на новом месте, после заметного изменения обстановки.
## Нейтральные наборы
**Create Neutral S21 Sets** сохраняет под именем из **Set name** два набора:
калибровку, равную 1, и референс, равный 0, для всех пар на текущей сетке частот.
С ними данные проходят предобработку без изменений. Они нужны, чтобы запустить
конвейер, пока настоящей калибровки нет: с симулятором, при первой проверке прибора.
Если набор с таким именем уже есть, программа откажется. После создания наборы
выбираются, история очищается, а процессор перезапускается: сразу, если сбор шёл,
иначе при следующем **Start**.
## Режекторный фильтр
Фильтр настраивается только в профиле, в разделе `preprocess.notch`; в окне
программы его нет.
| Ключ | По умолчанию | Что делает |
| --- | --- | --- |
| `enabled` | `false` | включает фильтр |
| `bands_hz` | `[]` | полосы подавления: список пар `[нижняя, верхняя]` в герцах, верхняя не меньше нижней |
| `taper_width_hz` | `40e6` | ширина плавного края w, Гц, не меньше 0 |
| `taper_type` | `"cosine"` | форма края: `"cosine"` — косинусный спад, `"hard"` — без спада |
Внутри полосы [L, H] множитель m(f) равен нулю. При `"cosine"` по краям полосы он
плавно спадает от 1 до 0 по закону 0,5·(1 + cos(…)) на участках [L − w, L] и
[H, H + w]. При `"hard"` (или w = 0) зануляется вся полоса [L − w, H + w]. Края
соседних полос перемножаются.
Во всех профилях-примерах `enabled: true` и `bands_hz: []`, то есть фильтр ничего не
подавляет. Изменение фильтра применяется при следующем **Start** и сбрасывает
историю.
## Калибровка по объекту
Калибровка по объекту находит для каждой пары комплексную поправку по частоте так,
чтобы отклик известного отражателя у всех пар совпадал по фазе. Эталон — ноутбук
`MIMO_calibration.ipynb`, он хранится вне репозитория. Поправку можно строить по
нескольким положениям отражателя: они уточняют друг друга.
Что нужно заранее:
- кольцо `rings.preprocessed_tap` в профиле: калибровка снимает данные из него;
- таблицы геометрии антенн: в калибровку входят только пары, у которых есть
геометрия обеих антенн, остальные получают поправку 1;
- выбранный набор калибровки (through): поправка запоминает его имя и содержимое и
годится только вместе с ним;
- металлический шар известного радиуса (или другой точечный отражатель) и способ
измерить положение его центра.
Окно **MIMO Object Calibration** открывается кнопкой **Create…** в окне
предобработки. Вверху — подсказка о порядке работы
(`Capture data, then enter the measured reflector centre: …`), внизу — строка
состояния (сначала `Ready to capture`), а на месте карт фаз до расчёта —
`Pair phases at the target will appear after calculation.`
| Элемент | Что делает |
| --- | --- |
| **Set name** | имя набора поправки |
| таблица **Target**, **Sweeps**, **X (m)**, **Y (m)**, **Z (m)** | снятые положения отражателя: сколько коллекций снято и измеренный центр |
| **Capture Data** | снимает данные для нового положения отражателя |
| **Remove Selected Target** | удаляет выбранное положение |
| **Sphere radius (m, 0 = point)** | радиус шара, по умолчанию 0,16 м; 0 — точечный отражатель |
| **Antenna origin height above ground (m)** | высота начала осей антенн над землёй, по умолчанию 0,92 м |
| **Radar downward tilt (°)** | наклон радара вниз, по умолчанию 10° |
| **Calculation Settings** | раскрывает остальные параметры расчёта (таблица ниже) |
| **Calculate and Save** | считает поправку в фоне и сохраняет набор |
| **Cancel Operation** | прерывает съём или расчёт |
| **Close** | закрывает окно |
Параметры в **Calculation Settings**:
| Параметр | По умолчанию | Что делает |
| --- | --- | --- |
| **Estimation band start (MHz)**, **Estimation band stop (MHz)** | 2000 и 6000 | полоса, по которой оценивается поправка; в ней должно быть не меньше 4 точек |
| **Echo path gate half-width (m)** | 0,15 | полуширина окна вокруг ожидаемого пути до отражателя, в котором ищется его эхо |
| **Gate taper width (m)** | 0,03 | ширина плавного края этого окна |
| **Coarse path search (±m, 0 = off)** | 0,6 | предварительный поиск: на сколько можно сдвинуть отражатель по дальности до самого сильного эха |
| **Coordinate refinement (±m)** | 0,1 | в каких пределах уточнять координаты отражателя |
| **Refine coordinates** X, Y, Z | все три | по каким осям уточнять координаты |
| **Antenna phase model** | `linear` | модель фазы антенны по частоте: `const` — постоянная, `linear` — линейная (задержка), `poly3` — многочлен третьей степени, `raw` — без сглаживания |
| **Equalize amplitudes** | выключен | выравнивать и амплитуды, а не только фазы |
Порядок:
1. Поставьте отражатель в сцену и измерьте положение его центра в осях сцены: X —
вдоль ряда антенн, Y — высота над землёй, Z — вдоль земли от радара.
2. Нажмите **Capture Data**. Программа приостанавливает текущий сбор и запускает
конвейер с этим же профилем, но без калибровки по объекту и по всем парам из
таблиц геометрии (многоприборная схема — по полной матрице). Она собирает
столько новых коллекций, сколько задано в
**Sweeps per combo (median)**, и возвращает прежний режим работы. Предел ожидания —
300 с.
3. Введите X, Y, Z в строке нового положения.
4. Повторите для других положений, если нужно. Все положения должны быть сняты при
одних и тех же настройках радара, геометрии и предобработке.
5. Нажмите **Calculate and Save**. Расчёт идёт в фоне. После него окно показывает
карты фаз пар в точке отражателя до и после поправки (**Before**, **After**):
строки — передающие антенны, столбцы — приёмные, фаза — относительно суммы по
всем парам, по шкале ±180°, с разбросом в градусах.
**Capture Data** отказывает, если идёт однократный съём или запись на диск
(`Finish the single capture or disk recording before taking calibration data`), если
нет таблиц геометрии (`Configure Tx/Rx antenna positions before taking calibration data`)
или кольца копии
(`Object calibration requires rings.preprocessed_tap; enable it in the run profile`).
6. Если набор подходит к текущим настройкам, он выбирается и применяется при
следующем **Start** (`Applies on the next Start.`). Иначе он сохраняется
невыбранным с объяснением `Not selected for the current configuration: <причина>`.
Коллекции одного положения усредняются. По каждой частоте поправка раскладывается на
множители передающих и приёмных антенн; фазы сглаживаются выбранной моделью и
продолжаются на всю сетку частот. Поправка пары — сопряжённая фаза произведения
множителей, а с **Equalize amplitudes** — обратное произведение.
Перед каждым **Start** программа проверяет, что поправка снята при тех же настройках
радара, той же геометрии и с тем же набором калибровки (по имени и содержимому), что
она и калибровка покрывают все пары сбора, не содержит нулей и имеет ту же сетку
частот. Сообщения при отказе:
- `Object calibration has no capture provenance; capture it again`;
- `Object calibration radar configuration differs from the captured state` (а также
`antenna geometry`, `through calibration name`, `through calibration content`);
- `Object calibration and through must cover input=i, output=o`;
- `Object calibration corrections must be nonzero`;
- `Object calibration frequency grid differs for input=i, output=o`.
Если изменить таблицу геометрии так, что она перестала совпадать с геометрией
выбранной поправки, выбор поправки снимается, а работающий с ней сбор
приостанавливается и продолжается уже без неё.
+240
View File
@@ -0,0 +1,240 @@
# Как работает радар
Радар закреплён над землёй с наклоном вниз и смотрит вперёд, на участок земли в
нескольких метрах от себя. Он излучает радиоволны, принимает отражённые и по ним
строит изображение этого участка: где лежат или стоят предметы, отражающие волну.
Незнакомые термины — в [словаре](glossary.md).
## Отражение и задержка
Передающая антенна излучает синусоидальную волну частоты f. Волна идёт в воздухе со
скоростью c ≈ 3·10⁸ м/с, отражается от предметов и частично доходит до приёмной
антенны. Длина волны λ = c/f; на частоте 3 ГГц это 10 см. Сильнее всего отражают
металлические предметы; вообще отражает всё, чьи электрические свойства отличаются
от воздуха.
Передающая и приёмная антенны стоят в разных местах, поэтому путь волны складывается
из двух отрезков: от передающей антенны до предмета (R_tx) и от предмета до приёмной
(R_rx). Принятая волна запаздывает на
τ = (R_tx + R_rx) / c.
Предмет в 4 м от антенн даёт путь около 8 м и задержку около 27 нс.
Для синусоиды задержка проявляется как сдвиг фазы: принятый сигнал
A·cos(2πf(t − τ)) отстаёт от излучённого на φ = 2πfτ. Фаза известна только с
точностью до целого числа периодов, поэтому по одной частоте расстояние не
определить. Его определяют по тому, как фаза меняется с частотой.
## Векторный анализатор цепей и S21
Векторный анализатор цепей (ВАЦ, англ. VNA) выдаёт на один свой порт синусоиду
заданной частоты и измеряет амплитуду и фазу сигнала, пришедшего на другой порт,
относительно выданного. Результат измерения — комплексный коэффициент передачи S21
из порта 1 в порт 2: отношение волны, пришедшей на порт 2, к волне, поданной в порт 1.
По смыслу он похож на отношение комплексных амплитуд выходного и входного напряжений
в расчёте цепей переменного тока и совпадает с ним, когда оба порта согласованы:
модуль |S21| показывает ослабление, аргумент — сдвиг фазы.
В радаре к излучающему порту прибора подключена передающая антенна, к принимающему —
приёмная. Цепью между портами служит сцена, и S21 показывает, что сцена передаёт от
одной антенны к другой на данной частоте.
В системе работает LibreVNA — двухпортовый ВАЦ с открытой прошивкой, частотами до
6 ГГц и подключением по USB. Его основные величины:
| Величина | Что это | У LibreVNA |
| --- | --- | --- |
| Модуль S21 в дБ | 20·lg\|S21\|: −20 дБ — амплитуда в 10 раз меньше, −60 дБ — в 1000 раз | — |
| Мощность излучения, дБм | децибелы относительно 1 мВт: 10·lg(P / 1 мВт); 0 дБм = 1 мВт, −3 дБм ≈ 0,5 мВт, −40 дБм = 0,1 мкВт | от −40 до 0 дБм |
| Полоса ПЧ (IF bandwidth, IFBW) | полоса фильтра приёмника; на каждой частоте прибор накапливает сигнал около 1/IFBW (при 50 кГц — 20 мкс). Узкая полоса снижает шум, но замедляет измерение | от 6 Гц до 50 кГц |
| Число частотных точек | сколько частот в одной развёртке | до 4501 |
## Развёртка по частоте и дальность
Развёртка (sweep) — проход прибора по списку частот: S21 измеряется в N частотных
точках, равномерно расставленных от f_start до f_stop с шагом
Δf = (f_stop − f_start) / (N − 1).
Если в сцене один отражатель с задержкой τ, то S21(f) = a·e^(−j2πfτ): модуль
постоянен, а фаза линейно убывает с частотой, тем быстрее, чем дальше отражатель.
Несколько отражателей дают сумму таких слагаемых.
Обратное преобразование Фурье по частоте переводит набор значений S21(f) в функцию
времени с пиками в моменты τ каждого отражателя — так же, как частотная
характеристика цепи переводится в импульсную. Время пересчитывается в путь L = c·t.
Получается А-скан (профиль дальности): амплитуда отражения в зависимости от
расстояния. Радары, которые так измеряют дальность, называют радарами со ступенчатым
изменением частоты. Дальность здесь — половина пути, L/2: при близко стоящих антеннах
это и есть расстояние до отражателя.
Что способен показать А-скан, определяют две величины:
| Величина | Формула | Пример: развёртка 0,5–6 ГГц, 1031 точка, обработка в полосе 1,5–6 ГГц |
| --- | --- | --- |
| Разрешение по дальности — на каком расстоянии друг от друга два отражателя ещё видны раздельно | ΔR ≈ c / (2B), B — ширина полосы, по которой строится А-скан | B = 4,5 ГГц, ΔR ≈ 3,3 см; с окном Ханна (ниже) пик вдвое шире |
| Предельная дальность — дальше неё картина повторяется | R_max = c / (2Δf) | Δf = 5,34 МГц, R_max ≈ 28 м |
Отражение от предмета дальше R_max не исчезает: оно появляется на неверной, меньшей
дальности. Поэтому число точек выбирают так, чтобы R_max с запасом превышала самую
дальнюю интересующую дальность. Развёртка 1 МГц–6 ГГц из 201 точки даёт Δf ≈ 30 МГц и
R_max ≈ 5 м.
У каждого пика А-скана есть боковые лепестки — ложные слабые пики по сторонам.
При резких краях полосы первый лепесток всего на 13 дБ ниже главного пика, и рядом с
сильным отражателем видны призраки. Окно Ханна плавно уменьшает вклад крайних частот:
лепестки опускаются примерно до −31 дБ, а главный пик становится примерно вдвое шире.
Окно Ханна применяют режимы GPR и 3D SAR и калибровка по объекту; B-scan строится
без окна.
## Антенны, пары и матрица
Каждое сочетание передающей и приёмной антенны — пара (в программе combo) — даёт
свою развёртку S21. M передающих и N приёмных антенн дают M × N пар. В программе
передающая сторона пары называется выходом (output, с него прибор излучает),
приёмная — входом (input). Пара записывается как `вход:выход` или `in0/out1`.
Положение каждой антенны задаётся в таблицах геометрии: номер выхода или входа и
координаты X, Y, Z в метрах.
Пары смотрят на сцену из разных точек. Сложенные с правильными задержками, они дают
изображение, чёткость которого поперёк сцены определяется размером всей группы
антенн, а не одной антенны.
Пар больше, чем портов у прибора. Их набирают двумя способами.
ВЧ-ключ (коммутатор H7992) подключает порт прибора к одной из антенн, до четырёх.
Положение ключа задают два логических сигнала с GPIO компьютера. Программа выставляет
ключи для очередной пары, ждёт, пока сигнал установится, и снимает развёртку.
Многоприборная схема — три LibreVNA, работающие синхронно. Ведущий прибор (мастер)
на каждой частоте излучает по очереди с двух портов, два ведомых принимают каждый на
два порта, и за
одну развёртку снимаются 2 × 4 = 8 пар. В режиме переключения стимула внутри
частотной точки на каждом порту мастера стоит ключ, которым управляет контроллер
STM32: на каждой частоте каждый порт излучает дважды, через оба положения ключа, и
развёртка даёт 4 × 4 = 16 пар. Все 16 измеряются на одной частоте за доли
миллисекунды, поэтому движение радара почти не сдвигает их друг относительно друга.
Обход всех пар называется коллекцией (collection). Программа обрабатывает данные
коллекциями. У коллекции и у каждой её развёртки есть окно съёма — время начала и
конца измерения по часам компьютера; по нему программа учитывает движение радара.
## Калибровка и вычитание фона
Измеренный S21 описывает не только сцену. Кабели, ключи, тракты антенн и сам прибор
по-своему меняют амплитуду и фазу на каждой частоте, а антенны «видят» друг друга
напрямую. Эти составляющие программа убирает наборами, снятыми заранее (подробно — в
главе [Калибровка и предобработка](preprocessing.md)):
1. Набор калибровки (through) — эталонное измерение тракта. Каждая развёртка
делится на него, и частотная характеристика аппаратуры сокращается.
2. Набор референса — измерение пустой сцены. В нём всё, что не меняется: прямая
связь между антеннами, земля, неподвижные предметы. Референс вычитается, и
остаётся то, что появилось в сцене после его съёма. Временный референс снимается
одной кнопкой прямо во время работы, например после перестановки радара.
3. Режекторный фильтр (notch) обнуляет заданные полосы частот, например полосу
постоянной помехи; края полос сглаживаются.
4. Калибровка по объекту — поправка для каждой пары, найденная по известному
отражателю (металлическому шару) в измеренной точке. Она выравнивает пары по фазе,
чтобы в этой точке они складывались синфазно.
## Изображения
### B-scan
B-scan — А-сканы одной пары за последовательные развёртки, поставленные рядом:
по горизонтали номер развёртки, по вертикали дальность, цветом — амплитуда.
Неподвижный отражатель рисует горизонтальную линию, приближающийся — наклонную.
### Когерентная обратная проекция
Изображение по всем парам строится обратной проекцией. Для каждой точки P
изображения программа вычисляет для каждой пары задержку
τ = (|P − Tx| + |P − Rx|) / c, берёт значение А-скана этой пары в момент τ и
складывает комплексные значения всех пар. Если в P есть отражатель, слагаемые
совпадают по фазе и сумма велика. В остальных точках фазы разные, и слагаемые
взаимно гасятся. Яркость точки — модуль суммы. Сложение с учётом фазы называется
когерентным.
Режим GPR считает такую сумму в точках плоскости земли перед радаром (сетка 300 × 300:
поперёк и вдоль дальности), находит на карте яркие пятна — объекты — и отдаёт их
внешним программам через TCP-локатор.
### 3D SAR
Режим 3D SAR считает ту же сумму в каждой ячейке (вокселе) трёхмерной сетки над
землёй. К текущей коллекции можно добавить несколько предыдущих: если радар едет,
каждая снята из другой точки, и вместе они образуют синтезированную апертуру
(synthetic aperture radar, SAR) — как будто антенн больше и они расставлены шире.
Объём показывается проекциями максимума: спереди, сбоку, сверху и в наклонном виде.
### Что программа делает перед суммированием
Развёртка длится заметное время, а радар может ехать. Программа знает время каждой
частотной точки и скорость радара и поправляет фазу так, будто вся коллекция снята в
один момент. Скорость задают в настройках или передаёт внешний клиент локатора.
Пары различаются усилением: у них разные кабели, антенны и углы обзора. Программа
выравнивает уровни пар по дальности и нормирует каждую пару на медиану её А-скана.
Фаза при этом не меняется.
### Фон кадра и CLEAN
В 3D SAR программа сравнивает максимум кадра с фоном — медианой значений рабочей
части объёма, типичным уровнем помех. Если объектов CLEAN нет, кадр появляется на
экране, когда максимум превышает фон в 6 раз, и показывается в полную яркость начиная
с 8,5 раза: кадр без явной цели не выглядит целью.
Сильный отражатель окружён боковыми лепестками, которые закрывают слабые. Алгоритм
CLEAN находит самую сильную точку, вычитает её отклик из данных и повторяет, пока в
изображении есть точки в 4,5–5,5 раза выше фона, но не больше 15 раз. Затем каждый
найденный объект рисуется в своей шкале, и слабый объект виден рядом с сильным;
найденные объекты видны всегда, а порог фона прячет только остаток. Рамки вокруг групп
объектов показывают, где
объект по высоте: очень низко, на земле, над землёй или у края сцены.
## Радиофотонный радар
Второй тип радара в системе формирует СВЧ-сигнал с помощью лазеров (радиофотонный
радар, RPH). Плата лазеров меняет ток лазера LD1 ступенями — по умолчанию от 97,3 до
107 мА с шагом 0,05 мА, 50 мкс на ступень, — и вместе с током меняется частота
сигнала. Принимаются два сигнала: основной, прошедший через сцену, и опорный,
прошедший постоянную задержку. Их оцифровывает АЦП L-Card E-502 или плата RFG.
Фаза опорного сигнала растёт с частотой линейно, поэтому по ней программа узнаёт
частоту каждой ступени: две опорные точки «фаза — частота» задаются фазовой
калибровкой. Затем развёртка пересчитывается на равномерную сетку частот (по
умолчанию 2,1–5,5 ГГц, 2048 точек), и дальше данные обрабатываются так же, как
развёртки ВАЦ.
## Координаты
Положения антенн задаются в осях радара: X — вдоль ряда антенн (поперёк направления
обзора), Y — вверх
относительно радара, Z — вдоль оси, куда радар смотрит. Начало осей находится на
высоте h над землёй, и радар наклонён вниз на угол обзора p.
Изображения 3D SAR, рамки объектов и положение отражателя при калибровке по объекту
задаются в осях сцены: X — та же ось вдоль ряда антенн, Y — высота над землёй
(Y = 0 — земля), Z — вдоль
земли от радара.
```text
Y
│
h ●- - - - - - - - - - - - горизонталь
│ \ p — угол обзора
│ \
│ \ ось обзора радара (Z радара)
│ \
──┴─────────\────────────────────► Z сцены, вдоль земли
земля, Y = 0 ось обзора пересекает землю при Z = h / tg p
```
Настройки «глубины» — **Min depth (m)** и **Max depth (m)** в GPR, **MIN_DEPTH** и
**MAX_DEPTH** в 3D SAR —
ограничивают расстояние вдоль оси обзора радара, а не глубину под землёй. Высота h
задаётся настройкой **Radar height (m)** в GPR и **BP_GROUND_DEPTH_M** в 3D SAR, угол
обзора — **Look angle (°)** и **LOOK_ANGLE_DEG**. Формулы перехода между осями — в
главе [Режимы обработки](processing.md).
+475
View File
@@ -0,0 +1,475 @@
# Режимы обработки
Процессор (`data_processor`) превращает предобработанные коллекции в результат
выбранного режима. Режимов четыре: pass-through, B-scan, GPR и 3D SAR; в списке
**Mode** под кнопками управления они называются `pass_through`, `bscan`, `gpr` и `sar3d`.
Режим и его настройки меняются
на ходу, сбор при этом не перезапускается. Физический смысл обработки объяснён в
главе [Как работает радар](principles.md), здесь — что делает каждый режим и каждая
его настройка.
## Когда применяются настройки
Изменённая настройка обработки сразу уходит процессору. Программа записывает живые
настройки в `python_app/runtime/processing_live.json` с новым номером ревизии.
Процессор перечитывает файл примерно раз в 100 мс и пересчитывает новейшую
сохранённую коллекцию, а в режиме B-scan — все сохранённые.
Если программа не смогла проверить или записать настройки, поля возвращаются к
последним опубликованным значениям и появляется сообщение
`Failed to update live processing settings: …` (в веб-интерфейсе —
`Failed to apply web live settings: …`). Неверную правку таблицы антенн окно
отклоняет сразу, сообщением **Invalid Antenna Geometry**. Если файл не принял сам процессор,
он продолжает работать с последними принятыми настройками и пишет предупреждение в
свой журнал `python_app/runtime/logs/data_processor.err.log`.
GPR и 3D SAR рисуют только результаты текущей ревизии. После изменения программа до
0,8 с ждёт пересчитанный результат, поэтому старая картинка на экране не мелькает.
Если процессор не работает, вид остаётся пустым до следующего результата.
Часть настроек только перерисовывает готовый результат: параметры вида pass-through,
**Subtract mean A-scan** и **Scans to show (stopped)** в B-scan, **Render mode** в GPR,
**SAR_CLOUD_THRESHOLD**, **Zoom (%)** и **SAR_MARK_ENABLE** в 3D SAR. Они сохраняются в
профиль, но процессору не передаются.
У страниц GPR и 3D SAR свои поля для одинаковых по смыслу настроек: номера антенн,
границы дальности, полоса частот, скорость, угол обзора, вычитание среднего,
выравнивание пар. Процессор получает значения со страницы выбранного режима, а в
pass-through и B-scan — со страницы GPR. Поэтому переключение между GPR и 3D SAR
меняет и эти значения: угол обзора по умолчанию — 0° в GPR и 19,5° в 3D SAR.
Границы полосы частот (**Start (MHz)**, **Stop (MHz)**, **F_START MHz**,
**F_STOP MHz**) не выходят за развёртку прибора, а у радиофотонного радара — за его
выходную полосу. Значения за пределами программа подрезает к границе; если это
случилось после изменения развёртки, в журнале появляется
`Processing frequency limits were clamped to the active radar sweep.`
## Pass-through
Показывает предобработанный S21 каждой пары по частоте: модуль в дБ (20·lg|S21|) и
фазу в градусах. Обработки нет; режим нужен, чтобы проверить сигнал, калибровку и
референс. Подпись кривой — `in<вход>/out<выход>`. У одиночного LibreVNA и
радиофотонного радара в режимах pass-through и B-scan снимается только пара `0:0`,
если оба ВЧ-ключа в `mock` или у обоих одно положение. Результаты других режимов здесь не
рисуются: после переключения на pass-through экран ждёт первого результата этого
режима.
Все параметры этого режима только перерисовывают график.
| Параметр | По умолчанию | Что делает |
| --- | --- | --- |
| **Show magnitude** | включён | показывает график модуля |
| **Show phase** | включён | показывает график фазы |
| **Unwrap phase** | выключен | убирает скачки фазы на 360° (развёртывает её); шкала фазы подбирается по данным, без развёртки она закреплена на ±180° |
| **Switch combos** | пусто — все пары | список пар `вход:выход` через запятую, например `0:0,1:0`. При ошибке в записи показываются все пары, а в журнале — `Invalid pass-through switch-combo filter.` |
| **Fix magnitude Y range** | выключен | закрепляет шкалу модуля; без неё шкала подбирается по данным |
| **Y min (dB)**, **Y max (dB)** | −100 и 0 | границы закреплённой шкалы, от −240 до 240 дБ; доступны при включённой **Fix magnitude Y range** |
## B-scan
Для каждой пары строит А-скан и укладывает А-сканы последовательных развёрток в
изображение: по горизонтали номер развёртки (**Sweep #**), по вертикали дальность
(**Range (m)**). На экране — одна пара; её выбирают в списке **B-scan pair** в
секции ключей (пары записаны как `вход : выход`), а кнопка **Next Combination**
переходит к следующей: сначала меняется выход, затем вход, после последней пары снова
первая. В списке все пары, которые есть в истории, независимо от текущего списка пар
сбора. Список и кнопка доступны только в режиме B-scan, кнопка — когда пар хотя бы
две.
Как строится А-скан:
1. Из развёртки берутся точки в полосе от **Start (MHz)** до **Stop (MHz)**. Если их
меньше двух, профиля нет, и развёртка в изображение не попадает.
2. Обратное преобразование Фурье без окна.
3. Отсчёт с временем t соответствует пути c·t (c = 299 792 458 м/с). Дальность
отсчитывается от **Cut (m)**: d = (c·t − 2·Cut) / 2, показываются отсчёты с
0 ≤ d ≤ **Max depth (m)**.
4. Значение — модуль, вещественная часть или фаза отсчёта (**Axis**), умноженное на
d в степени **Gain**.
| Параметр | По умолчанию | Пределы | Что делает |
| --- | --- | --- | --- |
| **Axis** | `abs` | `abs`, `real`, `phase` | что рисовать: модуль, вещественную часть или фазу (в радианах). Для `abs` — шкала от тёмного к светлому, для остальных — сине-бело-красная, симметричная относительно нуля |
| **Cut (m)** | 0,824 | 0–2 м | дальность, с которой начинается изображение; всё ближе отрезается, а ось дальности начинается с нуля в этой точке |
| **Max depth (m)** | 1,0 | 0,1–20 м | сколько метров дальности показывать после **Cut (m)** |
| **Gain** | 1,0 | 0–3 | усиление с дальностью: значение умножается на d^Gain, чтобы далёкие слабые отражения были видны рядом с близкими |
| **Start (MHz)**, **Stop (MHz)** | 100 и 8800, подрезаются по развёртке | в пределах развёртки | полоса частот, по которой строится А-скан |
| **Scans to show (stopped)** | 50 | 1–1000 | сколько последних развёрток показывать и пересчитывать, когда сбор остановлен |
| **Subtract mean A-scan** | выключен | — | вычитает из каждого столбца средний по показанным развёрткам А-скан: неподвижные отражения (горизонтальные полосы) пропадают, остаётся то, что меняется |
Во время сбора изображение показывает последние развёртки в пределах истории: не
больше 50 и не больше ёмкости колец `preprocessed` и `results`. Если истории меньше,
чем задано, в журнале появляется предупреждение
`B-scan is set to show W sweeps but only R are retained; the image shows what history there is.`
Если ось дальности изменилась (другие **Cut (m)**, **Max depth (m)** или полоса),
изображение начинается заново.
Когда сбор остановлен, изменение настройки B-scan пересчитывает всё изображение:
через 0,3 с после последнего изменения программа заново отправляет процессору
сохранённые развёртки (до **Scans to show (stopped)** штук) и собирает картинку
из ответов. Так одно изображение никогда не смешивает разные настройки. Пересчёт
возможен, только если конвейер в этом сеансе запускала эта же программа и процессор
ещё работает. Развёртки, которые процессор не вернул, в картинку не попадают, и в
журнале появляется `B-scan replay recovered R of S re-sent sweeps; the image leaves the others out.`
Если не вернулась ни одна, остаётся прежнее изображение из истории.
Если хотя бы один ВЧ-ключ работает с настоящим железом (`driver_mode: "native"`),
B-scan требует ровно одну пару сбора:
`B-scan with native switches requires exactly one run combo (now N)`.
## GPR
Строит изображение плоскости земли перед радаром когерентной обратной проекцией по
всем выбранным парам и находит на нём объекты. Объекты отображаются на картинке и
уходят клиентам TCP-локатора.
Порядок обработки:
1. Выбор антенн: номера из **Input positions** и **Output positions**, у которых есть
строка в таблице геометрии. Пустое поле — все антенны с геометрией.
2. Вычитание среднего предыдущих коллекций и поправка на движение (раздел «Общие
шаги GPR и 3D SAR»).
3. А-скан каждой пары в полосе **Start (MHz)**–**Stop (MHz)** с окном Ханна.
4. Выравнивание пар по дальности (**PAIR_DEPTH_EQUALIZE**) и нормировка каждой пары
на медиану её А-скана в пределах **Min depth (m)**–**Max depth (m)**; нормировка
выполняется всегда.
5. Сетка 300 × 300 точек на плоскости земли: поперёк — от крайних выбранных антенн
с запасом 2 м в обе стороны, вдоль — дальность от 0,1 м до **Max depth (m)**.
Положение плоскости земли относительно радара задают **Radar height (m)** и
**Look angle (°)**.
6. В каждой точке — сумма вкладов всех пар с весом компенсации дальности (степень
**Range compensation power**, вес не больше 5). Яркость — модуль суммы; точки ближе
**Min depth (m)** остаются нулевыми.
7. Карта сглаживается и нормируется к максимуму, затем на ней ищутся объекты.
Поиск объектов. Программа берёт самый яркий пик и выделяет вокруг него связную
область ярче 0,75 пика. Область меньше 10 см² отбрасывается. Положение объекта —
взвешенный центр яркой части. Затем окрестность объекта гасится, и поиск
повторяется. Он останавливается, когда очередной пик слабее **Object threshold fraction**
от максимума карты, и не идёт дальше 10 шагов. Для каждого объекта считается оценка:
- `peak` — яркость пика;
- `combined` — 0,45 × яркость + 0,25 × когерентность (насколько вклады пар совпадают
по фазе) + 0,20 × превышение над местным фоном + 0,10 × контраст (отношение к
фону). Каждое слагаемое приведено к диапазону 0–1.
Объект считается боковым лепестком более сильного, если он не ярче 0,85 его пика,
сдвинут поперёк не меньше чем на 0,35 м, по дальности — не больше чем на 0,70 м, а
среднеквадратичное по парам расхождение путей волны до обоих объектов не больше 0,2 м.
С включённой **Remove sidelobe objects** такие объекты удаляются из списка и
стираются с карты.
Какие объекты показываются и уходят локатору:
1. Только внутри окна **Visible X min (m)**–**Visible X max (m)** и
**Visible Z min (m)**–**Visible Z max (m)**; то же окно задаёт границы графика.
2. Отбор по числу: если **Max detected objects** (N) и **Draw top M objects** (M)
больше нуля, то при числе объектов больше N не показывается ни один (сцена считается
засорённой), иначе показываются M лучших по оценке.
3. Фильтр приближения: объект показывается, только если он виден в
**Approach min frames** коллекциях подряд и смещается так, как должен смещаться
неподвижный предмет при движении радара к нему: между соседними коллекциями не
больше 2 с, сдвиг поперёк не больше 0,45 м, сдвиг по дальности отличается от
ожидаемого (скорость × cos угла обзора × время) не больше чем на 0,12 м или на
0,85 ожидаемого, смотря что больше. При значении 1 фильтр выключен.
| Параметр | По умолчанию | Пределы | Что делает |
| --- | --- | --- | --- |
| **Tx geometry**, **Rx geometry** | из профиля | — | таблицы положений антенн, см. [Окно программы](desktop.md) |
| **Input positions** | номера с геометрией, которые есть в парах сбора (если таких нет — все с геометрией) | номера через запятую | приёмные антенны в обработке; пустое поле — все с геометрией |
| **Output positions** | так же для выходов | номера через запятую | передающие антенны в обработке; пустое поле — все с геометрией |
| **Min depth (m)** | 2,0 | 0–50 м | ближняя граница дальности изображения и нормировки. Если она не меньше **Max depth (m)**, изображение пустое |
| **Max depth (m)** | 14,0 | 0,1–50 м | дальняя граница сетки и изображения |
| **Range compensation power** | 0,1 | 0–5 | степень компенсации ослабления с расстоянием: вклад умножается на ((R_tx·R_rx) / (R_tx,оп·R_rx,оп))^p, где R_tx,оп и R_rx,оп — расстояния от антенн пары до опорной точки на земле под серединой пары, на дальности 5 м; вес не больше 5. 0 — без компенсации |
| **Object threshold fraction** | 0,7 | 0–1 | поиск объектов останавливается на пике слабее этой доли максимума |
| **Score mode** | `combined` | `peak`, `combined` | как считать оценку объекта (см. выше) |
| **Motion mode** | `int_minus` | `int_minus`, `int_focus` | поправка на движение: полная или только расфокусирующая часть, без сдвига по дальности |
| **Look angle (°)** | 0,0 | от −90 до 90° | наклон радара вниз: задаёт плоскость земли, поправку на движение и фильтр приближения. При ±90° плоскость не определена (`Ground BP is undefined at a look angle of 90 degrees`) |
| **Direction sign** | 1 | −1 или 1 | знак поправки фазы на движение. Подсказка в программе: +1 — более поздние частоты дальше. В 3D SAR не используется |
| **Ignore socket speed** | выключен | — | включена — скорость берётся из поля **Speed (m/s)**; выключена — из клиента локатора, а поле подставляется, пока клиент 5 с не присылал скорость |
| **Speed (m/s)** | 0,0 | от −100 до 100 м/с | скорость радара вдоль земли, положительная — к сцене. 0 — поправки нет. Редактируется при включённой **Ignore socket speed** |
| **Render mode** | `heatmap` | `heatmap`, `objects_only` | вид: карта с объектами или только объекты (без пересчёта) |
| **Max detected objects** | 5 | 0–10000 | N в отборе по числу; 0 — без отбора |
| **Draw top M objects** | 2 | 0–10000 | M в отборе по числу; 0 — без отбора |
| **Approach min frames** | 3 | 1–100 | длина цепочки коллекций для фильтра приближения; 1 — фильтр выключен |
| **Start (MHz)**, **Stop (MHz)** | 3000 и 6000 | в пределах развёртки | полоса частот А-сканов |
| **Radar height (m)** | 1,25 | 0–50 м | высота начала осей радара над землёй |
| **Visible X min (m)**, **Visible X max (m)** | крайние антенны ± 2 м | от −100 до 100 м | окно объектов и графика поперёк; перепутанные min и max программа меняет местами, ширина не меньше 0,1 м |
| **Visible Z min (m)**, **Visible Z max (m)** | 0 и 14 | 0–100 м | окно объектов и графика по дальности; так же не уже 0,1 м |
| **Subtract mean of previous collections** | включён | — | см. «Общие шаги GPR и 3D SAR» |
| **Mean count** | 10 | 0–10000 | сколько предыдущих коллекций усреднять; 0 — не вычитать |
| **PAIR_DEPTH_EQUALIZE** | включён | — | выравнивание уровней пар по дальности |
| **Remove sidelobe objects** | включён | — | удалять объекты, похожие на боковые лепестки |
Изменение **Max detected objects**, **Draw top M objects** и окна видимости
передаётся процессору, только пока выбран режим GPR.
Изображение (**GPR coherent BP — ground plane**): по горизонтали **X (m)**, по
вертикали **Range (m)**. В режиме `heatmap` — цветная карта и объекты белыми ромбами
с оценкой (`0.83`); в режиме `objects_only` — только объекты красными кругами с
подписью `0.83 | x=1.2 | z=5.4`, а к заголовку добавляется `Objects Only`. Передающие
антенны показаны красными треугольниками, приёмные —
синими; они видны, если **Visible Z min (m)** не больше нуля (тогда нижний край
графика немного опускается ниже нуля).
GPR требует хотя бы двух разных пар сбора и таблиц геометрии, а в парах сбора должны
быть все сочетания выбранных входов и выходов: для входов 0, 1 и выходов 0, 1 — пары
`0:0`, `0:1`, `1:0`, `1:1`. Те же требования и сообщения — у 3D SAR. Сообщения при
отказе:
`GPR requires at least 2 distinct run combos (now N)`,
`GPR requires non-empty Tx/Rx geometry in run_config`,
`GPR input positions are missing from geometry config: [...]`,
`GPR output positions are missing from geometry config: [...]`,
`GPR run combos do not cover selected input/output positions: [...]`.
Трассы коллекции должны идти в порядке списка пар сбора, иначе процессор отбрасывает
коллекцию с ошибкой в своём журнале.
На рабочей станции с процессором Intel i7-8700 кадр GPR из 16 пар по 1031 точке
считается примерно за 0,13 с.
## 3D SAR
Считает трёхмерное изображение объёма над землёй перед радаром на видеокарте NVIDIA
(CUDA). Алгоритм и имена настроек взяты из исследовательского ноутбука
`Multi_depthnorm_clean.ipynb` (ячейки 1 и 5); ноутбук хранится вне репозитория.
Порядок обработки:
1. Выбор антенн (**INPUT_IDX**, **OUTPUT_IDX**), вычитание среднего предыдущих
коллекций и поправка на движение (раздел «Общие шаги GPR и 3D SAR»).
2. А-скан каждой пары в полосе **F_START MHz**–**F_STOP MHz** с окном Ханна; у всех
пар коллекции сетка частот должна совпадать.
3. Выравнивание пар (**PAIR_DEPTH_EQUALIZE**) и нормировка на медиану
(**SAR_NORMALIZE_EACH_FRAME**).
4. Апертура: текущая коллекция и до **SAR_FRAME_RADIUS** предыдущих. Антенны
предыдущих коллекций сдвигаются на путь, который радар проехал со скоростью
**SPEED_M_S**; более старые коллекции входят с меньшим весом (от 1 у текущей до 0,8 у
самой старой). Коллекции, которые не удалось обработать, пропускаются.
5. Сетка вокселей с шагами **SAR_VOXEL_M X**, **SAR_VOXEL_M Y**, **SAR_VOXEL_M Z** в осях
сцены: поперёк — от
крайних выбранных антенн с запасом 2 м, по высоте — **SAR_Y_RANGE_M min**–**SAR_Y_RANGE_M max**,
вдоль земли — все точки, расстояние которых по оси обзора лежит в пределах
**MIN_DEPTH**–**MAX_DEPTH**. **SAR_X_SIZE_M** и **SAR_Z_SIZE_M** больше нуля
меняют ширину и длину сетки относительно того же центра.
6. Обратная проекция в каждом вокселе с компенсацией дальности
(**SAR_USE_COMPENSATION**, степень **COMP_RANGE_POWER**, вес не больше 5).
Воксели вне **MIN_DEPTH**–**MAX_DEPTH** по оси обзора остаются нулевыми.
7. Сглаживание по Z и X (гауссово, 1,5 вокселя) и поиск максимума на опорных
плоскостях — тех, что не ближе 0,3 м к верхней и нижней границе высоты.
8. Фон кадра и CLEAN (см. ниже) и проекции.
Объём при настройках по умолчанию и антеннах примера с переключением стимула — около
10,6 млн вокселей (232 × 126 × 364). Объём работы растёт обратно пропорционально
произведению трёх шагов вокселя: если уменьшить все три вдвое, работы станет в восемь
раз больше.
### Фон кадра (BG_GATE_ENABLE)
Фон — медиана объёма по рабочим вокселям (ненулевым, на опорных плоскостях). Отношение
максимума кадра к фону показывает, насколько самый сильный отклик выделяется над
типичным уровнем помех. Пороги 6 и 8,5 — постоянные ноутбука.
Если объектов CLEAN нет, **BG_GATE_ENABLE** управляет яркостью всего кадра: при
отношении меньше 6 кадр не показывается, при 6 его максимум появляется на уровне
**SAR_CLOUD_THRESHOLD**, с 8,5 кадр показывается в полную яркость, а между 6 и 8,5
яркость растёт линейно.
Если CLEAN нашёл объекты, они видны всегда, каждый в своей шкале, при любом отношении
кадра к фону. **BG_GATE_ENABLE** тогда прячет только остаток после вычитания, если его
максимум меньше 6 фонов.
### CLEAN (SAR_CLEAN_ENABLE)
CLEAN разделяет сцену на объекты. На каждом шаге он строит изображение остатка на
отдельной сетке поиска (4 × 10 × 4 см по X, Y и Z, на всю область сцены), находит
точку, которая сильнее всего превышает порог, и вычитает её отклик из А-сканов всех
пар. Порог для вокселя — меньшее из двух: 5,5 медианы положительных вокселей
изображения и 4,5 медианы на той же дальности (по кольцам дальности шириной 0,25 м).
Вычитание идёт, пока ни один воксель не превышает порог, но не больше 15 шагов.
Шаги, точки которых лежат не дальше 0,35 м по X, 0,50 м по Y и 0,40 м по Z от первой
точки группы, объединяются в один объект. Затем каждый объект строится отдельно и
рисуется в своей шкале от 0 до 1 поверх остатка: слабый объект виден рядом с
сильным. Уровень объекта — отношение его пика к фону кадра.
CLEAN работает только на апертуре из одного кадра: всегда при
**SAR_FRAME_RADIUS** = 0, а при большем значении — пока пригодных предыдущих
коллекций нет (сразу после **Start**, после очистки истории). Иначе статус сообщает,
что CLEAN пропущен.
Сетка поиска CLEAN охватывает всю область сцены, даже если **SAR_X_SIZE_M** и
**SAR_Z_SIZE_M** сужают сетку кадра: отражатель сразу за краем кадра вычитается и не
оставляет внутри кадра боковой лепесток, похожий на объект.
### Рамки (SAR_MARK_ENABLE)
С **SAR_MARK_ENABLE** объекты CLEAN, которые не дальше 0,8 м друг от друга поперёк и
0,5 м вдоль земли (в том числе цепочкой), объединяются в группу, и вокруг группы рисуется
рамка. Центр группы — средняя высота её объектов, взвешенная по уровню (учитываются
объекты не слабее 0,4 сильнейшего), верх — верхняя граница их откликов. Классы
проверяются по порядку строк таблицы, рамка получает первый подходящий:
| Цвет | Класс | Условие |
| --- | --- | --- |
| серый пунктир | at the boundary — у края сцены | сильнейший объект группы у края: по оси обзора не дальше 0,25 м от **MAX_DEPTH** или 0,10 м от **MIN_DEPTH**, поперёк не дальше 0,2 м от края сетки или по высоте не дальше 0,4 м от её верха. Отклик может частично лежать за сценой, класс пока не определить |
| красный | very low — очень низко (как мина) | центр не выше 0,15 м и верх не выше 0,45 м |
| оранжевый | on the ground — на земле | центр не выше 0,35 м |
| синий | above the ground — над землёй | остальные |
Легенда рамок — строкой под видами; она видна, когда включены и
**SAR_MARK_ENABLE**, и **SAR_CLEAN_ENABLE**. Без рамок объекты пронумерованы и
подписаны уровнем в фонах: `1: 12 bg`.
### Виды
Четыре вида: **Front (X-Y)**, **Side (Z-Y)**, **Top (X-Z)** — проекции максимума
объёма спереди, сбоку и сверху — и **3D Projection**, наклонный вид. На видах с осью
высоты пунктиром показана земля (Y = 0), на трёх плоских видах — положения выбранных
антенн. Пустой квадрат отмечает максимум; его сторона — наибольшая из ширин отклика
на половине максимума вдоль осей вида (не меньше одного вокселя). Это размер
отклика, а не размер предмета. С объектами CLEAN и при пустом кадре квадрата нет.
**SAR_CLOUD_THRESHOLD** скрывает пиксели слабее заданной доли максимума, не меняя
шкалу. **Zoom (%)** от 100 до 1000 уменьшает окно всех четырёх видов и плавно сдвигает
его центр к самому яркому отклику; при 100% видна вся сцена.
Строка над видами показывает состояние:
| Текст | Когда |
| --- | --- |
| `SAR3D: waiting for measurements` | результатов ещё не было |
| `SAR3D: waiting for projection data` | результат без изображений |
| `SAR3D: ready — CUDA …` | изображение посчитано; разбор строки — ниже |
| `SAR3D: no signal — No signal inside the selected depth range` | в пределах **MIN_DEPTH**–**MAX_DEPTH** нет сигнала |
| `SAR3D: error — …` | ошибка настроек, данных или расчёта; текст ошибки |
| `SAR3D: warning — …` | нет подходящей видеокарты или программа собрана без CUDA |
Строка `ready` складывается из частей через `|`, например:
```text
SAR3D: ready — CUDA | peak 7.2 backgrounds (appears from 6, full scale from 8.5): scale ×0.83 | max X +0.12, Y +0.30, Z 4.85 m | level 0.0412 | zoom 100%
```
| Часть | Что значит |
| --- | --- |
| `CUDA`, `CUDA; CLEAN: …` | расчёт на видеокарте и итог CLEAN: `CLEAN: K objects, each in its own scale (S subtractions, residual maximum R backgrounds)`, `CLEAN: no objects (…)` или `CLEAN skipped: it needs a one-frame aperture, this one has N frames` |
| `peak N backgrounds` | отношение максимума кадра к фону; без объектов CLEAN дальше — состояние порога: `below the appearance level, frame empty`, `scale ×K` (кадр показан с множителем яркости K) или `full scale` |
| `boxes: …` | число рамок каждого класса, например `boxes: 1 very low, 0 on the ground, 2 above the ground` (`at the boundary` — только если такие есть) |
| `max X …, Y …, Z … m` | положение максимума в осях сцены; у X и Y указан знак |
| `level …` | уровень, на который нормированы виды: максимум объёма до нормировки |
| `zoom N%` | масштаб из **Zoom (%)** |
### Настройки 3D SAR
| Параметр | По умолчанию | Пределы | Что делает |
| --- | --- | --- | --- |
| **INPUT_IDX**, **OUTPUT_IDX** | номера с геометрией, которые есть в парах сбора (если таких нет — все с геометрией) | номера через запятую | приёмные и передающие антенны в обработке; пустое поле — все с геометрией |
| **F_START MHz**, **F_STOP MHz** | 1500 и 6000 | в пределах развёртки | полоса частот А-сканов |
| **MIN_DEPTH**, **MAX_DEPTH** | 2,0 и 8,0 | 0–50 и 0,1–50 м | границы расстояния по оси обзора: задают длину сцены вдоль земли и обнуляют воксели вне себя |
| **SAR_VOXEL_M X**, **SAR_VOXEL_M Y**, **SAR_VOXEL_M Z** | 0,025, 0,02, 0,02 | 0,01–0,1 м | шаги сетки. Сглаживание задано в вокселях, поэтому от шагов зависит и оно, а с ним фон кадра и изображения объектов CLEAN; сетка поиска CLEAN от них не зависит |
| **LOOK_ANGLE_DEG** | 19,5 | от −90 до 90° | наклон радара вниз: оси сцены, поправка на движение |
| **Ignore socket speed** | выключен | — | как в GPR |
| **SPEED_M_S** | 0,0 | от −100 до 100 м/с | скорость радара вдоль земли для поправки на движение и сдвига апертуры |
| **SAR_FRAME_RADIUS** | 0 | 0–24 | сколько предыдущих коллекций добавлять в апертуру. 0 — только текущая (нужно разнесение антенн по высоте); больше нуля — нужна точная скорость |
| **BP_GROUND_DEPTH_M** | 1,25 | 0–10 м | высота начала осей радара (строки Y = 0 таблиц антенн) над землёй |
| **COMP_RANGE_POWER** | 0,3 | 0–2 | степень компенсации ослабления с расстоянием |
| **SAR_USE_COMPENSATION** | включён | — | включает компенсацию дальности |
| **SAR_NORMALIZE_EACH_FRAME** | включён | — | нормирует каждую пару на медиану её А-скана в пределах **MIN_DEPTH**–**MAX_DEPTH** |
| **PAIR_DEPTH_EQUALIZE** | включён | — | выравнивание уровней пар по дальности |
| **Subtract mean of previous collections**, **Mean count** | включён, 10 | 0–10000 | вычитание среднего предыдущих коллекций |
| **BG_GATE_ENABLE** | включён | — | фон кадра (см. выше) |
| **SAR_CLEAN_ENABLE** | включён | — | CLEAN (см. выше) |
| **SAR_X_SIZE_M (0 = auto)** | 0 | 0–100 м | ширина сетки кадра поперёк; 0 — от антенн с запасом 2 м |
| **SAR_Y_RANGE_M min**, **SAR_Y_RANGE_M max** | −0,5 и 2,0 | от −50 до 50 м | границы высоты; max должно быть больше min |
| **SAR_Z_SIZE_M (0 = auto)** | 0 | 0–100 м | длина сетки кадра вдоль земли; 0 — по MIN_DEPTH–MAX_DEPTH |
| **SAR_CLOUD_THRESHOLD** | 0,65 | 0–1 | только вид: скрыть пиксели слабее этой доли |
| **Zoom (%)** | 100 | 100–1000 | только вид: приближение к самому яркому отклику |
| **SAR_MARK_ENABLE** | включён | — | только вид: рамки вместо номеров |
Время расчёта на GTX 1060 при сцене по умолчанию: около 0,1–0,2 с на кадр, фон кадра
добавляет около 0,1 с, CLEAN — ещё 0,4–0,5 с в зависимости от числа шагов.
Результат 3D SAR должен поместиться в слот кольца `results`. Если не помещается,
статус сообщает, сколько нужно места, и советует увеличить шаги вокселя, сузить
дальность или увеличить `rings.results.slot_size_bytes`.
### Видеокарта
3D SAR нужна видеокарта NVIDIA с драйвером. Ядра расчёта компилируются при запуске
библиотекой NVRTC под установленную карту. Если при сборке не нашлись заголовки и
библиотеки CUDA, программа собирается без 3D SAR, и в этом режиме над видами висит
предупреждение
`SAR3D requires an NVIDIA GPU with CUDA; this program was built without nvrtc.h, cuda.h, libnvrtc or libcuda. Install CUDA and rebuild.`
Для карты старее, чем умеет установленная NVRTC, предупреждение советует собрать
программу с NVRTC 12 (как это сделать — в главе [Разработка](development.md)).
Остальные режимы видеокарты не требуют.
## Общие шаги GPR и 3D SAR
### Вычитание среднего предыдущих коллекций
Из S21 каждой пары вычитается
комплексное среднее этой пары за до **Mean count** предыдущих коллекций. Остаётся то,
что изменилось: неподвижные отражения и прямая связь антенн гасятся, движущиеся
относительно радара предметы остаются. Процессор хранит столько коллекций, сколько
нужно для среднего и апертуры, независимо от ёмкости колец.
### Поправка на движение
Время каждой частотной точки программа берёт из окна съёма
развёртки: точки идут равномерно от начала окна до конца, а в режиме переключения
стимула стадии одной частоты идут по очереди в порядке номеров выходов. Фаза каждой
точки поворачивается на 4π·f·v·cos(p)·t / c (v — скорость, p — угол обзора, t —
время точки относительно середины коллекции; в GPR ещё со знаком
**Direction sign**), и вся коллекция приводится к одному моменту. При нулевой
скорости поправки нет. В GPR с **Motion mode** = `int_focus` из поправки убирается
её постоянная и линейная по частоте часть — чистый сдвиг по дальности. Скорость берётся из
клиента локатора,
если он присылал её в последние 5 с и флажок **Ignore socket speed** выключен,
иначе — из поля скорости.
### Выравнивание пар по дальности (PAIR_DEPTH_EQUALIZE)
Для каждой пары считается
плавный уровень А-скана по дальности: медиана в окне 1,5 м по блокам 0,10 м. Каждая
пара умножается на положительный коэффициент, зависящий от дальности, так что её
уровень становится средним геометрическим уровней всех пар. Различия между парами
уходят, общий спад с дальностью и фаза сохраняются. Нужно хотя бы две пары.
### Нормировка на медиану
Каждая пара делится на медиану модуля своего А-скана в
пределах границ дальности. В GPR выполняется всегда, в 3D SAR — с
**SAR_NORMALIZE_EACH_FRAME**.
### Компенсация дальности
Отражение слабеет с расстоянием примерно как 1/(R_tx·R_rx). Вклад пары в точку
умножается на ((R_tx·R_rx) / (R_tx,оп·R_rx,оп))^p, где R_tx,оп и R_rx,оп — расстояния от
антенн пары до опорной точки на земле под серединой пары, на дальности 5 м; вес
ограничен пятью.
## Геометрия
Антенны задаются в осях радара (X, Y, Z), изображения 3D SAR — в осях сцены (X, Y —
высота над землёй, Z — вдоль земли); см. рисунок в главе
[Как работает радар](principles.md). Ось X у обеих систем общая. Переход из осей
сцены в оси радара при высоте h и угле обзора p:
Y_радар = (Y − h)·cos p + Z·sin p,
Z_радар = −(Y − h)·sin p + Z·cos p.
Плоскость земли (Y = 0) в осях радара: Y_радар = (−h + Z_радар·sin p) / cos p. На ней
лежит сетка GPR; при p = ±90° плоскость не определена, и программа отказывает
(`Ground BP is undefined at a look angle of 90 degrees`).
«Глубина» в разных режимах означает разное:
| Где | Что означает |
| --- | --- |
| B-scan, **Cut (m)** и **Max depth (m)** | половина пути волны, отсчитанная от **Cut (m)** |
| GPR, **Min depth (m)** и **Max depth (m)** | координата Z_радар точки на плоскости земли |
| 3D SAR, **MIN_DEPTH** и **MAX_DEPTH** | координата Z_радар вокселя |
| Нормировка А-скана | половина пути волны |
Волна в GPR и 3D SAR считается идущей со скоростью 3·10⁸ м/с (воздух), в B-scan —
299 792 458 м/с.
+90
View File
@@ -0,0 +1,90 @@
# Неполадки
Сначала посмотрите журнал окна (секция **Runtime Log**) и строку состояния. Если там
пусто, а картинка не обновляется, причина обычно в журналах процессов в
`python_app/runtime/logs/`:
| Файл | Что в нём искать |
| --- | --- |
| `acquisition.err.log` | приборы не открываются, ожидание приборов, ошибки коллектора E-502 |
| `data_preprocessor.err.log` | отброшенные коллекции: не та сетка частот, нет пары в наборе |
| `data_processor.err.log` | ошибки обработки, отклонённый live-файл, 3D SAR, локатор |
| `radar.log` | всё, что было в окне журнала |
У каждого процесса есть ещё `<процесс>.out.log` (обычный вывод), а перед каждым
запуском прежние журналы переименовываются в `*.prev`: после автоматического
перезапуска причина падения лежит в `<процесс>.err.log.prev`. Без экрана журнал
программы — `journalctl -u radar.service -f`.
## Программа не запускается
| Признак | Причина | Что делать |
| --- | --- | --- |
| окно не открывается, в выводе ошибка о LibreVNA (`No compatible LibreVNA USB device found…`) | профиль с `driver_mode: "native"`, а прибор не подключён: при запуске окно читает пределы прибора | подключите прибор или запустите с профилем симулятора: `./start.sh --profile run_config.json` |
| окно открывается с ошибкой `Cannot switch processing mode to …` и закрывается, и так при каждом запуске | запомненный профиль читается, но его настройки нельзя применить (например, `SAR_Y_RANGE_M max` не больше min) | удалите `python_app/runtime/gui_session_state.json` и исправьте профиль |
| `Another radar instance already holds /tmp/radar_system.lock; aborting.` | программа уже запущена: в другом окне или службой | закройте другой экземпляр или `sudo systemctl stop radar.service` |
| служба перезапускается каждые 3 с | без экрана не удаётся **Start** (часто — не выбраны наборы) или нет программ в `build/bin/` | `journalctl -u radar.service`; выберите наборы и сохраните профиль, запустив окно на машине |
| `[start.sh] … Run an interactive './start.sh' once (online)…` | без экрана не хватает Python-пакетов | запустите `./start.sh` один раз с экраном и сетью |
## Start отказывает
| Сообщение | Причина | Что делать |
| --- | --- | --- |
| `Select all required preprocess sets in Preprocessing Panel before Start: …` | не выбраны калибровка или референс | выберите или снимите их в **Preprocessing…**; для симулятора — **Create Neutral S21 Sets** |
| наборы пропали из списков, в журнале `Preprocess selection reset: …` | изменилась развёртка, серийные номера или положения ключей — другой ключ радара | верните прежние настройки или снимите наборы заново |
| `Selected … set does not cover requested run combos` | набор снят для другой матрицы | снимите набор заново при текущих ключах |
| `rings.<имя>.slot_size_bytes must be at least N for this sweep` | слот кольца меньше коллекции | увеличьте `slot_size_bytes` в профиле |
| `B-scan with native switches requires exactly one run combo (now N)` | B-scan с настоящими ключами показывает одну пару | задайте одну пару в **Single combo** |
| `GPR requires at least 2 distinct run combos (now N)` и другие `GPR …` | для GPR и 3D SAR нужны пары и геометрия | проверьте пары сбора и таблицы геометрии |
| `Object calibration … differs from the captured state` | калибровка по объекту снята при других настройках, геометрии или наборе калибровки | выберите **Off** или снимите калибровку по объекту заново |
| `Stop all pipeline processes before loading a config profile` | профиль загружают при работающем сборе | нажмите **Stop** |
## Нет данных от приборов
| Признак | Причина | Что делать |
| --- | --- | --- |
| сбор идёт, приборы не опрашиваются, на графиках один движущийся точечный отражатель | профиль симулятора: `driver_mode: "mock"` (например, корневой `run_config.json`) | загрузите профиль с `driver_mode: "native"`; если в журнале `Failed to load the last selected config profile…`, запомненный профиль не прочитался — исправьте его и загрузите снова |
| `No datapoints received from at least one device; hardware trigger sync did not start. Check Trigger Out/In loop and 10 MHz reference wiring.` | многоприборная схема: не работает петля триггера | проверьте кабели петли и контроллер STM32 в режиме переключения стимула |
| `Packet not received from <серийный номер> within 1.0 s` | включена внешняя опора (`force_external_reference: true`), а 10 МГц не подана | выключите опору в профиле или проверьте, что 10 МГц доходит до всех приборов |
| `<прибор> unavailable (attempt N); retrying until present` | прибор не открывается; процесс сбора ждёт его бесконечно | проверьте USB и серийный номер в профиле; правило udev ставит `./start.sh` при обычном запуске |
| в режиме переключения стимула приборы падают с `LIBUSB_ERROR_IO` | полоса ПЧ 1 кГц | используйте 5–50 кГц, рабочее значение — 50 кГц |
| ключи не переключаются, ошибок нет | неверный GPIO-чип или ключ в `mock` | проверьте `gpiodetect`, `gpioinfo -c gpiochip0` и `driver_mode` ключей |
| `Failed to open GPIO chip '…'` или `Failed to request GPIO lines…` | чипа нет или линии заняты другой программой | проверьте чип и что линии свободны (`gpioinfo`) |
## Радиофотонный радар
| Сообщение | Причина | Что делать |
| --- | --- | --- |
| `…sweep rejected (count=n): covered [a, b] Hz does not span band [c, d] Hz…` | развёртка лазера не покрывает выходную полосу | проверьте фазовую калибровку (`rph_radar_calibrate`) и `radar.rph_radar.band` |
| `Timed out waiting for a RadioPhotonic radar (RPH) sweep covering the configured band` | за **Read timeout (s)** не пришло ни одной пригодной развёртки | проверьте, что лазер запущен (**Apply Radar**), и сигналы на входах |
| `Cannot load libx502api.so: …` | нет библиотек L-Card | положите `libx502api.so` и `libe502api.so` в `~/.local/lib` или задайте `LD_LIBRARY_PATH` в **Environment JSON** |
| `Timed out waiting for RadioPhotonic radar (RPH) TTY …` | коллектор не запустился | смотрите строки `Collector: …` в `acquisition.err.log`; проверьте **Executable path** (`make rph_radar_collector`) |
| `ADC source clock leads host monotonic clock; restart acquisition` | часы АЦП ушли вперёд часов компьютера | перезапустите сбор |
| `Serial port '…' is busy — another program is already using the laser controller…` | порт платы лазеров занят другой программой | закройте её |
| `Device rejected command: STATE 0x…` | плата лазеров отвергла команду | проверьте значения токов и температур и питание платы |
| `RFG protocol violation: …` | на порт пришёл чужой пакет | привяжите `udp_bind` к проводному адресу, на котором работает плата |
## Картинка не обновляется или пустая
| Признак | Причина | Что делать |
| --- | --- | --- |
| сбор идёт, счётчик `History` растёт только у `raw` | препроцессор отбрасывает коллекции | `data_preprocessor.err.log`: обычно не совпадает сетка частот набора; снимите наборы заново |
| GPR пустой | нет антенн с геометрией среди выбранных или **Min depth (m)** не меньше **Max depth (m)** | проверьте поля и таблицы; ошибки — в `data_processor.err.log` |
| **Single Capture** в режиме GPR ждёт 300 с и останавливается | результат GPR пустой, и съём его не дождался | проверьте настройки GPR, как выше |
| `SAR3D: warning — …` | нет видеокарты NVIDIA или программа собрана без CUDA | глава [Установка и запуск](installation.md), раздел о видеокарте |
| `SAR3D: error — the views need N KiB but a results ring slot holds M KiB…` | изображения 3D SAR не помещаются в слот | увеличьте шаги вокселя, сузьте дальность или увеличьте `rings.results.slot_size_bytes` |
| `SAR3D: no signal — No signal inside the selected depth range` | в пределах MIN_DEPTH–MAX_DEPTH нет сигнала | проверьте границы дальности и полосу |
| кадр 3D SAR пустой, в строке `frame empty` | максимум ниже 6 фонов при включённой **BG_GATE_ENABLE** | это норма для пустой сцены; для проверки выключите **BG_GATE_ENABLE** |
| `Stopping pipeline after 400 consecutive reader poll failures` | конвейер остановлен из-за ошибок чтения колец | нажмите **Start**; повторяется — проверьте `/dev/shm` |
| `Status: result display error (acquisition continues)` | ошибка отрисовки; сбор продолжается | текст ошибки — в журнале |
## Прочее
| Признак | Причина | Что делать |
| --- | --- | --- |
| после перезапуска пропали настройки и выбор наборов | изменения в окне и вебе не пишутся в профиль сами | **Save Config…** |
| отказ в доступе к кольцам (`Permission denied`) | программу или тесты запускали от root, и кольца в `/dev/shm` принадлежат root | при закрытой программе удалите их: `sudo rm -f /dev/shm/radar_*` |
| веб-страница не открывается | порт занят или не тот адрес | проверьте вывод программы (`journalctl`) на `address already in use`; другой порт — `RADAR_SYSTEM_WEBUI_PORT` |
| `Disk recording stopped because the writer could not keep up` | диск не успевает за записью | пишите на более быстрый диск или меньше коллекций; записанное сохранено |
| `Snapshot directory already exists: …` | каталог набора данных уже есть | задайте другое **Name** |
| `Log panel overflow: N record(s) dropped…` | журнал в окне переполнился | всё есть в `radar.log` |
+226
View File
@@ -0,0 +1,226 @@
# Веб-интерфейс, кнопка и локатор
Кроме окна, с программой работают из браузера, GPIO-кнопкой и через TCP-локатор: он
отдаёт внешним программам найденные объекты и принимает скорость радара. Без экрана
(раздел «Работа без экрана») других способов нет.
## Веб-интерфейс
Веб-сервер работает внутри программы, и с окном, и без экрана. Откройте в браузере
`http://<адрес машины>:8080/`. Порт меняется переменной окружения
`RADAR_SYSTEM_WEBUI_PORT`; неверное значение заменяется на 8080. Если порт занят,
программа работает без веб-интерфейса, а ошибка (`address already in use`) видна
только в выводе процесса. Если сервер не смог запуститься по другой причине, в журнале
появляется `Failed to start web UI`.
> **Внимание.** У веб-интерфейса нет пароля и шифрования: любой, кто достаёт до
> порта по сети, управляет радаром. По адресам `/docs`, `/redoc` и `/openapi.json`
> открыто описание API. Подключайте машину только к доверенной сети.
### Страница
Верхняя строка — кнопки управления: **Start**, **Single Capture**, **Stop**,
**Capture Temporary Reference**, **Remove Last Measurement**. Они делают то же, что
одноимённые кнопки окна (глава [Окно программы](desktop.md)).
**Processor Output** — картинка с графиков окна программы. Масштабировать её в
браузере нельзя: она повторяет то, что показано в окне. В режиме B-scan над ней
список пар (**Input i / Output o**) и кнопка **Next Combination**, в режиме 3D SAR —
строка состояния 3D SAR.
Боковая панель:
| Блок | Элемент | Что делает |
| --- | --- | --- |
| **Config Profile** | список и **Load** | загружает профиль из каталога `run_configs/`, как **Load Config…**; доступна при остановленном сборе. Список читается при открытии страницы; новые файлы видны после её перезагрузки |
| | `current: <файл>` | текущий профиль |
| **Save / Record Dataset** | **Save path**, **Name (optional)** | каталог и имя набора данных; при открытии страницы берутся из окна, а сохранение и запись из браузера переписывают ими **Path** и **Name** окна (запись — и **Sweeps to record**) |
| | **Save Last 1000** | сохраняет до 1000 последних коллекций каждой стадии, как одноимённая кнопка окна |
| | **Sweeps to record** и **Start + Record to Disk** | запускает сбор и записывает на диск заданное число следующих коллекций; ниже — ход записи `recording N / M sweep(s)…` |
| **Processor Settings** | форма | настройки текущего режима обработки (ниже); заголовок сворачивает форму |
| | таблицы **Tx geometry**, **Rx geometry** | геометрия антенн в режимах GPR и 3D SAR: **−** удаляет антенну, **Add Antenna** добавляет следующую свободную |
| | **Apply** | отправляет изменённые поля |
| | **Clear Runtime History** | очищает историю, как одноимённая кнопка окна |
Нижняя строка: `running` — идёт ли сбор, `processor` — работает ли процессор,
`ring` — имя кольца результатов, `scans` — сколько коллекций в истории каждой стадии,
и признак картинки: `live` — кадры приходят, `stale` — новых кадров нет дольше 2 с
(так бывает и тогда, когда картинка просто не меняется), `no data` — кадров ещё не
было. Сообщения об успехе и ошибках всплывают на 2,6 с; ошибки — красным.
Если окно браузера уже 760 пикселей, панель уходит под картинку.
### Как выполняются действия
Каждая кнопка вызывает тот же код, что и кнопка окна, с теми же проверками. Действия
из нескольких браузеров выполняются по очереди. Если окно программы не начало
действие за 30 с, запрос отменяется
(`The desktop did not start the action in time; the request was cancelled`). Если
начало, но не закончило, браузер получает
`The desktop action is still running; check its status before retrying`, а действие
доводится до конца. Ошибки, которые возникают позже (например, на середине
однократного съёма), видны только в журнале.
### Форма настроек
Форма строится из полей окна: те же подписи, пределы и группы. В ней поле **Mode** и
поля текущего режима; смена режима сразу перестраивает форму. **Apply** отправляет
только изменённые поля, поэтому два человека, правящие разные поля, не мешают друг
другу. Порядок полей может отличаться от окна. Значения проверяются так же, как в
окне: вне пределов — `Numeric setting is outside its allowed range`, неизвестный
вариант — `Unknown setting option: …`; при ошибке не применяется ни одно поле из
отправленных. Дробное число округляется до точности поля окна. Изменения из окна
приходят в форму раз в секунду.
Только в окне программы:
- **Apply Radar**, **Load Config…** из произвольного места, **Save Config…**;
- окно **Preprocessing…**: съём и выбор наборов, калибровка по объекту;
- **Save S21 JSON**;
- секции **Radar** и **Switches**: развёртка, лазер, ключи, пары сбора;
- **Show magnitude**, **Show phase**, **Unwrap phase**, **Scans to show (stopped)**;
- журнал и его уровень, строка состояния;
- масштаб и сдвиг графиков мышью.
### Картинка и обновление
Программа снимает картинку графиков, только когда подключён хотя бы один браузер и
изображение изменилось, и не чаще 5 раз в секунду. Каждый браузер получает самую
свежую картинку, состояние и настройки; медленное соединение пропускает
промежуточные кадры. Пока окно занято синхронным съёмом (калибровка, референс),
картинка не обновляется. Браузер переподключается сам через 1,5 с после обрыва.
### API
Страница работает через HTTP API, которым можно пользоваться и из своих программ.
Тело запросов и ответов — JSON; ошибка действия — код 400 и
`{"detail": "<сообщение>"}`. Тело неверного вида (поле не того типа, нет обязательного
поля) даёт код 422 со списком ошибок в `detail`.
| Запрос | Тело | Что делает |
| --- | --- | --- |
| `GET /api/status` | — | состояние (то же, что в нижней строке страницы) |
| `POST /api/start` | — | **Start** |
| `POST /api/single_capture` | — | **Single Capture** |
| `POST /api/stop` | — | **Stop** |
| `POST /api/tmp_reference` | — | **Capture Temporary Reference** |
| `POST /api/remove_last` | — | **Remove Last Measurement** |
| `POST /api/clear_history` | — | **Clear Runtime History** |
| `GET /api/configs` | — | список профилей в `run_configs/` |
| `POST /api/load_config` | `{"name": "<файл>.json"}` | загрузить профиль |
| `POST /api/save_dataset` | `{"path": "…", "name": "…"}` | **Save Last 1000** |
| `POST /api/start_recording` | `{"path": "…", "name": "…", "count": N}` | **Start + Record to Disk**, N от 1 до 1 000 000 |
| `GET /api/live_settings` | — | описание полей формы |
| `POST /api/live_settings` | `{"<поле>": значение, …}` | применить настройки. Имена полей — из описания формы; у настроек обработки они совпадают с ключами `processing_live.json` |
| `POST /api/display_combo` | `{"input_pos": i, "output_pos": o}` | выбрать пару B-scan |
| `POST /api/next_combo` | — | **Next Combination** |
| `WS /ws` | — | поток сообщений `frame` (картинка PNG в base64), `status`, `settings` |
## GPIO-кнопка
Кнопка, подключённая к GPIO компьютера, снимает временный референс — то же, что
**Capture Temporary Reference**. Она настраивается в разделе `control_button`
профиля (глава [Файл конфигурации](configuration.md)) и читается один раз при запуске
программы.
Схема по умолчанию: кнопка между линией GPIO и землёй, внутренняя подтяжка к питанию,
нажатие — спад уровня, по одному действию на нажатие. Индикации на самой кнопке нет;
результат виден в журнале (`GPIO control button pressed: capturing tmp reference`, затем
`Tmp S21 Reference captured: …`) и на графике pass-through.
Нажатие, пришедшее, пока программа останавливает сбор перед съёмом, отбрасывается
(`Tmp reference capture already in progress; ignoring duplicate request`). Нажатие во
время самого съёма ждёт его конца и запускает ещё один съём. Ошибки съёма только
записываются в журнал, окон с ошибкой кнопка не
открывает. Если настройки неполные, кнопка не включается
(`GPIO control button configuration is incomplete or unsupported; watcher not started`).
Если линию не удалось открыть — `Failed to start GPIO control button watcher`; если
наблюдение за линией прервалось — `GPIO control button watcher stopped: …`. В обоих
случаях программа работает дальше без кнопки до перезапуска.
## TCP-локатор
Локатор — TCP-сервер внутри процессора. Он отдаёт подключившимся программам объекты,
найденные режимом GPR, и принимает от них скорость радара для поправки на движение.
Настройки — раздел `run.locator_server` профиля; по умолчанию порт 8888 на всех
интерфейсах. Сервер работает, пока работает процессор: от первого **Start** до
закрытия программы или перезапуска процессора. **Stop**, временный референс и
пересчёт B-scan процессор и локатор не останавливают.
Обмен в обе стороны идёт пакетами: заголовок из 8 байт — номер устройства и длина
данных (оба uint32, младший байт первым), затем текст JSON в UTF-8.
Пакет от радара:
```text
03 00 00 00 71 00 00 00
{"obs":[{"crs":0.3,"dst":4.3},{"crs":-1.0,"dst":6.0},{"crs":1.0,"dst":2.0}],"sts":1,"tim":"13:42:25.058","ver":1}
```
| Поле | Что это |
| --- | --- |
| `obs` | объекты: `crs` — координата X (вдоль ряда антенн, поперёк направления обзора), `dst` — дальность (Z), метры, округлены до 0,1 м |
| `sts` | всегда 1 |
| `tim` | местное время сборки пакета `ЧЧ:ММ:СС.ммм` (не время съёма) |
| `ver` | `run.locator_server.protocol_version` |
В заголовке — `run.locator_server.device_id`. Пакет уходит после обработки каждой
новой коллекции в любом режиме; вне режима GPR и без объектов список `obs` пуст.
Объекты — те же, что видны на графике GPR: внутри окна видимости, после отбора по
числу и фильтра приближения. При пересчёте после смены настроек уходит пакет только
для новейшей коллекции. Новый клиент сразу получает последний отправленный пакет.
Пакет к радару — скорость в м/с:
```text
00 00 00 00 0c 00 00 00
{"vlc":1.25}
```
Остальные поля и номер устройства в заголовке не читаются. Скорость считается свежей
5 с после прихода. Пока она свежая и на странице GPR или 3D SAR выключена
**Ignore socket speed**, она заменяет скорость из поля, а её изменение пересчитывает
новейшую коллекцию в GPR и 3D SAR. Использованная скорость сохраняется в каждом
результате.
Клиентов может быть несколько; у каждого очередь на `client_queue_size` пакетов (по
умолчанию 32), при переполнении выбрасываются старые. Клиент, который 5 с не принимает
данные или прислал пакет больше `max_payload_bytes` (по умолчанию 65 536 байт),
отключается. Сообщения локатора пишутся в
`python_app/runtime/logs/data_processor.err.log`; если порт занят, процессор работает
без локатора и пишет туда же `locator server disabled`.
## Работа без экрана
На машине без оператора (Raspberry Pi) программу запускает служба systemd
`radar.service` (установка — в главе [Установка и запуск](installation.md)). Она
запускает `./start.sh --headless --skip-build`: окно не показывается, через 0,5 с
выполняется **Apply Radar**, ещё через 1 с — **Start**, даже если **Apply Radar** не
удался. Если **Start** не удался, программа завершается, и systemd запускает её
снова через 3 с.
Профиль служба берёт последний использованный (из
`python_app/runtime/gui_session_state.json`), а если его нет — корневой
`run_config.json`, то есть симулятор. Поэтому перед установкой службы загрузите или
сохраните на машине нужный профиль. Если запомненный профиль не читается, программа
пишет `Failed to load the last selected config profile; falling back to root run_config.json.`,
берёт корневой профиль и запоминает уже его: исправленный профиль нужно загрузить снова.
| Канал | Для чего |
| --- | --- |
| браузер, `http://<машина>:8080/` | запуск и остановка, временный референс, сохранение данных, профили из `run_configs/`, настройки обработки |
| GPIO-кнопка | временный референс |
| TCP-локатор, порт 8888 | объекты и скорость |
| `sudo systemctl start` / `stop` / `restart radar.service` | управление службой |
| `journalctl -u radar.service -f` | журнал службы |
Без экрана нет окон с ошибками: всё пишется в журнал. Окно калибровки и
**Save Config…** без экрана недоступны: калибровку снимайте, запустив окно программы
на той же машине. При запуске окна `./start.sh` сначала останавливает службу; после
закрытия окна она сама не запускается.
Если процесс конвейера упал, программа перезапускает конвейер: первый раз сразу,
следующие подряд — не раньше чем через 6, 12, 24, 48, затем 60 с после предыдущей
попытки; когда данные снова пошли, счёт начинается заново. После 400 подряд ошибок чтения
колец конвейер останавливается и сам уже
не запускается: нажмите **Start** в браузере.
+111
View File
@@ -0,0 +1,111 @@
# Типовые сценарии
## Подготовка к измерениям
1. Подключите приборы, ВЧ-ключи и антенны (глава [Оборудование](hardware.md)).
2. Запустите программу с профилем установки:
`./start.sh --profile <профиль>`. Проверьте в секции **Radar** развёртку, а под
полями — пределы прибора: если они прочитаны, прибор на связи.
3. Нажмите **Apply Radar**.
4. Убедитесь, что в **Selected Preprocess Sets** выбраны наборы, снятые при этой
развёртке. Если списки пусты — снимите калибровку и референс (ниже).
5. Выберите режим pass-through (**Mode**), нажмите **Single Capture** и посмотрите
S21 каждой пары: у всех пар должен быть сигнал, без пар на уровне шума.
**Single Capture**, как и **Start**, требует выбранных наборов; при первой настройке
сигнал каждой пары виден на графиках окна предобработки во время съёма калибровки.
## Калибровка
Калибровку (through) снимают при первой настройке установки, после замены кабелей,
ключей или антенн и после смены развёртки: другая развёртка — другой ключ радара, и
старые наборы для неё не видны.
1. Нажмите **Preprocessing…**, задайте **Set name** и
**Sweeps per combo (median)**.
2. Подготовьте эталонный тракт для калибровки и нажмите **Start S21 Calibration**.
3. Снимайте пары (**Capture Current Combo** или **Capture All Remaining**), проверяя
каждую развёртку на графике; неудачную переснимите через **Undo Last Capture**.
4. Нажмите **Save Captured Set**.
Подробно — в главе [Калибровка и предобработка](preprocessing.md).
## Референс
Референс — пустая сцена: то, что должно остаться фоном.
1. Уберите из сцены цели, оставив всё постоянное на своих местах.
2. В окне **Preprocessing Setup** задайте новое **Set name** и нажмите
**Start S21 Reference**, затем снимите пары и сохраните набор.
На месте работы, когда сцена изменилась (новое место, переставили радар), снимите
временный референс: кнопка **Capture Temporary Reference**, та же кнопка в
веб-интерфейсе или GPIO-кнопка. Сбор приостанавливается на время съёма и
продолжается уже с новым референсом.
## Наблюдение
1. Выберите режим обработки в **Mode**: B-scan — дальность во времени для одной
пары, GPR — карта земли с объектами, 3D SAR — объём над землёй.
2. Нажмите **Start**.
3. Настраивайте режим на ходу: изменения применяются сразу и пересчитывают последний
результат (глава [Режимы обработки](processing.md)). Для GPR и 3D SAR задайте
высоту радара и угол обзора, как на установке.
4. Если радар движется, скорость передаёт клиент локатора; без него включите
**Ignore socket speed** и задайте скорость вручную.
5. Нажмите **Save Config…**, когда настройки подобраны.
## Сохранение данных
- Сохранить то, что уже есть в памяти (до 1000 коллекций каждой стадии): задайте
**Path** и **Name**, нажмите **Save Last 1000**.
- Записать следующие N коллекций: задайте **Sweeps to record**, **Path** и **Name**,
нажмите **Start + Record to Disk**. Ход записи виден в журнале и в веб-интерфейсе.
- Нужны развёртки S21 в JSON для других программ — **Save S21 JSON**.
Набор данных — новый каталог в **Path**; формат — в главе
[Данные на диске](data.md).
Если последняя коллекция испорчена (например, в кадр попал человек), нажмите
**Remove Last Measurement**: она удалится из истории в памяти. Уже записанные на диск
файлы останутся, а идущая запись на диск при этом завершится.
## Калибровка по объекту
Нужна, когда изображение GPR или 3D SAR размыто из-за того, что пары не совпадают по
фазе.
1. Поставьте металлический шар в сцену и измерьте положение его центра.
2. Нажмите **Preprocessing…**, затем **Create…** рядом с **Object calibration**.
3. Нажмите **Capture Data** и впишите X, Y, Z центра шара. Повторите для других
положений, если нужно.
4. Нажмите **Calculate and Save** и проверьте карты фаз **Before** и **After**.
5. Перезапустите сбор (**Stop**, затем **Start**): поправка применяется со следующего
запуска.
Подробно — в главе [Калибровка и предобработка](preprocessing.md).
## Работа без оператора
1. Настройте всё на самой машине с экраном: профиль, наборы, режим, и нажмите
**Save Config…**.
2. Установите службу: `sudo bash deploy/install-daemon.sh`, затем
`sudo systemctl start radar.service`.
3. Управляйте через браузер (`http://<машина>:8080/`), GPIO-кнопку и локатор.
Глава [Веб-интерфейс, кнопка и локатор](web.md), раздел «Работа без экрана».
## Обновление программы
1. На рабочей станции закоммитьте изменения и выполните `deploy/pack-usb.sh` (путь к
флешке можно не указывать, если подключена одна).
2. На машине закройте окно программы и из корня флешки выполните
`bash radar_update/update.sh`.
3. Проверьте `cat ~/Documents/radar_system/RELEASE`.
Глава [Установка и запуск](installation.md), раздел «Обновление с флешки».
## Окончание работы
1. Нажмите **Stop**.
2. Нажмите **Save Config…**, если меняли настройки или наборы.
3. Закройте окно: запись на диск дописывается, процессы конвейера останавливаются.