# Архитектура ## Слои ``` soniks-client-new │ один вызов: flowgraph_dispatcher --mode … ▼ ┌─────────────────────────────────────────────────────────────┐ │ flowgraph_dispatcher.py ← Python, слой управления │ │ • таблица режимов → скрипт │ │ • переписывание аргументов │ │ • запуск и остановка 2–4 процессов │ └───────┬──────────────────────────────────┬──────────────────┘ │ subprocess │ subprocess ×3 ▼ ▼ ┌────────────────────┐ ┌──────────────────────┐ │ satnogs_.py │ ──ZMQ──▶ │ zmq_frame_decoder_sub│ │ (сгенерирован grcc │ │ zmq_iq_sub │ │ из .grc) │ │ zmq_audio_sub │ └────────────────────┘ └──────────┬───────────┘ ▼ файлы ``` Слои общаются тремя способами, и все три — контракты, которые легко сломать незаметно: 1. **Аргументы командной строки** между диспетчером и флоуграфом; 2. **ZMQ-сокеты на localhost** между флоуграфом и подписчиками; 3. **Файлы** между подписчиками и клиентом станции. ## Контракт 1: `.grc` — источник правды Файлы `.py` флоуграфов **генерируются `grcc` при сборке и не коммитятся** (`generic/*.py` и `satellites/*.py` перечислены в `.gitignore`). Править нужно `.grc`, а не сгенерированный Python: любая правка `.py` исчезнет при следующей сборке. Каждый `.grc` — это YAML. Значимые для нас части: - блоки `id: parameter` — становятся аргументами `--имя-через-дефис` сгенерированного скрипта; - блоки `id: variable` — вычисляются внутри, снаружи недоступны; - блок `id: snippet` — вставка произвольного Python в заданную точку (используется, чтобы передать блоку доплер-компенсации адрес `rigctld`); - секция `connections` — рёбра графа. Списки собираемых файлов в `CMakeLists.txt` **явные, не по маске**. Новый `.grc`, не добавленный в `set(flowgraphs …)`, просто молча не соберётся. ## Контракт 2: имена аргументов Диспетчер не знает, какие параметры есть у конкретного флоуграфа. Он берёт все свои аргументы со значением не `None` и передаёт их как есть: ```python args = [script] + [f'--{key}={value}' for key, value in self.parameters.items() if value is not None] ``` Отсюда жёсткое требование: **имя аргумента в `client_argument_parser()` должно совпадать с именем блока `parameter` в `.grc`** (дефисы против подчёркиваний — единственное допустимое расхождение, `grcc` делает эту замену сам). Что произойдёт при расхождении: - аргумент есть у диспетчера, но нет во флоуграфе → `grcc` генерирует `parse_args()`, поэтому флоуграф немедленно завершается с кодом 2; - параметр есть во флоуграфе, но нет у диспетчера → используется значение по умолчанию из `.grc`, тихо и без предупреждения. Второй случай коварнее: наблюдение выглядит успешным, а декодер работает не на той скорости. Оба ловит `tests/test_dispatcher.py` — он строит командную строку для каждого режима и сверяет её с блоками `parameter` целевого `.grc`. ## Контракт 3: ZMQ и владение файлами Флоуграф **не пишет** ни кадры, ни аудио, ни IQ — он публикует их в сокеты. Записью владеют подписчики. Единственное исключение — водопад: его пишет сам флоуграф блоком `satnogs_waterfall_sink`. Практическое следствие: параметры `decoded_data_file_path`, `file_path`, `iq_file_path` и `enable_iq_dump` объявлены почти в каждом `.grc`, но ни к чему внутри не подключены. Они существуют только для того, чтобы флоуграф не упал, когда диспетчер передаст их в общем цикле. См. [](grc-conventions.md#рудиментарные-параметры). Порты и форматы описаны в [](../outputs.md). Все три выводятся из одного числа: `--zmq-base-port` у диспетчера и параметр `zmq_base_port` в `.grc`. Кадры уходят на `base`, IQ на `base + 1`, аудио на `base + 2`: ```yaml address: '"tcp://127.0.0.1:" + str(zmq_base_port + 2)' ``` Литеральных адресов в `.grc` быть не должно — `tests/test_flowgraphs.py` проверяет это и заодно сверяет смещение с типом стока. ## Порядок запуска Подписчики стартуют раньше флоуграфа. Причина — семантика ZMQ pub/sub: издатель не буферизует сообщения для ещё не подключившихся подписчиков, и всё, что опубликовано до подписки, теряется безвозвратно. Порядок: 1. подписчик кадров (если у режима есть декодер); 2. подписчик IQ (если `--enable-iq-dump`); 3. подписчик аудио (всегда); 4. флоуграф. Дальше `main()` опрашивает процесс флоуграфа каждые 250 мс и возвращает его код завершения. Остановка — по `SIGINT` или `SIGTERM`: сначала флоуграф, затем все подписчики разом, каждому 2 с на выход и `SIGKILL` при отказе; подробности в [](../dispatcher.md#завершение). ## Что где лежит | Путь | Назначение | Когда трогать | |---|---|---| | `generic/*.grc` | флоуграфы общего назначения | новая модуляция или общий протокол | | `satellites/*.grc` | декодеры под конкретные спутники | новый спутник | | `test_flowgraphs/*.grc` | генераторы сигнала, **вне сборки** | отладка декодера без эфира | | `tools/sync_frontend.py` | раскладка общего фронтенда из `generic/fm.grc` | правка приёмного тракта | | `tests/*.py` | инварианты `.grc` и контракт аргументов | вместе с любой правкой кода | | `flowgraph_dispatcher/flowgraph_dispatcher.py` | таблицы режимов, разбор аргументов, управление процессами | новый режим, новый аргумент | | `flowgraph_dispatcher/zmq_*_sub.py` | запись кадров, IQ, аудио | изменение формата выходных файлов | | `flowgraph_dispatcher/zmq_subs/helper_functions/` | общие разборщики аргументов, обработчик сигналов, метки времени | общая для подписчиков логика | | `*/CMakeLists.txt` | явные списки собираемых файлов | добавление или удаление `.grc` | | `debian/` | метаданные пакета | изменение зависимостей | | `docs/` | эта документация | вместе с любым изменением поведения | ## Чего в проекте нет - Python-упаковки (`setup.py`, `pyproject.toml`) — точки входа создаются символическими ссылками из CMake; - генерации API-документации из кода; - сборки `.deb` в CI. Тесты (`tests/`) и линтер (`flake8`, настройки в `setup.cfg`) есть и выполняются в CI. Тесты работают на чистом PyYAML, без GNU Radio. Дальше: [](grc-conventions.md), [](adding-flowgraph.md).