Потоковый граф¶
Демодуляция целиком за пределами Python-кода: клиент запускает
flowgraph_dispatcher — исполняемый файл из
soniks-flowgraphs,
собранный в образ. Задача класса Flowgraph
(src/soniks_client/flowgraph.py) — построить командную строку, запустить
процесс, пробросить его вывод в лог и корректно остановить.
Как строится командная строка¶
Все параметры лежат в словаре self.parameters, собираемом в конструкторе, и
превращаются в аргументы механически:
args = [self.dispatcher_script]
for parameter, value in self.parameters.items():
if value is not None:
args.append(f"--{parameter}={value}")
Два следствия:
None= аргумент не передаётся вовсе. Поэтому вFlowgraphSettingsстолько полей типаX | None = None: это способ сказать «не трогай, пусть граф решает сам».Имя поля настроек и имя аргумента связаны вручную, через словарь. Это не автоматический
kebab-caseот имени переменной: например,settings.flowgraph.RX_SAMP_RATEуезжает в--samp-rate-rx, аRF_GAIN— в--gain.
Часть параметров приходит не из настроек, а из самого прохода: mode,
rx-freq, baud, norad-cat-id, пути к файлам. baud и norad-cat-id
добавляются только если заданы.
Как добавить параметр графа¶
Добавьте поле в
FlowgraphSettings(src/core/configs/flowgraph.py).Добавьте строку в словарь
self.parametersвFlowgraph.__init__с тем именем аргумента, которое понимаетflowgraph_dispatcher.Опишите переменную в справочнике.
Примечание
Оба bool уезжают как 0/1: flowgraph_dispatcher объявляет и
--dc-removal, и --enable-iq-dump как type=int. До 2026-09-12 dc-removal
передавался сырым значением, то есть --dc-removal=True, а int("True")
роняет диспетчер с кодом 2 ещё до старта графа — FLOWGRAPH__DC_REMOVAL=true
означало пустой проход. Сверено с тегом 2.6.0 (Фаза 3.6 в
дорожной карте сети).
Соответствие режимов¶
Два словаря в src/core/configs/flowgraph.py связывают строку режима,
присланную порталом, с конкретным скриптом satnogs. Это константы модуля,
а не поля настроек: через окружение они не задаются.
SCRIPTS— символическое имя → имя файла скрипта ("FSK": "satnogs_fsk.py").MODES— режим портала →{script_filename}.FSK,GFSK,GMSK,MSKи варианты AX.100 указывают на одинsatnogs_fsk.py: кадровую структуру различает диспетчер по--mode, клиенту она не нужна.
Примечание
script_filename не справочный, хотя выбор графа от него и не зависит: имя
уходит шестым аргументом в satnogs-pre/satnogs-post, а оттуда в
find_samp_rate.py, который по подстрокам в имени определяет частоту
дискретизации IQ. См. врезку про SSTV_PD120 ниже.
До 2026-09-12 в таблице лежали ещё is_baudrate и framing — без единого
читателя; сняты (Фаза 3.6 в дорожной карте сети).
Если портал прислал неизвестный режим, исход зависит от того, знает ли портал
режимы станции. Пока клиент их не опубликовал (POST /api/v2/stations/<id>/status/, раз на процесс после первой успешной сверки
расписания), берётся FLOWGRAPH__DEFAULT_MODE (по умолчанию FM) с
предупреждением в лог. После публикации портал такой режим станции не
планирует, и если он всё же пришёл — проход не начинается, в логе ошибка.
SSTV ведёт на satnogs_fm.py намеренно: в форке этот режим только пишет
аудио, а картинку собирает библиотека sstv из sstv_wrapper.py уже после
прохода.
Примечание
До версии 2.2.2 MODES["SSTV_PD120"] тоже брал SCRIPTS["SSTV"], то есть
satnogs_fm.py. На выбор графа это не влияло — граф выбирает диспетчер по
--mode, и его таблица всегда указывала на satnogs_sstv_pd120_demod.py.
Ломалось другое: имя скрипта уходит в satnogs-pre/satnogs-post, а оттуда в
scripts/find_samp_rate.py, который ищет в нём подстроку _sstv. Без неё
функция возвращала 48000 вместо 4·4160·4 = 66560 — реальной out_samp_rate
графа. Следствий было два, оба тихие: gr-satellites получал UDP-поток IQ,
размеченный неверной частотой, а IQ-дамп — неверную частоту в имени файла.
Как добавить режим демодуляции¶
Нужны обе части, иначе не заработает:
Запись в конфигурации. Добавьте скрипт в
SCRIPTSи режим вMODESвsrc/core/configs/flowgraph.py. Ключ вMODESдолжен буква в букву совпадать со строкой режима, которую присылает портал.Скрипт в образе. Файл
satnogs_*.pyдолжен присутствовать в контейнере — он приходит изgr-satnogsилиsoniks-flowgraphs. Если его там нет, потребуется правкаDockerfileили версии соответствующего компонента.
Проверять — scripts/test-flowgraph.sh внутри контейнера: он гоняет граф без
ожидания реального прохода.
Совет
Дописать режим в MODES, не убедившись в наличии скрипта, — самая частая
ошибка. Симптом: наблюдение стартует, граф умирает мгновенно, в логе строка
про отсутствующий файл.
Жизненный цикл процесса¶
Flowgraph.start() # Popen, stdout+stderr в одну трубу, поток-логгер
Flowgraph.is_running # process.poll() is None
Flowgraph.stop() # SIGINT → wait(10 c) → kill
Остановка сигналом SIGINT, а не SIGKILL, — принципиально: по нему GNU Radio
успевает закрыть выходные файлы. Убийство процесса оставит повреждённый
payload.ogg и обрезанный .dat водопада. Через 10 секунд ожидания процесс всё
же убивается принудительно, с предупреждением в логе.
Вывод графа читает демон-поток _log_output и пишет его построчно с префиксом
[Flowgraph] на уровне LOG__FLOWGRAPH_LEVEL. На DEBUG строк очень много.
Метаданные¶
get_metadata() возвращает блок, который уезжает на портал вместе с
результатами наблюдения:
{"radio": {"name": "gr-satnogs",
"version": gr_satnogs_git_hash[:8],
"flowgraphs": flowgraphs_git_hash[:8],
"gr_satellites": gr_satellites_git_hash[:8],
"sat_data": applied_version(), # версия bundle, None — из образа
"exit_code": self.exit_code,
"mode_known": mode in MODES,
"decoder": "gr-satellites" | "satnogs", # дописывает Observation
"parameters": self.parameters}}
decoder — единственный выбор декодера на проход (Фаза 3.2 дорожной карты
сети): Observation спрашивает sat_data.decoder_for(norad) — есть satyaml
для NORAD в каталоге пакета satellites → gr-satellites, нет → satnogs.
NORAD берётся из norad_cat_id портала, а без него — из второй строки TLE.
То же значение уезжает скриптам прохода переменной SONIKS_DECODER, по ней
grsat.py start решает, запускать ли gr_satellites вовсе. Ключ кладётся в
radio рядом с mode_known, а не в parameters: те целиком превращаются в
argv диспетчера.
То есть на портал уходит полный набор параметров графа. Это удобно для
разбора неудачных приёмов, но означает, что случайно добавленный в
self.parameters секрет окажется в публичных метаданных наблюдения.
exit_code — код возврата, если граф кончился сам, до того как его
попросили остановиться; None означает штатный проход. Без него упавший граф
неотличим от успешного наблюдения: клиент код возврата не читал вовсе, и проход,
на котором диспетчер вышел по argparse (код 2, расхождение контракта), выглядел
просто как наблюдение с пустой директорией. Судить по process.returncode
нельзя — на штатном пути там всегда -2 от SIGINT.
Выходные файлы¶
Граф пишет три вещи, пути к ним конструирует Observation:
Файл |
Аргумент |
Что это |
|---|---|---|
|
|
Аудиозапись прохода |
|
|
Сырой водопад, см. Водопад |
|
|
Декодированные кадры (префикс) |
Кадры data_* подхватывает наблюдатель файловой системы и отправляет на портал
не дожидаясь конца прохода — см. Загрузку данных.
Дополнительно граф может отдавать IQ: в файл (--enable-iq-dump,
--iq-file-path) и в UDP-поток (--udp-dump-host, --udp-dump-port).
UDP-поток читает grsat.py — по нему кадры уходят декодеру gr_satellites,
см. Скрипты.