# Обзор эндпоинтов Полное машинно-сгенерированное описание — в [интерактивном справочнике](https://sonik.space/docs/network/api-reference/index.html). Здесь — карта API с пояснениями, которых в схеме нет. ## Наблюдения и задания ### `/api/observations/` Список и карточки наблюдений; сюда же станция загружает аудио и водопад. Запись доступна владельцу станции (`StationOwnerPermission`). При загрузке артефакта, который уже загружен, возвращается `403` с телом: ```json {"detail": "<артефакт> has already been uploaded", "code": "already_uploaded"} ``` ```{important} Формулировка `has already been uploaded` — часть контракта, а не текст для человека: клиент станции распознаёт успешный повтор именно по этой подстроке. Менять её нельзя, пока весь парк клиентов не перейдёт на чтение поля `code`. ``` `DELETE` отменяет наблюдение, которое ещё не началось. Право спрашивается тем же `delete_perms`, по которому в интерфейсе появляется кнопка удаления: автор, владелец станции, модератор, суперпользователь. Начавшееся наблюдение не удаляет никто — `403` с `code: cannot_delete`. Успех — `204` без тела. Фильтры по времени: `start` (не раньше) и `end` (не позже) выбирают наблюдения целиком внутри окна; `start__lt` и `end__gt` — вторые половины тех же границ, ими выбираются наблюдения, которые в заданный момент шли. Чтение списка ограничено по частоте — `OBSERVATIONS_LIST_ANON_THROTTLE_RATE` (60/час) и `OBSERVATIONS_LIST_AUTH_THROTTLE_RATE` (240/час). Ограничение висит только на списке: карточка наблюдения, планирование и загрузка артефактов не ограничены, поэтому станция его не замечает. ### `/api/jobs/` Задания для станции — будущие наблюдения с параметрами приёма. Только чтение. Новый клиент запрашивает его при старте контейнера и пока нет связи с брокером; дальше тот же список приходит retained-сообщением `stations//jobs` при каждом изменении расписания (см. «Расписание станции» ниже). Старый клиент опрашивает его раз в минуту. Анонимный доступ управляется `JOBS_ALLOW_ANONYMOUS` (по умолчанию включён). Одиннадцать полей ответа неизменны с 2021 года. Всё, что добавлено позже, выдаётся только по явному объявлению клиента — `?capabilities=norad_cat_id,transmitter_parameters,max_altitude`: клиент, не объявивший ничего, получает прежний ответ байт в байт. `transmitter_parameters` берётся из снимка наблюдения, то есть описывает передатчик таким, каким он был на момент планирования. ### `/api/demoddata/` Демодулированные кадры. Чтение — только с аутентификацией, отправка кадра по протоколу SIDS открыта и анонимам (`SafeMethodsWithPermission`); привязать кадр к станции или наблюдению может только владелец. Отправка ограничена по частоте — `DEMODDATA_CREATE_THROTTLE_RATE` (3600/час на пользователя или IP), чтение не ограничено. Список постраничный, с курсором, от новых кадров к старым (`-timestamp, -id`). По умолчанию на странице 25 кадров, `?page_size=` поднимает размер до 1000. Кадр и `payload_json` берутся из копии в базе (`DemodDataContent`), поэтому страница стоит один SQL-запрос и ни одного обращения к S3. Кадры, принятые до появления копии, читаются из файла, пока их не скопирует [`copy_demoddata_content`](../commands.md#copy_demoddata_content). Как выкачивать архив: `page_size=1000`, переходить по `next` и распараллеливать по спутникам или по окнам `start`/`end`. Новые кадры по мере приёма удобнее получать по MQTT (см. [](../satellites.md)), а REST использовать для догонки. Имя файла разбирается по контракту `<префикс>__<временная метка>.<расширение>`, метка допускается с разделителем времени `-` или `:`. Имя, не соответствующее контракту, отбрасывается штатно, а не приводит к ошибке 500. Разобранная метка должна попадать в окно наблюдения с допуском `DEMODDATA_TIME_TOLERANCE_MINUTES` (по умолчанию 5 минут) с обеих сторон — допуск на уход часов станции, а не на кадр из другого пролёта. Кадр вне окна отклоняется `400` с `code: frame_outside_window`. На SiDS-отправки (`POST /api/demoddata/`) проверка не распространяется: там метку объявляет клиент по своему протоколу. ## Станции | Эндпоинт | Метод | Назначение | |---|---|---| | `/api/stations/` | GET | Список станций и их состояний | | `/api/station/register` | POST | Регистрация новой станции | | `/api/stations/check` | GET | Проверка состояния станции | Список станций одинаков для всех, поэтому отдаётся из кэша на `CACHE_TTL` (5 минут) — не на час, как у апстрима: в ответе есть `last_seen` и `status`, и час означал бы устаревшие сведения о том, кто сейчас в эфире. Анонимное чтение списка ограничено `STATIONS_LIST_ANON_THROTTLE_RATE` (256/час). ## Спутники и передатчики | Эндпоинт | Назначение | |---|---| | `/api/satellites/` | Каталог спутников | | `/api/transmitters/` | Передатчики; `params` — параметры режима для станции (у `LoRa` — JSON со страницы спутника TinyGS, проверяется при сохранении; пустой — передатчик не настроен и не планируется) | | `/api/modes/` | Справочник режимов модуляции | Везде, где фильтр или параметр принимает номер NORAD (`satellite__norad_cat_id`, `norad_cat_id`, `satellite`, `satellite_norad`, `noradID` в SIDS), номера от 100000 можно передавать и в записи Alpha-5 (`A0469` = 100469), как их публикует Space-Track. Вспомогательные представления: * `/api/satellitestationobs` — наблюдения спутника по станциям; * `/api/future_satellite_obs` — будущие наблюдения спутника; * `/api/topdata` — сводка по наиболее активным объектам. ## Орбитальные данные ### `/api/tles/` Чтение — публичное, но спутник назвать обязательно: нужен один из фильтров `satellite_norad`, `satellite_sat_id`, `satellite_name`. Без него — `400` с `results: null`. Раньше эндпоинт отдавал весь архив портала на любой запрос. Постраничная выдача курсорная (`-updated`, `-id`), поэтому в теле нет `count`: архив растёт с головы, и смещение и уплывало бы, и замедлялось с глубиной. `POST` — публикация набора внешним сервисом определения орбиты. Требует аутентификации (включая Bearer через OIDC), точечного права на конкретный спутник и проходит полную валидацию TLE. Сохраняется с источником `Manual`, после чего сразу пересчитывается `LatestTleSet`. Подробности и ограничения — в [](../tle.md). ### `/api/latesttles/` Актуальный набор на спутник. Доступен анонимно, ограничен `LATEST_TLES_THROTTLE_RATE` (60/мин), поддерживает `?FORMAT=tle` и `?FORMAT=json`. Отдаёт только аппараты на орбите — отбор по возрасту TLE (`LATEST_TLES_MAX_AGE_DAYS`), а не по полю `status`. ## Аналитика | Эндпоинт | Назначение | |---|---| | `/api/analytics/network/` | Суточные метрики по сети | | `/api/analytics/station/` | Суточные метрики по станциям | | `/api/analytics/dynamic-demod/` | Динамика поступления демодулированных данных | | `/api/analytics/needs-attention/` | Станции, требующие внимания | Подробнее о том, что и как считается, — в [](../analytics.md). ## `/api/v2/` — определения спутников Первый эндпоинт новой версии API. Всё новое живёт здесь и ничего старого не двигает: станция, которая никогда не обновится, обращается только к `/api/` и появления `/api/v2/` заметить не может. | Эндпоинт | Метод | Кто | Назначение | |---|---|---|---| | `/api/v2/bundles/<версия>/` | GET | станция, анонимно | Метаданные sat-data bundle: `version`, `sha256`, `size` и адрес архива. Вместо версии можно указать `latest` | | `/api/v2/bundles/` | POST | CI, staff-токен | Публикация нового bundle: поле `version` и файл `archive` | **Что такое bundle.** Архив `tar.gz` с определениями спутников (`satyaml/`) и `sat.cfg`, собираемый CI репозитория [soniks-satyaml](https://gitlab.com/space-education-development/soniks/client/soniks-satyaml). Станция скачивает его при старте вместо того, чтобы клонировать определения из git: в закрытых сетях, где открыт только `sonik.space`, клон падал и станция работала с устаревшими определениями, ничего об этом не сообщая. **Чтение анонимно намеренно.** Определения спутников — публичные данные, а токен между станцией в закрытой сети и её единственным каналом обновления был бы лишней движущейся частью. **Версия неизменяема.** Хеш считает портал по принятому телу, как и у артефактов наблюдений. Повторная публикация тех же байт под той же версией — `200` без новой записи (перезапущенный пайплайн), других байт — `409` с кодом `version_exists`: станции, уже применившие эту версию, иначе остались бы с содержимым, которого больше нигде нет. ## `/api/v2/` — станция и портал | Эндпоинт | Метод | Кто | Назначение | |---|---|---|---| | `/api/v2/stations//status/` | POST | владелец станции, `Authorization: Token` | Станция сообщает о себе: `modes` — режимы демодуляции, `satellites` — NORAD спутников, которые она декодирует при любом режиме, `client_version` — версия клиента, `config` — итог применения конфигурации с портала, `sdr` — найденные приёмники, `calibration` — итог калибровки усиления. Ограничен `STATION_STATUS_THROTTLE_RATE` (1200/час на владельца) | | `/api/v2/stations//status/` | GET | владелец | Сохранённый документ, время его приёма и текущее поколение конфигурации — для страницы настроек | | `/api/v2/stations//state/` | GET | владелец | Документ состояния: конфигурация, координаты, канал релизов, команды станции. `ETag`, `If-None-Match` → `304` | | `/api/v2/mqtt/auth/`, `/api/v2/mqtt/acl/` | POST | брокер MQTT, внутри сети compose | Проверка логина и прав на топики для `mosquitto-go-auth`. Снаружи закрыты nginx, в схему не входят | ### Статус станции Тело `POST` — JSON: `modes` — необязательный непустой список строк до 25 символов (как `Mode.name`; отсутствует — прежний список остаётся, пустой — `400`), `satellites` — необязательный список положительных целых NORAD (спутники, для которых у станции есть satyaml gr-satellites: их передатчики планируются при любом режиме; пустой список допустим и значит «исключений нет», хранится отсортированным без повторов), `client_version` — необязательная строка до 45 символов (обновляет и колонку `Station.client_version`, которую раньше писала только выгрузка наблюдения), `config` — необязательный объект `{generation, ok, error, applied_at, actual}`, где `actual` — фактическая конфигурация станции в той же форме, что и `state.config` (ключи вне `STATION_CONFIG_FIELDS` — `400`). `sdr` — необязательный объект `{scanned_at, devices, error}`: приёмники, которые станция нашла через `SoapySDR.Device.enumerate()`, у каждого `args` (строка для `OBSERVATION__SOAPY_RX_DEVICE`), `driver`, `label`, `serial`, `antennas`, `gains: {overall: [min, max], stages: {имя: [min, max]}}`, `sample_rates`, `frequency_range`, `dc_offset_mode`, либо `error`, если не открылся. `calibration` — необязательный объект `{calibrated_at, results, recommended, lift_db, error}`, где `results` — по одному на частоту: `device`, `frequency`, `points` (пары `[усиление_дБ, мощность_дБ]`), `recommended`. `scheduling` — необязательный объект `{lead_seconds}`, 10…900: за сколько секунд до начала станции нужно получить задание; пока станция на связи по MQTT (`connection: online`), портал планирует ей по этому порогу вместо `OBSERVATION_DATE_MIN_START` (`Station.scheduling_lead()`), а начало, попавшее внутрь порога, сдвигает вперёд и сообщает об этом планирующему. Остальные ключи отбрасываются. Документ **сливается** с сохранённым по ключам верхнего уровня: станция заменяет свои ключи (`modes`, `satellites`, `client_version`, `config`, `sdr`, `calibration`, `scheduling`) и не трогает чужие (`connection` пишет мост MQTT, `agent` — будущий агент). Лежит в `Station.reported_status`, время приёма — в `reported_status_at`, `last_seen` сдвигается. Ответ — `200` со слитым документом, режимы в нём отсортированы и без повторов. Аноним — `401`, чужая станция — `403`, нет станции — `404`, неверное тело — `400`. **Зачем.** Портал планирует станции только режимы из её списка: иначе клиент получал режим, которого нет в его таблице, и молча принимал его как FM. Станция, которая ничего не заявила, планируется на все режимы, как раньше, — поэтому эндпоинт ничего не меняет для парка, который никогда не обновится. `config.actual` предзаполняет форму настроек: станция, настроенная через `.env`, переезжает на портал без перепечатывания. ### Состояние станции `GET /api/v2/stations//state/` отдаёт то, что владелец сохранил в форме «Настройки станции», плюс то, что станции нужно знать о себе: ```json { "generation": 3, "config": {"OBSERVATION__SOAPY_RX_DEVICE": "driver=rtlsdr", "FLOWGRAPH__RF_GAIN": 25.0}, "location": {"lat": 55.75, "lng": 37.61, "alt": 150}, "release": {"channel": "stable", "mode": "notify", "client_image": "sonik.space/sonikspace/soniks-client@sha256:…"}, "commands": {"rescan_sdr": {"at": "2026-09-11T10:00:00+00:00"}, "calibrate_gain": {"at": "…", "frequencies": [137500000, 435000000]}} } ``` `generation` — номер сохранения формы, `0` — владелец ничего не сохранял и станция работает по своему `.env`. `config` — плоский словарь, ключи — имена переменных окружения клиента (единый словарь имён — `network/base/station_config.py`). `ETag` считается по всему телу: координаты и канал меняются без нового поколения. `commands` — разовые просьбы к станции (кнопки «Пересканировать» и «Откалибровать» в форме настроек, `POST stations//command/` со страницы): станция выполняет команду один раз на отметку `at`, между проходами, и отвечает ключами `sdr` / `calibration` статуса. Повторное нажатие — новая отметка, а не новый ключ. `last_seen` запрос не двигает — станция ходит за расписанием в `/api/jobs/` тем же циклом. Тот же документ портал публикует в MQTT (`stations//state`, retained) при сохранении формы, так что станция на связи применяет его сразу, а не на следующей минуте. Как устроен брокер — [](../dev/deploy.md), раздел «MQTT-брокер». ### Расписание станции Тот же список, что `GET /api/jobs/?ground_station=&capabilities=norad_cat_id,transmitter_parameters,max_altitude` — будущие наблюдения станции со всеми opt-in полями, — портал публикует в `stations//jobs` (retained, QoS 1) при каждом изменении: планирование, отмена, пересчёт TLE, обновление снимка передатчика. Снимок, а не поток событий: у брокера всегда лежит актуальное расписание, станция после (пере)подключения получает его без реплея и применяет тем же сравнением, что и ответ REST; отменённое наблюдение — то, которого в списке больше нет. Публикацию ставят сигналы `post_save`/`post_delete` наблюдения с будущим `start` (одна задача на станцию за 2 с) и два пути мимо сигналов — `refresh_future_observation_snapshots` и `update_future_observations_with_new_tle_sets`; раз в 30 минут `publish_all_station_jobs` переиздаёт снимки станций на связи — страховка от сообщения, потерянного при недоступном брокере. Без `MQTT_BROKER_HOST` ничего не публикуется. ### Подписка на данные спутника Кроме станций, в брокер входит подписчик. Логин — `subscriber-`, пароль — `User.subscription_key`. Ключ выпускается при первой подписке, а в профиле его можно перевыпустить (`POST /users/update/subscription-key/`). API-токен в брокер под этим логином не пускает. Имя пользователя Django на права не влияет: логин строится из id. ACL разрешает подписчику чтение и подписку (`acc` 1 и 4) ровно на `satellites//frames`, если соблюдены два условия: - у пользователя есть строка `SatelliteSubscription` на этот спутник; - хотя бы одна его станция сейчас в сети (`Station.objects.connected()`). Всё остальное — запись, шаблоны, чужие топики — `403`. Решения брокер кеширует на 300 с, поэтому доступ закрывается и открывается с такой задержкой. Публикует портал из задачи `decode_current_frame`: через неё проходит каждый новый кадр станции и SiDS. Кадр публикуется после попытки декодирования, от имени служебного пользователя, с QoS 1, без retain, и только если на спутник есть хоть одна подписка. Суточный импорт SatNOGS ставит задачу с `publish=False`. Тело — `DemodDataViewSerializer`, то есть элемент `GET /api/demoddata/`. Ключи `payload_json` переводятся через `trans_decode` на русский. Флаг «возможных данных» в поток не входит, пока его нет в модели. ## Где что лежит в коде | Файл | Содержимое | |---|---| | `network/api/urls.py` | Маршруты и регистрация ViewSet'ов в роутере | | `network/api/urls_v2.py` | Маршруты `/api/v2/` | | `network/base/station_config.py` | Поля конфигурации станции: единый словарь для формы, сериализатора и `state/` | | `network/api/views.py` | ViewSet'ы и функциональные представления | | `network/api/serializers.py` | Сериализаторы | | `network/api/filters.py` | Фильтры | | `network/api/pagination.py` | Классы постраничной выдачи | | `network/api/renderers.py` | Браузерный рендерер без форм | | `network/api/tests.py` | Тесты API |