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