204 lines
19 KiB
Markdown
204 lines
19 KiB
Markdown
# Устройство программы
|
||
|
||
## Процессы и кольца
|
||
|
||
Программа — четыре процесса (с АЦП 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, без видеокарты.
|