closed TG-1; git was inited;
This commit is contained in:
+630
@@ -0,0 +1,630 @@
|
||||
# ais_hub API reference
|
||||
|
||||
Полный контракт всех публичных интерфейсов `ais_hub` для веб-морды / SPI-bridge / BLE-сервера.
|
||||
|
||||
- **Base URL (HTTP/WS):** `http://<host>:8081` (порт из `publish.http.port`).
|
||||
- **Авторизация:** нет (сервис привязан к localhost / доверенной сети).
|
||||
- **Кодировка:** `UTF-8`. Все JSON — одна строка либо pretty, клиент не должен полагаться на форматирование.
|
||||
- **Timestamps:** все поля `ts` — **Unix epoch в секундах**, `float`. Поля в миллисекундах (от AIS-catcher) явно называются `*_unix_ms`.
|
||||
- **Nullability:** любое поле с неизвестным значением = `null`, даже если тип в таблице int/float.
|
||||
- **Стабильность:** REST пути версионированы (`/api/v1/*`). Новые поля могут добавляться без breaking change — клиент должен игнорировать неизвестные ключи.
|
||||
|
||||
---
|
||||
|
||||
## 1. HTTP REST
|
||||
|
||||
### 1.1 `GET /api/v1/health`
|
||||
|
||||
Liveness probe. Всегда 200, если процесс жив.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"uptime_sec": 12345.678
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 `GET /api/v1/stats`
|
||||
|
||||
Все внутренние счётчики и gauges. Полезно для мониторинга и диагностики.
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime_sec": 12345.678,
|
||||
"started_at": 1700000000.123,
|
||||
"counters": {
|
||||
"ais_udp_datagrams": 10423,
|
||||
"ais_udp_malformed": 2,
|
||||
"ais_reports": 9812,
|
||||
"ais_msg_1": 4200,
|
||||
"ais_msg_3": 1800,
|
||||
"ais_msg_5": 410,
|
||||
"ais_msg_18": 3200,
|
||||
"ais_msg_24": 202,
|
||||
"ais_fragment_errors": 0,
|
||||
"ais_fragment_timeouts": 0,
|
||||
"parser_errors": 1,
|
||||
"parser_checksum_errors": 0,
|
||||
"state_ownship_updates": 1234,
|
||||
"state_targets_new": 120,
|
||||
"state_targets_updates": 9500,
|
||||
"state_base_station_updates": 0,
|
||||
"state_aton_updates": 0,
|
||||
"state_rssi_updates": 45200,
|
||||
"state_slot_bitmap_updates": 12,
|
||||
"state_slot_detail_updates": 12,
|
||||
"state_signal_events": 8800,
|
||||
"state_warmup_vessels": 42,
|
||||
"storage_batches": 950,
|
||||
"storage_rows": 31200,
|
||||
"storage_errors": 0,
|
||||
"udp_events_sent": 123456,
|
||||
"udp_events_oversize": 3,
|
||||
"supervisor_restarts_ingest_ais_udp": 1,
|
||||
"ws_clients_total": 4
|
||||
},
|
||||
"gauges": {
|
||||
"ws_clients": 1,
|
||||
"storage_last_batch": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ключевые counters для UI**
|
||||
- `ais_msg_<N>` — сколько сообщений каждого типа декодировано (1/2/3/4/5/18/19/21/24/27 и т.д.).
|
||||
- `ais_fragment_*` — проблемы мультифрагментной сборки.
|
||||
- `state_targets_new` — количество впервые увиденных MMSI.
|
||||
- `state_warmup_vessels` — сколько судов поднято из SQLite при старте (persistence).
|
||||
- `supervisor_restarts_*` — если не 0, какой-то subsystem падает.
|
||||
|
||||
---
|
||||
|
||||
### 1.3 `GET /api/v1/ownship`
|
||||
|
||||
Последний GPS-fix (собственное судно).
|
||||
|
||||
```json
|
||||
{
|
||||
"ts": 1700000000.123,
|
||||
"lat": 59.123456,
|
||||
"lon": 10.987654,
|
||||
"sog": 5.4,
|
||||
"cog": 128.5,
|
||||
"alt": 12.3,
|
||||
"fix_quality": 1,
|
||||
"sats": 9,
|
||||
"hdop": 0.8
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| --- | --- | --- |
|
||||
| `ts` | float | Unix-время последнего обновления. 0 если fix ещё не пришёл. |
|
||||
| `lat`, `lon` | float \| null | Широта/долгота в градусах (WGS-84). |
|
||||
| `sog` | float \| null | Speed over ground, узлы. |
|
||||
| `cog` | float \| null | Course over ground, градусы от истинного севера. |
|
||||
| `alt` | float \| null | Высота над эллипсоидом, метры (GGA). |
|
||||
| `fix_quality` | int \| null | NMEA GGA fix quality (0=нет, 1=GPS, 2=DGPS…). |
|
||||
| `sats` | int \| null | Количество спутников в решении (GGA). |
|
||||
| `hdop` | float \| null | Horizontal dilution of precision. |
|
||||
|
||||
---
|
||||
|
||||
### 1.4 `GET /api/v1/vessels`
|
||||
|
||||
Все известные MMSI, отсортированные по `last_seen` убыванию.
|
||||
|
||||
**Query-параметры**
|
||||
|
||||
| Параметр | Тип | По умолчанию | Описание |
|
||||
| --- | --- | --- | --- |
|
||||
| `since` | float (unix ts) | — | Вернуть только target'ы с `last_seen >= since`. |
|
||||
| `limit` | int | без лимита | Срез первых N (после сортировки). |
|
||||
|
||||
**Response** — массив объектов `MergedTarget` (см. 1.5).
|
||||
|
||||
---
|
||||
|
||||
### 1.5 `GET /api/v1/vessels/{mmsi}`
|
||||
|
||||
Один target. `mmsi` в URL — целое число.
|
||||
|
||||
**Коды ответа**
|
||||
- `200` — найдено;
|
||||
- `400` — невалидный MMSI;
|
||||
- `404` — `{"error": "not found", "mmsi": <mmsi>}`.
|
||||
|
||||
```json
|
||||
{
|
||||
"mmsi": 506140446,
|
||||
"name": "CELESTIAL STAR",
|
||||
"callsign": "V7AB3",
|
||||
"imo": 9123456,
|
||||
"ship_type": 70,
|
||||
"dims": { "a": 180, "b": 30, "c": 16, "d": 16 },
|
||||
"voyage": {
|
||||
"eta": "05-20 12:00",
|
||||
"draught": 7.5,
|
||||
"destination": "ROTTERDAM"
|
||||
},
|
||||
"dynamic": {
|
||||
"lat": 59.912345,
|
||||
"lon": 10.754321,
|
||||
"sog": 12.4,
|
||||
"cog": 210.3,
|
||||
"heading": 208,
|
||||
"nav_status": 0,
|
||||
"rot": -1.2
|
||||
},
|
||||
"signal": {
|
||||
"last_db": -72.5,
|
||||
"last_ts": 1700000000.123,
|
||||
"last_slot": 1487,
|
||||
"last_channel": "A"
|
||||
},
|
||||
"last_static_ts": 1699990000.0,
|
||||
"last_dynamic_ts": 1700000000.123,
|
||||
"last_seen": 1700000000.123,
|
||||
"sources": ["ais_udp:0.0.0.0:5005:127.0.0.1"],
|
||||
"msg_types": [5, 18, 24]
|
||||
}
|
||||
```
|
||||
|
||||
| Блок | Поле | Тип | Описание |
|
||||
| --- | --- | --- | --- |
|
||||
| root | `mmsi` | int | MMSI (9 цифр для судов, другие диапазоны — base stations / AtoN / SAR и т.д.). |
|
||||
| root | `name` | str \| null | Имя судна (type 5 / 24A). Trim + trailing `@` удалены. |
|
||||
| root | `callsign` | str \| null | Callsign (type 5 / 24B). |
|
||||
| root | `imo` | int \| null | IMO номер (type 5). `0` нормализуется в `null` при наличии. |
|
||||
| root | `ship_type` | int \| null | Ship/cargo type 0–99 (всегда плоский int, не enum). |
|
||||
| `dims` | `a/b/c/d` | int \| null | Расстояния от GPS-антенны: `a`=нос, `b`=корма, `c`=левый борт, `d`=правый борт; метры. Длина = a+b, ширина = c+d. |
|
||||
| `voyage` | `eta` | str \| null | ETA в формате `MM-DD HH:MM` (как отдаёт pyais). |
|
||||
| `voyage` | `draught` | float \| null | Осадка, метры. |
|
||||
| `voyage` | `destination` | str \| null | Порт назначения (до 20 символов). |
|
||||
| `dynamic` | `lat`, `lon` | float \| null | Позиция, градусы WGS-84. |
|
||||
| `dynamic` | `sog` | float \| null | Скорость, узлы (0…102.2). |
|
||||
| `dynamic` | `cog` | float \| null | Course over ground, градусы (0…359.9). |
|
||||
| `dynamic` | `heading` | float \| null | Heading, градусы (0…359, 511=unknown → `null`). |
|
||||
| `dynamic` | `nav_status` | int \| null | Navigational status (0–15). |
|
||||
| `dynamic` | `rot` | float \| null | Rate of turn, град/мин. |
|
||||
| `signal` | `last_db` | float \| null | Уровень сигнала последнего декода (от AIS-catcher decode events, port 10114). |
|
||||
| `signal` | `last_ts` | float \| null | Unix-время последнего decode event. |
|
||||
| `signal` | `last_slot` | int \| null | TDMA-слот (0…2249). |
|
||||
| `signal` | `last_channel` | str \| null | `"A"` или `"B"`. |
|
||||
| root | `last_static_ts` | float | Последний приход type-5 / 24 для этого MMSI. 0 если не было. |
|
||||
| root | `last_dynamic_ts` | float | Последний приход type-1/2/3/18/19/27. |
|
||||
| root | `last_seen` | float | `max(last_static_ts, last_dynamic_ts, last_signal_ts)`. |
|
||||
| root | `sources` | str[] | Отсортированный список source-тегов, с которых приходили данные (см. формат в разделе "Источники"). |
|
||||
| root | `msg_types` | int[] | Отсортированный список типов AIS-сообщений, наблюдавшихся для этого MMSI. |
|
||||
|
||||
---
|
||||
|
||||
### 1.6 `GET /api/v1/base_stations`
|
||||
|
||||
AIS base stations (msg 4/11).
|
||||
|
||||
```json
|
||||
[
|
||||
{ "mmsi": 2570001, "ts": 1700000000.0, "lat": 59.9, "lon": 10.7, "epfd": 1 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.7 `GET /api/v1/atons`
|
||||
|
||||
Aids-to-Navigation (msg 21).
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"mmsi": 992570001,
|
||||
"ts": 1700000000.0,
|
||||
"lat": 59.9, "lon": 10.7,
|
||||
"type": 3,
|
||||
"name": "FLAKFORTET",
|
||||
"virtual": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| --- | --- | --- |
|
||||
| `type` | int \| null | AtoN type (1–31). |
|
||||
| `virtual` | bool \| null | `true` = виртуальный AtoN. |
|
||||
|
||||
---
|
||||
|
||||
### 1.8 `GET /api/v1/slots`
|
||||
|
||||
Снапшот TDMA-слотов и occupancy (наполняется из AIS-catcher, порты 10111/10112).
|
||||
|
||||
```json
|
||||
{
|
||||
"per_channel": {},
|
||||
"occupancy": {
|
||||
"A": {
|
||||
"ts": 1700000060.0,
|
||||
"utc_minute": 28333334,
|
||||
"slots_total": 2250,
|
||||
"occupied_count": 437,
|
||||
"occupied_fraction": 0.1942,
|
||||
"slot0_unix_ms": 1700000040000,
|
||||
"first_occupied_unix_ms": 1700000041123
|
||||
},
|
||||
"B": { ... }
|
||||
},
|
||||
"detail": {
|
||||
"A": {
|
||||
"ts": 1700000060.0,
|
||||
"utc_minute": 28333334,
|
||||
"slot0_unix_ms": 1700000040000,
|
||||
"entries": [
|
||||
{ "slot": 42, "level_db": -72.5 },
|
||||
{ "slot": 1487, "level_db": -80.1 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `per_channel` — зарезервировано (пусто в текущей версии).
|
||||
- `occupancy` — загрузка эфира. Обновляется раз в минуту.
|
||||
- `detail` — список занятых слотов с уровнем сигнала. Обновляется раз в минуту. Может отсутствовать или быть с пустым `entries`.
|
||||
|
||||
---
|
||||
|
||||
### 1.9 `GET /api/v1/radio`
|
||||
|
||||
Мгновенная мощность каналов A/B (от AIS-catcher RSSI, порт 10113, ~10 Гц).
|
||||
|
||||
```json
|
||||
{
|
||||
"power": {
|
||||
"ts": 1700000000.123,
|
||||
"power_a_db": -42.25,
|
||||
"power_b_db": -55.75
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Если RSSI ни разу не приходил — `"power": {}`.
|
||||
|
||||
---
|
||||
|
||||
### 1.10 `GET /api/v1/logs`
|
||||
|
||||
Service logs tail из in-memory ring buffer (не raw NMEA).
|
||||
|
||||
**Query-параметры**
|
||||
|
||||
| Параметр | Тип | По умолчанию | Описание |
|
||||
| --- | --- | --- | --- |
|
||||
| `level` | str | — | Фильтр: `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL` (точный match). |
|
||||
| `limit` | int | 200 | Сколько последних записей вернуть. |
|
||||
| `since` | float | — | Unix-ts; вернуть записи с `ts >= since`. |
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ts": 1700000000.123,
|
||||
"level": "INFO",
|
||||
"logger": "ais_hub.app",
|
||||
"message": "warm start: 42 vessels, 0 base_stations, 0 atons from sqlite"
|
||||
},
|
||||
{
|
||||
"ts": 1700000001.234,
|
||||
"level": "WARNING",
|
||||
"logger": "ais_hub.ingest.ais_udp",
|
||||
"message": "ais_udp: cannot bind 0.0.0.0:5005 — already in use..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.11 `GET /api/v1/nmea/tail`
|
||||
|
||||
Raw NMEA tail — отдельно от `/logs`. Сперва отдаёт in-memory ring buffer (всегда ведётся), при необходимости фоллбэчит в SQLite `raw_nmea` (если `storage.store_raw_nmea: true`).
|
||||
|
||||
**Query-параметры**
|
||||
|
||||
| Параметр | Тип | По умолчанию | Описание |
|
||||
| --- | --- | --- | --- |
|
||||
| `source` | str | — | Фильтр: `ais`, `gps`, `radio`. |
|
||||
| `limit` | int | 200 | Сколько последних строк. |
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ts": 1700000000.123,
|
||||
"source": "ais_udp:0.0.0.0:5005:127.0.0.1",
|
||||
"kind": "ais",
|
||||
"line": "!AIVDM,1,1,,A,15MwkT001s8rFfwJh<dC<mNf0<0Q,0*2F"
|
||||
},
|
||||
{
|
||||
"ts": 1700000000.456,
|
||||
"source": "gps_uart:/dev/ttyS1",
|
||||
"kind": "gps",
|
||||
"line": "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,,,*7A"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.12 `GET /api/v1/history/positions`
|
||||
|
||||
История AIS dynamic для конкретного MMSI из SQLite.
|
||||
|
||||
**Query-параметры**
|
||||
|
||||
| Параметр | Тип | Обязательный | Описание |
|
||||
| --- | --- | --- | --- |
|
||||
| `mmsi` | int | да | MMSI. |
|
||||
| `from` | float (unix ts) | нет | Нижняя граница по `ts`. |
|
||||
| `to` | float (unix ts) | нет | Верхняя граница по `ts`. |
|
||||
| `limit` | int | нет | По умолчанию 1000, максимум 100000. |
|
||||
|
||||
**Коды**
|
||||
- `200` — массив точек (может быть пустой).
|
||||
- `400` — нет/невалидный `mmsi`.
|
||||
- `503` — `{"error": "history storage disabled"}`, если `storage.path` пуст.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ts": 1700000000.123,
|
||||
"lat": 59.912345, "lon": 10.754321,
|
||||
"sog": 12.4, "cog": 210.3,
|
||||
"heading": 208, "nav_status": 0, "rot": -1.2,
|
||||
"msg_type": 18
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Отсортировано по `ts` **убыванию**.
|
||||
|
||||
---
|
||||
|
||||
### 1.13 `POST /api/v1/tx`
|
||||
|
||||
Инжекция NMEA-строк в TX outbox (UDP 127.0.0.1:6010 → SPI bridge).
|
||||
|
||||
**Body (JSON)** — одна из двух форм:
|
||||
|
||||
```json
|
||||
{ "payload": "!AIVDM,1,1,,A,15MwkT001s8rFfwJh<dC<mNf0<0Q,0*2F" }
|
||||
```
|
||||
|
||||
или
|
||||
|
||||
```json
|
||||
{ "nmea": [
|
||||
"!AIVDM,1,1,,A,...,0*2F",
|
||||
"$GPRMC,...,*7A"
|
||||
] }
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
|
||||
```json
|
||||
{
|
||||
"queued": 1,
|
||||
"rejected": [],
|
||||
"queue_depth": 3
|
||||
}
|
||||
```
|
||||
|
||||
**Response (400)** — невалидный JSON или нет `payload`/`nmea`.
|
||||
**Response (503)** — outbox переполнен:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "outbox full",
|
||||
"accepted": ["!AIVDM,..."],
|
||||
"rejected": []
|
||||
}
|
||||
```
|
||||
|
||||
Каждая строка валидируется: структура NMEA + XOR-чексумма `*HH`. Невалидные попадают в `rejected: [{"line": "...", "reason": "invalid nmea or checksum"}]`, валидные — в outbox.
|
||||
|
||||
---
|
||||
|
||||
## 2. WebSocket `/ws`
|
||||
|
||||
**URL:** `ws://<host>:8081/ws`
|
||||
**Subprotocol:** нет.
|
||||
**Heartbeat:** 30 секунд (aiohttp ping, клиент должен отвечать на pong).
|
||||
**Направление:** publish-only от сервера; сообщения от клиента игнорируются.
|
||||
|
||||
### 2.1 Формат конверта
|
||||
|
||||
Все сообщения — JSON одной строкой:
|
||||
|
||||
```json
|
||||
{ "type": "<event_type>", "ts": 1700000000.123, "data": { ... } }
|
||||
```
|
||||
|
||||
### 2.2 Первое сообщение после коннекта — `state.snapshot`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "state.snapshot",
|
||||
"ts": 1700000000.123,
|
||||
"data": {
|
||||
"ownship": { ... }, // см. GET /api/v1/ownship
|
||||
"vessels": [ ... ], // см. GET /api/v1/vessels
|
||||
"base_stations": [ ... ], // см. GET /api/v1/base_stations
|
||||
"atons": [ ... ], // см. GET /api/v1/atons
|
||||
"slots": { ... }, // см. GET /api/v1/slots
|
||||
"stats": { ... } // см. GET /api/v1/stats
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Клиент инициализирует своё состояние из снапшота, затем применяет живые updates.
|
||||
|
||||
### 2.3 Живые события
|
||||
|
||||
| `type` | Когда публикуется | `data` |
|
||||
| --- | --- | --- |
|
||||
| `ownship.update` | На каждый GPS-fix с новыми полями | Объект, идентичный `GET /api/v1/ownship`. |
|
||||
| `target.update` | На каждое AIS dynamic/static для MMSI, которое что-то изменило | Полный `MergedTarget` (см. 1.5). |
|
||||
| `base_station.update` | Type 4/11 | `{mmsi, ts, lat, lon, epfd}`. |
|
||||
| `aton.update` | Type 21 | `{mmsi, ts, lat, lon, type, name, virtual}`. |
|
||||
| `radio.update` (JSON) | Радиотелеметрия в JSON (port `radio_udp`) | `{channel, rssi, snr, slot, raw}`. |
|
||||
| `radio.update` (RSSI) | От AIS-catcher RSSI (10113, ~10 Гц) | `{source: "aiscatcher_rssi", power_a_db, power_b_db}`. Отличайте по `data.source`. |
|
||||
| `slots.update` | Bitmap (10111, ~1/мин) | `{channel, ts, utc_minute, slots_total, occupied_count, occupied_fraction, slot0_unix_ms, first_occupied_unix_ms, bitmap_hex}`. |
|
||||
| `slots.detail` | Detail (10112, ~1/мин) | `{channel, ts, utc_minute, slot0_unix_ms, entries:[{slot, level_db}]}`. |
|
||||
| `signal.update` | Пачка decode events (10114) | `{channel, events:[{unix_ms, slot, mmsi, level_db}]}`. |
|
||||
| `stats.update` | Раз в 5 секунд | Полный snapshot stats (см. 1.2). |
|
||||
|
||||
**Правила апдейта** клиента:
|
||||
- `target.update`: merge по `mmsi`; не терять предыдущие поля (новый пакет с `null` в static не перезаписывает старые имя/IMO — backend этим занимается через `COALESCE` в SQLite и `merge.apply`).
|
||||
- `signal.update`: обновлять sparkline сигнала; `target.update` уже содержит `signal` блок, но `signal.update` приходит чаще (с каждой успешной декодой пакета).
|
||||
- Порядок событий не гарантируется быть идеально хронологическим между разными типами, но в пределах одного `type` он монотонный.
|
||||
|
||||
### 2.4 Обратное давление
|
||||
|
||||
У каждого WS-клиента внутренняя очередь размера `queues.ws_client` (по умолчанию 512). Медленный клиент приводит к drop-oldest; счётчик — `stats.counters.ws_dropped`.
|
||||
|
||||
---
|
||||
|
||||
## 3. UDP интерфейсы (localhost only)
|
||||
|
||||
Все UDP-сокеты слушают/шлют только на `127.0.0.1`.
|
||||
|
||||
### 3.1 UDP JSON events — `127.0.0.1:7001` (publish, out)
|
||||
|
||||
- Контракт: **одно событие = одна UDP-датаграмма**.
|
||||
- Формат: JSON-сериализованный `Event` envelope (такой же, как в WS, но без `state.snapshot`).
|
||||
- Максимальный размер датаграммы: `publish.udp_events.max_datagram_bytes` (1400 по умолчанию). Превышение → drop + `stats.counters.udp_events_oversize`.
|
||||
- Подходит для локальных читателей (BLE-сервер, SPI bridge), которым не нужен full-duplex.
|
||||
|
||||
Пример одной датаграммы:
|
||||
|
||||
```json
|
||||
{"type":"target.update","ts":1700000000.123,"data":{"mmsi":506140446,...}}
|
||||
```
|
||||
|
||||
### 3.2 UDP raw NMEA fan-out — `127.0.0.1:6007` (publish, out)
|
||||
|
||||
- Контракт: **одна NMEA-строка = одна UDP-датаграмма**, завершённая `\r\n`.
|
||||
- Формат: как было получено по UDP/UART (после валидации структуры, но сохраняется исходная `*HH`).
|
||||
- Используется SPI bridge'ем для сквозного форвардинга в MCU.
|
||||
|
||||
Пример одной датаграммы:
|
||||
|
||||
```
|
||||
!AIVDM,1,1,,A,15MwkT001s8rFfwJh<dC<mNf0<0Q,0*2F\r\n
|
||||
```
|
||||
|
||||
### 3.3 UDP TX outbox — `127.0.0.1:6010` (publish, out)
|
||||
|
||||
- Контракт: **одна NMEA-строка = одна UDP-датаграмма**, `\r\n` в конце.
|
||||
- Источник: `POST /api/v1/tx`, внутренние эмиттеры.
|
||||
- Это **не ingest**: сервис только шлёт наружу. Для приёма NMEA использовать `127.0.0.1:5005` (AIS UDP) или UART.
|
||||
|
||||
### 3.4 Ingest AIS UDP — `<ingest.ais_udp>`
|
||||
|
||||
- Порт в конфиге (по умолчанию 4001, у вас на устройстве 5005 — совпадает с AIS-catcher).
|
||||
- Формат входа: **одна или несколько NMEA-строк в одной датаграмме**, разделитель `\r\n`/`\n`.
|
||||
- Сервис декодирует AIVDM/AIVDO и мультифрагментные.
|
||||
|
||||
### 3.5 Ingest Radio UDP (JSON) — `<ingest.radio_udp>`
|
||||
|
||||
- Порт по умолчанию 4010.
|
||||
- Формат: одна датаграмма = один JSON-объект с полями `{channel, rssi, snr, slot, ...}` — любые ключи сохраняются в `raw`.
|
||||
|
||||
### 3.6 Ingest AIS-catcher binary — `<ingest.aiscatcher_udp>`
|
||||
|
||||
Четыре независимых UDP-листенера (все BE, binary, подробные layout'ы — в `src/ais_hub/parser/aiscatcher.py`):
|
||||
|
||||
| Порт по умолчанию | Что | Длина |
|
||||
| --- | --- | --- |
|
||||
| 10111 | Slot bitmap occupancy (per minute) | 315 Б |
|
||||
| 10112 | Slot detail (per-slot levels) | 15 + 6×N |
|
||||
| 10113 | RSSI IQ pair (~10 Гц) | 8 Б |
|
||||
| 10114 | Decode events (mmsi/slot/level) | 3 + 18×N |
|
||||
|
||||
В REST/WS эти потоки отображаются как `/api/v1/radio`, `/api/v1/slots`, `radio.update`, `slots.update`, `slots.detail`, `signal.update`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Источники (`source` / `sources`)
|
||||
|
||||
Формат строки источника в `RawFrame.source`, `sources` array у target'а, в `GET /api/v1/nmea/tail`:
|
||||
|
||||
| Prefix | Пример | Смысл |
|
||||
| --- | --- | --- |
|
||||
| `ais_udp:<listener>:<sender_ip>` | `ais_udp:0.0.0.0:5005:127.0.0.1` | NMEA пришёл по UDP. Sender-порт намеренно не включается (ephemeral, скачет). |
|
||||
| `gps_uart:<device>` | `gps_uart:/dev/ttyS1` | NMEA с UART. |
|
||||
| `radio_udp:<listener>:<sender_ip>` | `radio_udp:0.0.0.0:4010:127.0.0.1` | JSON radio telemetry. |
|
||||
| `ownship` | `ownship` | Используется в WriteJob для `gps_fix` в БД. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Коды ответов
|
||||
|
||||
| HTTP | Когда |
|
||||
| --- | --- |
|
||||
| `200` | OK |
|
||||
| `400` | Невалидные query-параметры / JSON-тело / MMSI. |
|
||||
| `404` | Unknown MMSI в `/api/v1/vessels/{mmsi}`. |
|
||||
| `500` | SQLite query провалился. |
|
||||
| `503` | Storage disabled (`history/positions`) или TX outbox full. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Пример клиента на TypeScript
|
||||
|
||||
```ts
|
||||
type Event = { type: string; ts: number; data: any };
|
||||
|
||||
async function loadInitial() {
|
||||
const [ownship, vessels, stats] = await Promise.all([
|
||||
fetch("/api/v1/ownship").then(r => r.json()),
|
||||
fetch("/api/v1/vessels?limit=500").then(r => r.json()),
|
||||
fetch("/api/v1/stats").then(r => r.json()),
|
||||
]);
|
||||
return { ownship, vessels, stats };
|
||||
}
|
||||
|
||||
function connectWs(onEvent: (ev: Event) => void) {
|
||||
const ws = new WebSocket(`ws://${location.host}/ws`);
|
||||
ws.onmessage = (m) => onEvent(JSON.parse(m.data));
|
||||
ws.onclose = () => setTimeout(() => connectWs(onEvent), 2000);
|
||||
return ws;
|
||||
}
|
||||
```
|
||||
|
||||
Стандартная схема для web UI:
|
||||
1. Загрузить `GET /api/v1/vessels` (для первичного рендера карты).
|
||||
2. Открыть `/ws` — получить `state.snapshot`, заменить состояние им.
|
||||
3. Применять `target.update`/`ownship.update`/и т.д. поверх.
|
||||
4. `signal.update` и `radio.update` (RSSI) — для графиков / индикатора сигнала.
|
||||
5. `slots.update` / `slots.detail` — для визуализации загрузки эфира.
|
||||
|
||||
---
|
||||
|
||||
## 7. Persistence при рестарте
|
||||
|
||||
При старте сервис автоматически загружает из SQLite:
|
||||
|
||||
- `vessel_static` → все `MergedTarget.<name|callsign|imo|ship_type|dim_*|eta|draught|destination>`, а также `last_static_ts`. Dynamic (lat/lon/sog/cog/heading/nav_status/rot) не восстанавливается — придёт с первым новым type-1/2/3/18.
|
||||
- `base_station` → `State.base_stations`.
|
||||
- `aton` → `State.atons`.
|
||||
|
||||
В логе при успешном warm start:
|
||||
|
||||
```
|
||||
INFO warm start: 42 vessels, 0 base_stations, 0 atons from sqlite
|
||||
```
|
||||
|
||||
И счётчики `stats.counters.state_warmup_*`.
|
||||
|
||||
Таким образом, сценарий «принял 5-е и 24-е, перезапустил, принял 18-е» теперь отдаст target с заполненными именем/IMO/размерениями сразу.
|
||||
Reference in New Issue
Block a user