Скрипты и декодеры

Содержимое scripts/ копируется в /usr/local/bin образа и делается исполняемым. Это точки расширения станции: всё, что не влезает в основной конвейер — дополнительные декодеры, обзор диапазона, управление реле — живёт здесь, а не в Python-коде клиента.

Важно

Новый декодер подключают сюда, а не в src/. Клиент вызывает всего два скрипта, satnogs-pre и satnogs-post, а те уже разветвляются дальше.

Два хука

Клиент запускает их до и после прохода с одинаковым набором аргументов:

satnogs-pre  {{ID}} {{FREQ}} {{TLE}} {{TIMESTAMP}} {{BAUD}} {{SCRIPT_NAME}}
satnogs-post {{ID}} {{FREQ}} {{TLE}} {{TIMESTAMP}} {{BAUD}} {{SCRIPT_NAME}}

$1

$2

$3

$4

$5

$6

ID наблюдения

Частота, Гц

TLE

Метка времени

Скорость

Имя скрипта графа

Отключаются переменными OBSERVATION__RUN_PRE_OBSERVATION_SCRIPT и OBSERVATION__RUN_POST_OBSERVATION_SCRIPT; выключение pre-хука отключает все внешние декодеры разом.

satnogs-pre — останавливает обзор диапазона (SDR нужен для приёма), запускает grsat.py и meteor.sh, при GPIO_ENABLE дёргает gpio.py с частотой прохода.

satnogs-post — останавливает те же декодеры, запускает декодирование SSTV, переименовывает IQ-дамп, возвращает обзор диапазона и паркует ротатор.

Обратите внимание на порядок в satnogs-post: bandscan.sh start идёт после остановки декодеров, потому что обзор снова займёт приёмник.

Примечание

Фоновый процесс, запущенный из хука, наследует его stdout и продолжает писать в тот же пайп после того, как сам хук завершился, — так делает bandscan.sh start, живущий до bandscan.sh stop следующего прохода. Клиент это учитывает: пайп закрывает поток-читатель, а не вызывающий, иначе уборка после satnogs-post ждала бы смерти обзора диапазона и откладывала бы построение водопада и выгрузку данных до следующего прохода. Ждать вывода клиент готов не дольше subprocess_log.JOIN_TIMEOUT_IN_SECONDS, дальше чтение продолжается в фоне. Если вашему скрипту нужен собственный лог, перенаправляйте вывод фонового процесса в файл — как делает grsat.py.

Декодирование телеметрии

grsat.py

Главный скрипт декодирования — обёртка над gr_satellites, работающая параллельно основному потоковому графу. Собирает декодированные кадры, пишет JSON-телеметрию в директорию наблюдения (её подхватит наблюдатель файловой системы и отправит на портал), а также передаёт кадры декодерам изображений и SSTV.

Режимы вызова: start, stop, stream, sstv.

start запускает gr_satellites только если клиент выбрал его декодером прохода: переменная SONIKS_DECODER (gr-satellites либо satnogs) выставляется клиентом в окружение satnogs-pre/satnogs-post на каждый проход по наличию satyaml для NORAD (sat_data.decoder_for(), Фаза 3.2 дорожной карты сети). Без satyaml gr_satellites раньше запускался на каждом проходе и умирал молча — его вывод идёт в DEVNULL. Без переменной (ручной запуск) скрипт ведёт себя как прежде. stop и sstv от выбора не зависят: первый терпит отсутствие pid- и kiss-файлов, второй — отдельный декодер картинок.

Кадры уходят потоком, во время прохода (Фаза 3.1 дорожной карты сети). start запускает gr_satellites с двумя выходами KISS: файлом --kiss_out и TCP-сервером --kiss_server на порту 8100, — и фоном grsat.py stream. Тот подключается к серверу, пишет каждый кадр файлом data_* сразу, как он пришёл, и выходит по EOF, когда stop гасит gr_satellites. Файлы подхватывает наблюдатель файловой системы клиента — кадр оказывается на портале через OBSERVATION__BATCH_DELAY, а не после прохода.

stop после этого разбирает --kiss_out целиком, как и раньше: это страховка на случай, если поток не подключился или умер. Кадры, уже записанные потоком, запишутся второй раз под суффиксом _g1, но портал отсеивает повтор по sha256 содержимого внутри наблюдения — ответ 200 без записи. Сам --kiss_out читать по мере записи нельзя: GNU Radio file_sink буферизует и сбрасывает хвост только при закрытии, а SIGKILL его теряет.

--start_time не передаётся намеренно: он для записей, проигрываемых с --throttle. На живом UDP метка кадра считалась бы от момента сборки графа gr_satellites и уезжала назад на время подготовки прохода; без него метка — реальное UTC часов станции.

Переменные: GRSAT_LOG_LEVEL, GRSAT_KEEPLOGS, GRSAT_ZMQ_PORT; SONIKS_DECODER ставит клиент, в .env её не задают.

sat.cfg

Per-NORAD переопределения аргументов gr_satellites — по одной строке на аппарат:

35933 --clk_bw 0.3
46276 --disable_dc_block --deviation 500 --clk_bw 0.15

Здесь место точечным исправлениям для конкретных спутников, у которых стандартные параметры демодуляции не работают.

Файл живёт в soniks-satyaml вместе с satyaml: это данные спутников, и меняются они теми же правками. На станцию он приезжает sat-data bundle’ом с портала, см. Поддерживаемые спутники; в образ попадает через сабмодуль — как запас на первый старт, до первого похода за bundle.

gr_satellites читает его строго из ~/.gr_satellites/sat.cfg, а $HOME станции (/var/lib/soniks-client) перекрыт именованным томом. Кладут файл туда двое: grsat.py копирует его из образа, если в томе пусто, а sync_sat_data() обновляет из bundle. Правки оператора не затираются: bundle переписывает файл, только если тот совпадает с прошлым доставленным, иначе оставляет как есть и пишет предупреждение в лог. Чтобы принять версию из bundle, удалите файл в томе и перезапустите контейнер.

imagedecode.py

Сборка изображений из телеметрических кадров. Класс ImageDecode выбирает декодер по NORAD ID; каждый декодер объявляет свой список supported_norad:

Декодер

Аппараты

StratosatDecode

Geoscan-Edelveis, StratoSat

GeoscanDecode

Geoscan

SputnixDecode

Sputnix

MarinaDecode

MARINA

Cas5aDecode

CAS-5A

Lucky7Decode, SharjahsatDecode

Lucky-7, Sharjahsat-1. Подключены к диспетчеру, но помечены в коде WIP: разбор кадров починен, сборка изображения на реальных данных не подтверждена.

Добавление аппарата к существующему формату — это дописать его NORAD ID в supported_norad нужного класса. Новый формат — новый подкласс плюс ветка выбора.

sstv_wrapper.py

Декодирование SSTV для захардкоженного списка NORAD ID, обёртка над CLI sstv. Вызывается из satnogs-post через grsat.py sstv.

Сам декодер — пакет из сабмодуля sstv/, в образ ставится из него же: версию фиксирует указатель сабмодуля в этом репозитории. Правка декодера — коммит в сабмодуль, затем коммит здесь с новым указателем.

Как декодируется изображение (после поиска заголовка и VIS):

  • частота — ЧМ-дискриминатор: полоса 1000–2500 Гц, аналитический сигнал, разность фаз соседних отсчётов. Прежняя оценка по пику короткого БПФ ошибалась в яркости на десятки ступеней даже на чистом тоне;

  • синхро — одна прямая по синхроимпульсам всех строк с отсевом выбросов: строка с синхро в шуме не сдвигается, дрейф часов правится наклоном;

  • сглаживание — окно Ханна, которое удлиняется (до ×4) по шуму, измеренному внутри синхроимпульсов. Robot 36 выбирает фазу цвета голосованием по разделителям строк.

В лог (GRSAT_KEEPLOGS) декодер пишет строку Sync: — число строк с чистым синхро, дрейф в ppm, джиттер, шум и выбранное окно: по ней видно, что было с сигналом.

Внешние конвейеры

meteor.sh

Демодуляция и декодирование METEOR 2-3 / 2-4 из файла IQ-дампа (не из UDP). Работает только при FLOWGRAPH__ENABLE_IQ_DUMP=True. Список аппаратов — METEOR_NORAD (по умолчанию 57166 59051).

bandscan.sh

Широкополосный обзор диапазона в простое, через rx_sdr и rffft из strf. Запускается и останавливается хуками, чтобы не конкурировать за приёмник. Число каналов БПФ подбирается по частоте автоматически: 20 для VHF, 50 для UHF, 100 для S-диапазона.

Требует BANDSCAN_FREQ и реального диска под BANDSCAN_DIR — по умолчанию там /srv/bandscan, и писать туда в tmpfs — плохая идея.

Вспомогательные

Скрипт

Что делает

liveupdate-satyaml.sh

Точка входа контейнера: exec python3 /src/main.py. Имя историческое — клоны satyaml из git отсюда убраны, определения приезжают bundle’ом (см. Спутники), а переименовать файл нельзя: на нём стоит command: в compose всего парка.

iq_dump_rename.sh

Переименовывает IQ-дамп в <файл>_<id>_<samplerate>.raw и опционально жмёт zstd.

find_samp_rate.py

Считает частоту дискретизации IQ по скорости и имени скрипта графа. Портирован из satnogs.

rotor-park.sh

Отправляет ротатору P 180 90 после прохода. Включается ROT_PARK.

gpio.py

Управление реле (МШУ, выбор антенны, PA, ротатор) через MCP2221 в зависимости от частоты. Исключён из проверки ruff.

kiss.py

Разбор KISS gr_satellites: кадры и метки времени. Общий для grsat.py (файл и поток --kiss_server) и imagedecode.py. Экранирование снимается до проверки метки: gr_satellites экранирует и её.

test-flowgraph.sh

Ручной прогон потокового графа — проверка SDR + GNU Radio без ожидания прохода. Вызывается как test-flowgraph.sh [MODE] [FREQ] [BAUD] и идёт через тот же flowgraph_dispatcher, что и клиент, поэтому проверяет именно выбранный режим. Конфигурация берётся станционная: FLOWGRAPH__RX_SAMP_RATE обязательна, --gain передаётся только при заданном FLOWGRAPH__RF_GAIN, а приёмник выбирается по частоте той же функцией, что и на проходе.

Ещё две вещи в scripts/ — не скрипты. libresdr/usrp_b210_fpga.bin — прошивка FPGA для LibreSDR: при сборке образа она подменяет штатную в /usr/share/uhd, а из /usr/local/bin удаляется.

gnuradio/vmcircbuf_default_factory задаёт реализацию кольцевого буфера GNU Radio, но лежит не по назначению: COPY scripts/* в Dockerfile разворачивает поддиректории плоско, поэтому файл оказывается прямо в /usr/local/bin/. GNU Radio по этому пути его не читает — то есть сейчас настройка ни на что не влияет. Куда именно его класть, проверяется только запущенным GNU Radio; пункт открыт в дорожной карте.

Соглашения

  • Скрипты запускаются от пользователя 9999, не от root.

  • Рабочая директория наблюдения — $PATHS__BASE/$PATHS__OUTPUT_DIR (в контейнере /var/lib/soniks-client/data/output). Всё, что положено туда с префиксом data_, будет выгружено на портал.

  • Скрипты читают переменные окружения напрямую, минуя модель настроек. Они не валидируются на старте: опечатка не выдаст ошибку, функция просто молча не включится.

  • Признак «включено» — регулярное выражение (TRUE|YES|1) без учёта регистра.

  • Имена переменных с префиксом секции сверяются с полями Settings автотестом tests/test_script_env_names.py: потерянный префикс или опечатка роняют тест, а не «просто молча не работают».