Архитектура

Слои

soniks-client-new
      │  один вызов: flowgraph_dispatcher --mode <MODE> …
      ▼
┌─────────────────────────────────────────────────────────────┐
│ flowgraph_dispatcher.py            ← Python, слой управления │
│   • таблица режимов → скрипт                                 │
│   • переписывание аргументов                                 │
│   • запуск и остановка 2–4 процессов                         │
└───────┬──────────────────────────────────┬──────────────────┘
        │ subprocess                       │ subprocess ×3
        ▼                                  ▼
┌────────────────────┐            ┌──────────────────────┐
│ satnogs_<mode>.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 и передаёт их как есть:

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, но ни к чему внутри не подключены. Они существуют только для того, чтобы флоуграф не упал, когда диспетчер передаст их в общем цикле. См. Рудиментарные параметры.

Порты и форматы описаны в Выходные данные. Все три выводятся из одного числа: --zmq-base-port у диспетчера и параметр zmq_base_port в .grc. Кадры уходят на base, IQ на base + 1, аудио на base + 2:

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 при отказе; подробности в Завершение.

Что где лежит

Путь

Назначение

Когда трогать

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, Добавление флоуграфа и режима.