soniks_client

Ядро клиента: точка входа, наблюдение, потоковый граф, обмен с порталом, водопад.

Точка входа

Точка входа клиента.

Поднимает планировщик, регистрирует постоянные задания и следит за ними: при исключении планировщик перезапускается, чтобы сломанный проход не остановил станцию.

class main.Application(scheduler_factory)[исходный код]

Базовые классы: object

Супервизор планировщика.

Ловит исключения планировщика и перезапускает его, чтобы сломанный проход не остановил станцию. Завершается по SIGINT/SIGTERM.

Параметры:

scheduler_factory (Callable[[], BaseScheduler])

run()[исходный код]

Запустить планировщик и держать его живым до остановки.

Тип результата:

None

stop()[исходный код]

Остановить планировщик и завершить работу приложения.

Тип результата:

None

main.main()[исходный код]
Тип результата:

None

Планировщик

Фабрика BackgroundScheduler (UTC), собранного из настроек.

soniks_client.scheduler.create_scheduler()[исходный код]

Собрать новый планировщик.

Именно фабрика, а не общий экземпляр: APScheduler после shutdown() навсегда гасит пул потоков исполнителя, поэтому перезапуск того же объекта возвращает планировщик, который принимает задания и не выполняет их.

Тип результата:

BackgroundScheduler

soniks_client.scheduler.find_next_observation(scheduler)[исходный код]

Найти ближайшее запланированное наблюдение.

Результат:

Задание наблюдения с самым ранним началом или None, если проходов не запланировано.

Параметры:

scheduler (BaseScheduler)

Тип результата:

Job | None

Здоровье

Состояние станции: GET /healthz и его сборка.

Процесс с мёртвым планировщиком снаружи неотличим от здорового — эндпоинт закрывает именно это. Отдаёт состояние планировщика, время последней успешной сверки расписания, глубину очереди incomplete/, признак идущего прохода и время следующего.

Ходят сюда двое: HEALTHCHECK образа и агент управления парком из соседнего контейнера.

soniks_client.health.mark_sync_success()[исходный код]

Отметить успешно завершённый цикл сверки расписания.

Тип результата:

None

soniks_client.health.set_clock_skew(seconds)[исходный код]

Запомнить расхождение часов станции с порталом.

Плюс — часы станции спешат. None — портал не прислал Date.

Параметры:

seconds (int | None)

Тип результата:

None

soniks_client.health.set_config_generation(generation)[исходный код]

Запомнить поколение конфигурации с портала, применённое станцией.

Параметры:

generation (int)

Тип результата:

None

soniks_client.health.observation_running()[исходный код]

Идёт ли сейчас хотя бы один проход.

Тип результата:

bool

soniks_client.health.seconds_to_next_observation()[исходный код]

Сколько секунд до ближайшего прохода; None — проходов нет.

По этому окну обследование приёмника решает, можно ли его занять (soniks_client.sdr_survey). Идущий проход — ноль.

Тип результата:

float | None

soniks_client.health.observation_started(job_id)[исходный код]

Отметить начало прохода.

Множество, а не флаг и не мьютекс: наложившиеся проходы разрешены осознанно (лок на них отклонён в дорожной карте техдолга), и вложенный проход не должен гасить признак у соседнего.

Параметры:

job_id (str)

Тип результата:

None

soniks_client.health.observation_finished(job_id)[исходный код]

Отметить завершение прохода.

Параметры:

job_id (str)

Тип результата:

None

soniks_client.health.snapshot(get_scheduler)[исходный код]

Собрать состояние станции и код ответа.

Результат:

503, если планировщик не запущен или потеряно задание сверки расписания.

Тип результата:

Тело ответа и код HTTP

Параметры:

get_scheduler (Callable[[], BaseScheduler | None])

soniks_client.health.start_health_server(get_scheduler)[исходный код]

Поднять /healthz в фоновом потоке.

Параметры:

get_scheduler (Callable[[], BaseScheduler | None]) – Колбэк, а не сам планировщик: супервизор подменяет экземпляр при перезапуске, и захваченная ссылка устарела бы.

Результат:

Запущенный сервер или None, если порт занять не удалось.

Тип результата:

ThreadingHTTPServer | None

Наблюдение

Жизненный цикл прохода целиком — в Архитектуре.

Наблюдение одного прохода: подготовка, приём, постобработка.

class soniks_client.observation.Observation(rig_controller, rotator_controller=None)[исходный код]

Базовые классы: object

Наблюдение одного прохода спутника.

Выбирает приёмник по частоте, строит пути файлов и потоковый граф, ведёт сессии слежения и доплеровской коррекции, а после прохода строит водопад и отправляет метаданные.

Параметры:
property observation_directory: str

Директория файлов текущего наблюдения.

set_observation_parameters(observation_id, tle, observation_start, observation_end, frequency, mode, baud, norad_cat_id=None, mode_requested=None)[исходный код]

Подготовить наблюдение к проходу.

Выбирает приёмник по частоте, строит пути выходных файлов и конструирует потоковый граф.

Исключение:

NoCompatibleRxDeviceError – Нет приёмника под частоту прохода.

Параметры:
  • observation_id (str)

  • tle (dict[str, str])

  • observation_start (datetime)

  • observation_end (datetime)

  • frequency (int)

  • mode (str)

  • baud (int)

  • norad_cat_id (int | None)

  • mode_requested (str | None)

Тип результата:

None

run_pre_script()[исходный код]

Выполнить скрипт, запускаемый перед проходом (satnogs-pre).

Тип результата:

None

run()[исходный код]

Провести проход: слежение, доплеровская коррекция, потоковый граф.

Завершается по окончании окна наблюдения или при смерти графа. Оборудование останавливается в любом случае: без finally любое исключение оставляло GNU Radio жить дальше, а потоки rig и rotator — крутить антенну в следующий проход.

Тип результата:

None

run_post_script()[исходный код]

Выполнить скрипт, запускаемый после прохода (satnogs-post).

Тип результата:

None

post_processing()[исходный код]

Построить водопад, собрать метаданные и положить их в очередь выгрузки.

Метаданные не отправляются отсюда: файл забирает та же выгрузка, что и аудио с кадрами. Раньше это был единственный «выстрелил и забыл» путь — при ошибке сети блок signal (SNR, девиация, ppm) исчезал навсегда, очереди для него не было.

Тип результата:

None

Части, не зависящие от состояния наблюдения и от железа, вынесены из класса отдельными модулями — так они проверяются тестами напрямую, без подделки self. Сам Observation остался оркестратором.

Выбор приёмника SDR по частоте прохода.

OBSERVATION__SOAPY_RX_DEVICE может содержать либо одну строку драйвера, либо несколько записей вида <МГц>-<МГц>:<драйвер>, разделённых пробелами. Во втором случае устройство выбирается по частоте прохода.

soniks_client.rx_device.select_rx_device_by_frequency(rx_device, frequency)[исходный код]

Выбрать драйвер приёмника, покрывающего частоту прохода.

Исключение:

NoCompatibleRxDeviceError – Запись не разобралась либо ни один диапазон не покрывает частоту. Наблюдение не начинается.

Параметры:
  • rx_device (str)

  • frequency (int)

Тип результата:

str

Имена и раскладка файлов одного наблюдения.

Все имена строятся по OBSERVATION__FILENAME_TEMPLATE из префикса, идентификатора наблюдения, отметки времени и расширения.

class soniks_client.observation_files.ObservationFiles(directory, payload_ogg, waterfall_raw, waterfall_png, decoded_data_prefix, metadata_json)[исходный код]

Базовые классы: object

Пути файлов одного прохода.

Параметры:
  • directory (Path)

  • payload_ogg (str)

  • waterfall_raw (str)

  • waterfall_png (str)

  • decoded_data_prefix (str)

  • metadata_json (str)

directory: Path
payload_ogg: str
waterfall_raw: str
waterfall_png: str
decoded_data_prefix: str
metadata_json: str
soniks_client.observation_files.create_observation_files(observation_id, timestamp, mode)[исходный код]

Создать директорию наблюдения и собрать пути его файлов.

Параметры:
  • observation_id (str)

  • timestamp (str)

  • mode (str)

Тип результата:

ObservationFiles

Постобработка прохода: водопад и метаданные сигнала.

Отправкой на портал занимается вызывающий: блок signal отсюда — только часть метаданных наблюдения, остальное собирает Observation.

soniks_client.post_processing.build_waterfall(raw_path, png_path, observation_id)[исходный код]

Построить PNG водопада и вернуть метаданные сигнала.

Возвращает None, если водопад не построен или анализ ничего не дал. Сырой .dat удаляется только при успешном построении: при упавшем он единственное, с чем можно разбираться.

Параметры:
  • raw_path (str)

  • png_path (str)

  • observation_id (str)

Тип результата:

dict | None

Скрипты вокруг прохода: satnogs-pre и satnogs-post.

Оба получают одинаковый набор аргументов и запускаются одинаково, поэтому сборка командной строки и запуск разделены: первая — чистая функция, её видно из теста без подпроцесса.

soniks_client.observation_scripts.build_script_argv(script_type, observation_id, frequency, tle, timestamp, baud, mode)[исходный код]

Собрать командную строку скрипта прохода.

Режим, которого нет в таблице, заменяется на FLOWGRAPH__DEFAULT_MODE: скрипту нужно имя потокового графа, а не сам режим. Подмена пишется в лог предупреждением, потому что тихой она уже стоила двух обходов бага SSTV_PD120: это имя уходит шестым аргументом в satnogs-pre и satnogs-post, оттуда в scripts/find_samp_rate.py, а тот определяет по подстрокам в нём частоту дискретизации IQ. Имя из чужого режима даёт неверную частоту и UDP-потоку в gr-satellites, и имени IQ-дампа — без единого признака отказа.

Подмена доживает сюда, только пока портал не знает режимов станции: после их публикации неизвестный режим отказывается раньше, в execute_observation, и проход не начинается.

Параметры:
  • script_type (str)

  • observation_id (str)

  • frequency (int)

  • tle (dict[str, str])

  • timestamp (str)

  • baud (int)

  • mode (str)

Тип результата:

list[str]

soniks_client.observation_scripts.run_script(argv, timeout, env=None)[исходный код]

Выполнить скрипт прохода, слив его вывод в лог.

Ошибки гасятся: ни отсутствующий скрипт, ни зависший не должны отменять проход. Зависший добивается по таймауту.

Параметры:
  • argv (list[str])

  • timeout (float)

  • env (dict[str, str] | None)

Тип результата:

None

Запуск подпроцесса с чтением вывода общий у скриптов прохода и потокового графа.

Запуск подпроцесса с чтением его вывода в лог отдельным потоком.

Общее для потокового графа и скриптов вокруг прохода: одни и те же параметры Popen (расхождение в bufsize, encoding или errors молча ломает чтение вывода) и один и тот же слив stdout.

Читать вывод именно потоком обязательно: for line in process.stdout в основном блоке висит до EOF, из-за чего wait(timeout) становится недостижим и зависший процесс держит проход навсегда.

soniks_client.subprocess_log.start_logged_process(argv, log, env=None)[исходный код]

Запустить процесс и поток, сливающий его вывод в лог.

Параметры:
  • argv (list[str]) – Команда целиком, первым элементом — исполняемый файл.

  • env (dict[str, str] | None) – Окружение процесса целиком; None — унаследовать своё.

  • log (Callable[[str], None]) – Куда писать очередную строку вывода. Уровень и префикс — забота вызывающего, здесь про них ничего не известно.

Исключение:

OSError – Процесс не удалось запустить. Гасить решает вызывающий: у скрипта прохода и у потокового графа политика разная.

Тип результата:

tuple[Popen, Thread]

soniks_client.subprocess_log.stop_logging(log_thread, name)[исходный код]

Дождаться, пока поток чтения допишет остаток вывода.

stdout закрывает сам поток чтения, а не вызывающий: close() ждёт лок буфера, который читатель держит всё время чтения. Скрипт, оставивший после себя фоновый процесс (bandscan.sh start из satnogs-post), отдаёт ему свой stdout по наследству — и закрытие отсюда блокировало вызывающего до смерти этого процесса, то есть до следующего прохода. Таймаут на процесс от этого не спасал: сам скрипт к тому моменту давно завершился.

Параметры:
  • log_thread (Thread | None)

  • name (str)

Тип результата:

None

Потоковый граф

См. также Потоковый граф.

Запуск и остановка потокового графа GNU Radio отдельным процессом.

class soniks_client.flowgraph.Flowgraph(rx_device, frequency, mode, baud, payload_ogg_path, waterfall_raw_path, prefix_of_decoded_data_file, norad_cat_id=None)[исходный код]

Базовые классы: object

Потоковый граф GNU Radio, запускаемый отдельным процессом.

Поля settings.flowgraph вместе с параметрами прохода превращаются в аргументы --kebab-case=значение; значения None не передаются. Вывод процесса читается отдельным потоком и уходит в лог.

Параметры:
  • rx_device (str)

  • frequency (int)

  • mode (str)

  • baud (int)

  • payload_ogg_path (str)

  • waterfall_raw_path (str)

  • prefix_of_decoded_data_file (str)

  • norad_cat_id (int | None)

property is_running: bool

Живёт ли процесс графа.

start()[исходный код]

Запустить граф и поток чтения его вывода.

Исключение:

OSError – Процесс не удалось запустить. Раньше ошибка только логировалась, self.process оставался None, и вызывающий продолжал проход как ни в чём не бывало.

Тип результата:

None

stop()[исходный код]

Остановить граф.

Сначала SIGINT с ожиданием 10 секунд — по нему GNU Radio успевает корректно закрыть выходные файлы. Только затем принудительное завершение.

Граф, кончившийся сам до этого вызова, здесь же и опознаётся: отдельного состояния для этого не нужно, stop() вызывается из finally на любом пути прохода. Раньше такой отказ молчал, и проход без единого файла выглядел успешным наблюдением.

Тип результата:

None

get_metadata()[исходный код]

Вернуть блок метаданных приёмного тракта для отправки на портал.

Включает полный набор параметров графа, поэтому эти метаданные становятся публичными вместе с наблюдением.

Тип результата:

dict[str, Any]

Обмен с порталом

См. также Загрузку данных.

Обмен с порталом СОНИКС: расписание проходов, метаданные и выгрузка файлов.

Ошибки HTTP логируются здесь же. Наружу поднимаются только FileNotUploadedError и ObservationNotFoundError — они определяют, что делать с файлом дальше.

soniks_client.api.get_observation_jobs()[исходный код]

Получить расписание проходов с портала.

Ошибки сети логируются и гасятся: недоступность портала не должна ломать цикл синхронизации.

Результат:

Список заданий либо None, если запрос не удался.

Тип результата:

list[JobData] | None

soniks_client.api.status_body()[исходный код]

Документ статуса станции: режимы, версия, конфигурация.

Один и тот же для POST status/ и для MQTT. Портал сливает его со своей копией по ключам верхнего уровня, поэтому ключи здесь — собственность клиента: modes, client_version, config, sdr, calibration. Два последних есть только после обследования приёмника — отсутствующий ключ портал не трогает.

Тип результата:

dict

soniks_client.api.publish_station_status()[исходный код]

Сообщить порталу режимы станции, её версию и состояние конфигурации.

Портал не планирует станции режим, которого нет в её списке, — так закрывается подмена неизвестного режима на FM (канал C в docs/roadmap-network.md). Станция, которая ничего не объявила, для портала умеет всё, поэтому неудача здесь ничего не ломает: остаётся прежнее поведение.

Публикуется при каждом изменении документа (применённая конфигурация — тоже изменение) и повторяется на каждой сверке расписания, пока не удастся. 404 значит, что портал эндпоинта ещё не знает: это не ошибка станции.

Тип результата:

None

soniks_client.api.modes_published()[исходный код]

Портал принял режимы станции в этом процессе — и планирует по ним.

Принять их может только портал, который фильтрует планирование: эндпоинт и фильтр пришли одним изменением. Поэтому True значит, что режим вне MODES станции планироваться не должен был.

Тип результата:

bool

soniks_client.api.upload_observation_metadata(observation_id, data_to_send, file_path)[исходный код]

Выгрузить метаданные наблюдения.

Сигнатура общая с upload_observation_data() — метаданные проходят через ту же очередь. Отличие одно: содержимое уезжает полем формы, а не multipart-файлом, поэтому портал перезаписывает поля наблюдения и повтор безопасен без content-hash.

Параметры:
  • observation_id (str) – Идентификатор наблюдения на портале.

  • data_to_send (dict[str, bytes]) – Пара «поле формы — содержимое». Словарь при отправке опустошается.

  • file_path (Path) – Путь файла, нужен для сообщений в логе.

Исключение:
Тип результата:

None

soniks_client.api.upload_observation_data(observation_id, data_to_send, file_path)[исходный код]

Выгрузить один файл наблюдения.

Параметры:
  • observation_id (str) – Идентификатор наблюдения на портале.

  • data_to_send (dict[str, bytes]) – Пара «поле формы — содержимое». Словарь при отправке опустошается.

  • file_path (Path) – Путь файла, нужен для имени и MIME-типа.

Исключение:
Тип результата:

None

Конфигурация с портала

См. также Конфигурацию станции.

Конфигурация станции с портала: получение, проверка, применение.

Портал хранит желаемую конфигурацию станции (форма «Настройки станции» на sonik.space) и отдаёт её документом GET /api/v2/stations/<id>/state/: generation — номер сохранения, config — плоский словарь, ключи которого — имена переменных окружения (FLOWGRAPH__RF_GAIN), та же лексика, что в docs/station/environment_variables.md; location — координаты станции; release — канал обновлений. Тот же документ приходит по MQTT (soniks_client.mqtt) в момент сохранения формы.

Применение — в процессе, без перезапуска контейнера: разделы settings пересобираются через pydantic поверх значений из .env, значения зеркалятся в os.environ для скриптов из scripts/, кэш контроллеров ротатора и рига сбрасывается. Пока владелец ничего не сохранил на портале (generation 0), станция работает по .env как раньше.

Последний полученный документ лежит на диске (PATHS__STATE_FILE): станция без портала стартует на нём.

class soniks_client.remote_config.ApplyResult(generation, ok, error='', applied_at=None)[исходный код]

Итог применения документа состояния.

Параметры:
  • generation (int)

  • ok (bool)

  • error (str)

  • applied_at (str | None)

generation: int
ok: bool
error: str = ''
applied_at: str | None = None
soniks_client.remote_config.actual_config()[исходный код]

Фактическая конфигурация станции — плоский словарь по MANAGED_FIELDS.

Тип результата:

dict

soniks_client.remote_config.report()[исходный код]

Что станция сообщает порталу о конфигурации — ключ config статуса.

actual уходит всегда, и до первого сохранения на портале тоже: форма предзаполняется им, так что станция, настроенная через .env, переезжает на портал без перепечатывания значений.

Тип результата:

dict

soniks_client.remote_config.load_state_file()[исходный код]

Прочитать последний сохранённый документ состояния.

Тип результата:

dict | None

soniks_client.remote_config.save_state_file(doc)[исходный код]

Записать документ состояния на диск — атомарно, через соседний файл.

Параметры:

doc (dict)

Тип результата:

None

soniks_client.remote_config.fetch_state()[исходный код]

Забрать документ состояния с портала.

Результат:

не изменился (304), портал недоступен или эндпоинта ещё не знает (404 — не ошибка станции).

Тип результата:

Документ, либо None

soniks_client.remote_config.sync()[исходный код]

Шаг сверки: забрать новое состояние либо доприменить отложенное.

Тип результата:

None

soniks_client.remote_config.bootstrap()[исходный код]

Старт: применить сохранённое состояние, затем свежее с портала.

Порядок важен: портал может лежать, а станция обязана подняться на том, что применяла в прошлый раз.

Тип результата:

None

soniks_client.remote_config.apply_state(doc)[исходный код]

Применить документ состояния.

Отказ (неизвестный ключ, невалидное значение, не открылся приёмник) оставляет прежнюю конфигурацию и уезжает на портал причиной в report(). Идущий проход откладывает применение до следующей сверки.

Параметры:

doc (dict)

Тип результата:

ApplyResult

Проверка приёмника перед применением конфигурации с портала.

Открывает устройство python-биндингом SoapySDR и выставляет частоту дискретизации, антенный вход и усиление — то, что в реальном проходе делает граф. Работает в подпроцессе с таймаутом: зависший драйвер не должен вешать поток планировщика. Запускать только вне прохода — приёмник занят графом.

Без биндинга (старая база образа) проверка пропускается с предупреждением: иначе на такой станции не применился бы ни один конфиг.

soniks_client.sdr_check.check(device, sample_rate, antenna, gain, timeout=20)[исходный код]

Открыть приёмник с заданными параметрами.

Результат:

None, если устройство открылось (или проверить нечем), иначе текст ошибки для лога и портала.

Параметры:
  • device (str)

  • sample_rate (float | None)

  • antenna (str | None)

  • gain (float | None)

  • timeout (float)

Тип результата:

str | None

Обследование приёмника: какие устройства подключены и какое усиление ставить.

Две операции, обе — python-биндингом SoapySDR в подпроцессе с таймаутом, как sdr_check, и только когда приёмник свободен: во время прохода он занят графом, а Airspy и RTL-SDR второй раз не открываются.

  • Поиск устройствSoapySDR.Device.enumerate() плюс возможности каждого найденного (входы, диапазоны усиления, частоты дискретизации). На старте, раз в SDR__SCAN_INTERVAL_IN_MINUTES в простое и по команде с портала. Портал показывает найденное в форме настроек и подставляет строку устройства, вход и границы усиления одной кнопкой.

  • Калибровка усиления — по команде с портала: свип по общему усилению на заданных частотах, средняя мощность на каждом шаге. Шумовая полка растёт с усилением, пока внешний шум не перекроет собственный; рекомендуется первое усиление, на котором полка поднялась на SDR__NOISE_LIFT_DB над минимумом (Фаза 4, шаг 1 в docs/roadmap-network.md).

Команды приходят блоком commands документа состояния (remote_config): {"rescan_sdr": {"at": ...}, "calibrate_gain": {"at": ..., "frequencies": [...]}}. Команда выполняется, если её отметка at отличается от последней выполненной; отметки и последние результаты лежат рядом с файлом состояния. Итоги уходят ключами sdr и calibration документа статуса (api.status_body()).

soniks_client.sdr_survey.recommend(points, lift_db)[исходный код]

Выбрать усиление по подъёму шумовой полки.

Параметры:
  • points (list[list[float]]) – Пары [усиление, мощность_дБ] по возрастанию усиления.

  • lift_db (float) – На сколько полка должна подняться над минимальной.

Результат:

Первое усиление, на котором подъём достигнут; если не достигнут нигде — усиление с максимальной мощностью (приёмник упёрся раньше); None без точек.

Тип результата:

float | None

soniks_client.sdr_survey.report()[исходный код]

Ключи sdr и calibration для документа статуса — только известные.

Тип результата:

dict

soniks_client.sdr_survey.request(commands)[исходный код]

Запомнить команды из документа состояния; выполнит run_due().

Тип результата:

None

soniks_client.sdr_survey.run_due()[исходный код]

Запустить назревшее обследование в фоновом потоке, если приёмник свободен.

Зовётся после каждой сверки и после каждого документа по MQTT. Один поток на всё: два обследования разом не откроют один приёмник.

Тип результата:

None

soniks_client.sdr_survey.scan()[исходный код]

Перечислить приёмники и их возможности; итог — ключ sdr статуса.

Тип результата:

dict

soniks_client.sdr_survey.calibrate(frequencies)[исходный код]

Свип по усилению на частотах; итог — ключ calibration статуса.

Приёмник для каждой частоты — тот, что взял бы проход (OBSERVATION__SOAPY_RX_DEVICE может быть списком диапазонов), поэтому частоты группируются по устройству и каждое обследуется одним подпроцессом.

Параметры:

frequencies (list)

Тип результата:

dict

Живой канал с порталом: MQTT через WebSocket на sonik.space.

Портал публикует документ состояния станции в момент сохранения формы, станция применяет его сразу, а не на следующей сверке расписания, и тут же публикует статус. Last Will даёт порталу мгновенный «offline». REST остаётся источником истины и фолбэком: без брокера (закрытая сеть, старая база без paho) всё то же самое происходит раз в минуту через remote_config.sync().

Топики: stations/<id>/state (портал → станция, retained), stations/<id>/status (станция → портал, тело — как у POST status/), stations/<id>/online (retained 1, Last Will 0).

soniks_client.mqtt.start_mqtt()[исходный код]

Подключиться к брокеру в фоновом потоке.

Результат:

True, если поток запущен. Ошибки гасятся — станция без MQTT живёт на REST.

Тип результата:

bool

soniks_client.mqtt.publish_status()[исходный код]

Опубликовать статус станции — тот же документ, что уходит REST’ом.

Тип результата:

None

Определения спутников

См. также Поддерживаемые спутники.

Определения спутников с портала: satyaml и sat.cfg одним архивом.

Раньше это делал liveupdate-satyaml.sh: при каждом старте контейнера он клонировал gr-satellites и soniks-satyaml из git. В закрытой сети, где открыт только sonik.space, клон падал и станция молча оставалась на определениях из образа — то есть обновиться не могла никак. В открытой сети было не лучше: две станции с одним тегом образа получали разный satyaml, смотря когда их перезапускали.

Теперь источник один — портал, архив версионирован, а версия уезжает в client_metadata вместе с наблюдением. Дальше по важности:

  • ничто здесь не имеет права уронить старт. Портал лежит, архив битый, хеш не сошёлся — предупреждение в лог и работа на том, что уже есть;

  • скачивание и применение разделены. Кэш живёт на постоянном томе (PATHS__BASE), а каталог satyaml внутри пакета satellites — в эфемерной файловой системе контейнера. Значит применять кэш надо на каждом старте, а не только когда приехала новая версия.

soniks_client.sat_data.applied_version()[исходный код]

Версия применённого bundle. None — станция на данных из образа.

Тип результата:

str | None

soniks_client.sat_data.sync_sat_data()[исходный код]

Обновить определения спутников и применить их к текущему контейнеру.

Результат:

Версия применённого bundle либо None, если применять нечего и станция работает на определениях из образа.

Тип результата:

str | None

soniks_client.sat_data.resync_sat_data()[исходный код]

Периодически подтянуть bundle, не дожидаясь рестарта контейнера.

Раньше новый спутник доезжал до работающей станции только с перезапуском: sync_sat_data() звался один раз из main(). Во время прохода раскладка откладывается — gr_satellites и выбор декодера читают тот же каталог. Новая версия меняет список спутников станции, и статус на портале публикуется заново.

Тип результата:

None

soniks_client.sat_data.decoder_for(norad)[исходный код]

Выбрать декодер прохода по наличию satyaml для спутника.

Смотрит в каталог пакета satellites — тот же, куда _apply_satyaml кладёт bundle и где лежат штатные определения gr-satellites. Индекс строится на каждом вызове: файлов сотни, а кэш пришлось бы сбрасывать после каждого apply(). Нет пакета — gr-satellites всё равно не запустится, поэтому ответ satnogs здесь честный.

Параметры:

norad (int | None)

Тип результата:

str

soniks_client.sat_data.norad_of(tle, norad_cat_id)[исходный код]

NORAD прохода: от портала (опт-ин) либо из второй строки TLE.

Параметры:
  • tle (dict)

  • norad_cat_id (int | None)

Тип результата:

int | None

soniks_client.sat_data.iq_mode_for(norad)[исходный код]

Режим satnogs-графа, который даст gr-satellites поток IQ.

Когда режим портала клиенту неизвестен, а satyaml для спутника есть, отказывать проходу незачем: демодулятор satnogs-графа не нужен, нужен только IQ с подходящей частотой, и её задаёт модуляция передатчика. Три семейства покрывают gr-satellites целиком; незнакомая модуляция или отсутствие satyaml — None, и проход отказывается как раньше.

Параметры:

norad (int | None)

Тип результата:

str | None

soniks_client.sat_data.baudrate_for(norad)[исходный код]

Скорость первого передатчика satyaml — для задания без baud.

Параметры:

norad (int | None)

Тип результата:

int | None

soniks_client.sat_data.known_norads()[исходный код]

NORAD всех определений в каталоге satyaml пакета satellites.

Уезжает на портал ключом satellites статуса станции: по нему портал сможет планировать спутник с satyaml независимо от режима передатчика.

Тип результата:

set[int]

Модели данных

Описание задания наблюдения, полученного от портала.

class soniks_client.models.JobData(id, start, end, tle, frequency, mode, baud, norad_cat_id=None)[исходный код]

Базовые классы: object

Задание наблюдения, полученное от портала.

Параметры:
  • id (str)

  • start (datetime)

  • end (datetime)

  • tle (dict[str, str])

  • frequency (int)

  • mode (str)

  • baud (int)

  • norad_cat_id (int | None)

id: str
start: datetime
end: datetime
tle: dict[str, str]
frequency: int
mode: str
baud: int
norad_cat_id: int | None = None
classmethod from_dict(job_data)[исходный код]

Собрать задание из ответа API.

Параметры:

job_data (dict[str, str | int | None]) – Словарь одного задания из ответа портала.

Результат:

Разобранное задание наблюдения.

Тип результата:

JobData

static parse_datetime(datetime_str)[исходный код]

Разобрать метку времени из ответа портала в datetime с UTC.

Параметры:

datetime_str (str)

Тип результата:

datetime

Водопад

Пакет из трёх модулей; наружу выдаётся только Waterfall. Приватные DSP-хелперы не выводятся, см. Водопад.

Водопад наблюдения: разбор сырого .dat, PNG для портала и анализ сигнала.

Пакет разложен по слоям: dsp — примитивы numpy, deviation — оценщик девиации V7, plot — чтение файла и отрисовка. Наружу выдаётся только класс Waterfall.

Разбор сырого .dat водопада и отрисовка PNG для портала.

Здесь живёт весь matplotlib. Фигура создаётся явно (Figure + FigureCanvasAgg), без pyplot: глобальный менеджер фигур не переживает параллельные постобработки в воркерах планировщика.

class soniks_client.waterfall.plot.Waterfall(datafile_path, output_path)[исходный код]

Базовые классы: object

Водопад одного наблюдения: разбор сырого .dat, отрисовка и анализ.

Исключение:
Параметры:
  • datafile_path (str)

  • output_path (str)

plot(vmin=None, vmax=None)[исходный код]

Отрисовка водопада с анализом сигнала, маркерами и перекрестием

Параметры:
  • vmin (float | None)

  • vmax (float | None)

Тип результата:

None

get_signal_metadata()[исходный код]

Return formatted signal metadata for API submission.

Returns dict with string-formatted values matching the server schema, or None if analysis was not performed.

Тип результата:

dict | None

Оценка девиации сигнала: единственный живой оценщик — V7.

Читает разобранный водопад (wf_data, metadata) и возвращает словарь с характеристиками сигнала, который Waterfall.plot кладёт в Waterfall.analysis. Описание конвейера — в шапке ниже.

Примитивы numpy-DSP, на которых стоит оценка девиации.

Ни один из них не знает ни о matplotlib, ни о настройках станции: на входе массивы спектра, на выходе числа. Модуль намеренно без побочных эффектов — его можно вызывать из тестов, не поднимая наблюдение.