157 lines
5.5 KiB
Markdown
157 lines
5.5 KiB
Markdown
# ais_hub
|
|
|
|
Central AIS/GPS/radio telemetry aggregation service for embedded Linux.
|
|
|
|
`ais_hub` is a Python 3.11 asyncio service. It ingests raw AIS (UDP), GPS
|
|
(UART) and radio-telemetry (UDP) streams, reassembles multi-fragment AIS
|
|
sentences, maintains a single authoritative in-memory state keyed by MMSI,
|
|
writes history to SQLite (WAL + retention), and publishes data to external
|
|
processes (BLE server, web UI, SPI bridge) via stable interfaces.
|
|
|
|
No BLE, web UI or MCU code lives here; those are separate processes that
|
|
consume the interfaces below.
|
|
|
|
## Interfaces
|
|
|
|
Полный JSON-контракт с примерами для фронтенда — в **[docs/API.md](docs/API.md)**.
|
|
Короткая сводка ниже.
|
|
|
|
### HTTP (REST, `GET` unless noted)
|
|
|
|
| Path | Purpose |
|
|
| --- | --- |
|
|
| `/api/v1/health` | Liveness + uptime + version. |
|
|
| `/api/v1/stats` | Counters, gauges, uptime. |
|
|
| `/api/v1/ownship` | Latest GPS fix. |
|
|
| `/api/v1/vessels` | All merged targets. Query: `since`, `limit`. |
|
|
| `/api/v1/vessels/{mmsi}` | One merged target. |
|
|
| `/api/v1/base_stations` | AIS base station snapshots. |
|
|
| `/api/v1/atons` | AtoN snapshots. |
|
|
| `/api/v1/slots` | TDMA slot occupancy + per-slot detail from AIS-catcher. |
|
|
| `/api/v1/radio` | Latest RSSI pair (channel A/B) from AIS-catcher. |
|
|
| `/api/v1/logs` | **Service logs tail**, from in-memory ring. Query: `level`, `limit`, `since`. |
|
|
| `/api/v1/nmea/tail` | **Raw NMEA tail** (separate from `/logs`). Query: `source=ais\|gps\|radio`, `limit`. |
|
|
| `/api/v1/history/positions` | AIS dynamic history. Query: `mmsi`, `from`, `to`, `limit`. |
|
|
| `POST /api/v1/tx` | Inject NMEA sentence(s) into the TX outbox. Body: `{"payload": "!AIVDM,..."}` or `{"nmea": ["...", "..."]}`. |
|
|
|
|
### WebSocket
|
|
|
|
- `GET /ws` — on connect, clients receive a full `state.snapshot` event
|
|
(ownship, vessels, base_stations, atons, slots, stats). Then a live
|
|
stream of `Event` envelopes:
|
|
- `ownship.update`, `target.update`, `base_station.update`,
|
|
`aton.update`, `radio.update`, `slots.update`, `slots.detail`,
|
|
`signal.update`, `stats.update`.
|
|
- Each client has its own bounded queue; slow consumers drop oldest
|
|
events and the `ws_dropped` counter increments.
|
|
|
|
### Local UDP
|
|
|
|
| Endpoint | Direction | Contract |
|
|
| --- | --- | --- |
|
|
| `127.0.0.1:7001` | ais_hub → external | JSON `Event`, one event per datagram, dropped if > `max_datagram_bytes`. |
|
|
| `127.0.0.1:6007` | ais_hub → SPI bridge | Raw NMEA fan-out, one sentence per datagram (`<line>\r\n`). Untouched from ingest. |
|
|
| `127.0.0.1:6010` | ais_hub → SPI bridge | **TX publish outbox**. One NMEA sentence per datagram. Fed by `POST /api/v1/tx` and internal emitters. |
|
|
|
|
ais_hub does **not** listen on `:6010`; it is publish-only.
|
|
|
|
## Layout
|
|
|
|
- `src/ais_hub/ingest/` — AIS UDP / GPS UART / radio UDP listeners, raw tap.
|
|
- `src/ais_hub/parser/` — NMEA utils, AIS multi-fragment assembler, GPS,
|
|
radio telemetry parsers.
|
|
- `src/ais_hub/core/` — domain models, in-memory state, eager per-MMSI
|
|
merge, stats counters, internal pub/sub bus.
|
|
- `src/ais_hub/storage/` — aiosqlite DB (WAL), batched writer with retry,
|
|
retention cleanup, read queries for REST.
|
|
- `src/ais_hub/publish/` — aiohttp REST + WS server, UDP publishers,
|
|
bounded queue helpers with drop-oldest policy.
|
|
- `src/ais_hub/app.py` — orchestrator (supervisors, graceful shutdown).
|
|
|
|
## Install
|
|
|
|
```bash
|
|
pip install -e .
|
|
```
|
|
|
|
Runtime dependencies: `aiohttp`, `pyais`, `pyserial-asyncio`, `aiosqlite`,
|
|
`PyYAML`.
|
|
|
|
## Run
|
|
|
|
```bash
|
|
ais_hub --config config/config.example.yaml
|
|
```
|
|
|
|
### Environment variable overrides
|
|
|
|
Env vars override any YAML value. Prefix `AIS_HUB_`, level separator
|
|
`__`, upper-case. Examples:
|
|
|
|
```bash
|
|
AIS_HUB_INGEST__AIS_UDP__PORT=4011
|
|
AIS_HUB_STORAGE__PATH=/data/ais_hub.db
|
|
AIS_HUB_STORAGE__STORE_RAW_NMEA=true
|
|
AIS_HUB_LOGGING__LEVEL=DEBUG
|
|
```
|
|
|
|
## Deploy on embedded Linux (systemd)
|
|
|
|
1. Create user and directories:
|
|
|
|
```bash
|
|
useradd -r -s /sbin/nologin ais_hub
|
|
install -d -o ais_hub -g ais_hub /var/lib/ais_hub /var/log/ais_hub /etc/ais_hub
|
|
```
|
|
|
|
2. Install package (system-wide or in a venv the unit points at):
|
|
|
|
```bash
|
|
pip install .
|
|
```
|
|
|
|
3. Copy config:
|
|
|
|
```bash
|
|
install -m 0644 config/config.example.yaml /etc/ais_hub/config.yaml
|
|
```
|
|
|
|
4. Install unit and enable:
|
|
|
|
```bash
|
|
install -m 0644 systemd/ais_hub.service /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
systemctl enable --now ais_hub
|
|
journalctl -u ais_hub -f
|
|
```
|
|
|
|
For UART access, add the `ais_hub` user to the serial/dialout group or
|
|
grant access via a udev rule on `/dev/ttyS1`.
|
|
|
|
## Resilience notes
|
|
|
|
- Each subsystem runs under a supervisor task: on exception it is logged
|
|
and restarted with exponential backoff; the process itself never dies
|
|
because of a single subsystem failure.
|
|
- All inter-subsystem queues are bounded with drop-oldest. Per-channel
|
|
drop counters are exposed via `/api/v1/stats`:
|
|
- `parser_in_dropped`, `udp_nmea_dropped`,
|
|
- `storage_in_dropped`, `tx_outbox_dropped`,
|
|
- `bus_dropped`, `ws_dropped`.
|
|
- SQLite transient errors are retried up to 3 times with exponential
|
|
backoff; on persistent failure the batch is dropped, the writer keeps
|
|
draining the queue, and REST history may return an error while the
|
|
rest of the service continues to serve live data.
|
|
- Multi-fragment AIS is keyed by
|
|
`(source_tag, talker, channel, seq_id, total_fragments)`. Fragment
|
|
ordering is strictly validated; any deviation resets the per-key
|
|
buffer and increments `ais_fragment_errors`. Stale buffers are GC'd
|
|
after 60 s (`ais_fragment_timeouts`).
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
pip install -e .[dev]
|
|
pytest
|
|
```
|