# Скрипты и декодеры Содержимое `scripts/` копируется в `/usr/local/bin` образа и делается исполняемым. Это точки расширения станции: всё, что не влезает в основной конвейер — дополнительные декодеры, обзор диапазона, управление реле — живёт здесь, а не в Python-коде клиента. ```{important} Новый декодер подключают **сюда**, а не в `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` идёт после остановки декодеров, потому что обзор снова займёт приёмник. ```{note} Фоновый процесс, запущенный из хука, наследует его `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](https://gitlab.com/space-education-development/soniks/client/soniks-satyaml) вместе с satyaml: это данные спутников, и меняются они теми же правками. На станцию он приезжает sat-data bundle'ом с портала, см. [Поддерживаемые спутники](../station/satellites.md); в образ попадает через сабмодуль — как запас на первый старт, до первого похода за 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](https://github.com/cbassa/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'ом (см. [Спутники](../station/satellites.md)), а переименовать файл нельзя: на нём стоит `command:` в compose всего парка. | | `iq_dump_rename.sh` | Переименовывает IQ-дамп в `<файл>__.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; пункт открыт в [дорожной карте](../roadmap.md). ## Соглашения * Скрипты запускаются от пользователя `9999`, не от root. * Рабочая директория наблюдения — `$PATHS__BASE/$PATHS__OUTPUT_DIR` (в контейнере `/var/lib/soniks-client/data/output`). Всё, что положено туда с префиксом `data_`, будет выгружено на портал. * Скрипты читают переменные окружения **напрямую**, минуя модель настроек. Они не валидируются на старте: опечатка не выдаст ошибку, функция просто молча не включится. * Признак «включено» — регулярное выражение `(TRUE|YES|1)` без учёта регистра. * Имена переменных с префиксом секции сверяются с полями `Settings` автотестом `tests/test_script_env_names.py`: потерянный префикс или опечатка роняют тест, а не «просто молча не работают».