Files
radar_system/docs/architecture.md
T
2026-10-16 18:35:02 +03:00

204 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Устройство программы
## Процессы и кольца
Программа — четыре процесса (с АЦП 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, без видеокарты.