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Наблюдение одного прохода спутника.
Выбирает приёмник по частоте, строит пути файлов и потоковый граф, ведёт сессии слежения и доплеровской коррекции, а после прохода строит водопад и отправляет метаданные.
- Параметры:
rig_controller (RigController)
rotator_controller (RotatorController | None)
- 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)
- Тип результата:
Постобработка прохода: водопад и метаданные сигнала.
Отправкой на портал занимается вызывающий: блок 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) – Путь файла, нужен для сообщений в логе.
- Исключение:
ObservationNotFoundError – Наблюдение удалено на портале.
FileNotUploadedError – Выгрузить не удалось, нужна повторная попытка.
- Тип результата:
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-типа.
- Исключение:
ObservationNotFoundError – Наблюдение удалено на портале.
FileNotUploadedError – Выгрузить не удалось, нужна повторная попытка.
- Тип результата:
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)
- Тип результата:
Проверка приёмника перед применением конфигурации с портала.
Открывает устройство 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]) – Словарь одного задания из ответа портала.
- Результат:
Разобранное задание наблюдения.
- Тип результата:
- 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, отрисовка и анализ.- Исключение:
WaterfallFileError – Файл не найден или не читается.
EmptyWaterfallError – Файл прочитан, но отсчётов нет.
- Параметры:
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, ни о настройках станции: на входе массивы спектра, на выходе числа. Модуль намеренно без побочных эффектов — его можно вызывать из тестов, не поднимая наблюдение.