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, без видеокарты.