closd TG-6; Initial push after server migration

This commit is contained in:
2026-05-04 08:53:25 +03:00
parent 939f069681
commit 1009f49a59
93 changed files with 16246 additions and 9549 deletions
+245
View File
@@ -0,0 +1,245 @@
# BLE protocol v2 (AIS Hub transport)
Source of truth: `ais_hub`. BLE is **transport** for snapshot + live updates.
## 1. GATT layout
One custom service:
- **Service UUID**: `AIS_HUB_SERVICE_UUID` (see `ble_gatt.py`)
Characteristics:
- **`CONTROL`** (`AIS_HUB_CONTROL_UUID`): `write` / `write-without-response`
Client → server commands as **UTF-8 JSON**.
- **`DATA`** (`AIS_HUB_DATA_UUID`): `notify`
Server → client frames with **binary envelope + JSON payload**.
- **`STATUS`** (`AIS_HUB_STATUS_UUID`): `read` (+ optional `notify`)
Short **UTF-8 JSON** status blob (see section 7).
## 2. DATA frame format (binary envelope)
Every `DATA` notify is one **frame**:
```text
Byte 0 : protocol_version (u8)
Byte 1 : msg_type (u8)
Byte 2-3 : session_msg_id (u16 LE)
Byte 4-5 : chunk_index (u16 LE)
Byte 6-7 : chunk_count (u16 LE)
Byte 8-9 : payload_len (u16 LE)
Byte 10+ : payload bytes (payload_len bytes, UTF-8 JSON)
```
Encoding: little-endian for all multi-byte integers. Header size = **10 bytes**.
If the logical message fits into one frame:
- `chunk_index = 0`
- `chunk_count = 1`
## 3. msg_type enum
Server → client (`DATA`):
| Hex | Name | When |
| --- | --- | --- |
| `0x01` | `HELLO_ACK` | Reply to `hello` |
| `0x02` | `SNAPSHOT_BEGIN` | Snapshot start |
| `0x03` | `SNAPSHOT_CHUNK` | Snapshot section chunk |
| `0x04` | `SNAPSHOT_END` | Snapshot end |
| `0x05` | `EVENT` | Live event from `ais_hub` |
| `0x06` | `STATUS` | Acknowledgements / state updates |
| `0x07` | `ERROR` | Error payload |
| `0x08` | `PONG` | Reply to `ping` |
## 4. CONTROL commands (client → server)
Write UTF-8 JSON into `CONTROL`.
### 4.1 hello
```json
{"cmd":"hello","client":"android","app_version":"1.2.3","proto":1}
```
Reply: `HELLO_ACK` (payload JSON).
### 4.2 get_snapshot
```json
{"cmd":"get_snapshot","include":["ownship","vessels","base_stations","atons","stats"],"max_vessels":500}
```
Reply sequence: `SNAPSHOT_BEGIN` → N×`SNAPSHOT_CHUNK``SNAPSHOT_END`.
### 4.3 subscribe / unsubscribe
```json
{"cmd":"subscribe","events":["ownship.update","target.update","base_station.update","aton.update","stats.update"]}
```
```json
{"cmd":"unsubscribe","events":["stats.update"]}
```
### 4.4 set_filters (reserved)
```json
{"cmd":"set_filters","targets":{"radius_nm":20,"classes":["A","B"]}}
```
### 4.5 ping
```json
{"cmd":"ping","id":123}
```
Reply: `PONG` payload: `{"id":123,"server_time":...}`.
## 5. Chunking algorithm (server side)
Given a logical message payload object:
1. Serialize payload as JSON (UTF-8), no pretty formatting required.
2. Let `max_payload_bytes` be the per-session limit (default 120; may be reduced based on negotiated ATT MTU).
3. Split payload bytes into chunks of at most `max_payload_bytes`.
4. For each chunk:
- Build 10-byte header with the same `session_msg_id`
- Set `chunk_index` from `0..chunk_count-1`
- Set `chunk_count` to total chunks
- Set `payload_len` to the chunk length
- Append `payload bytes`
5. Send frames in order.
Client-side reassembly:
1. Group frames by `(session_msg_id, msg_type)` within the BLE connection.
2. Collect `chunk_count` frames.
3. Concatenate chunk payloads by `chunk_index`.
4. Decode UTF-8 and JSON-parse.
## 6. Snapshot payloads (JSON inside DATA)
### 6.1 SNAPSHOT_BEGIN
```json
{
"snapshot_id": 42,
"sections": ["ownship","vessels","base_stations","atons","stats"],
"total_objects": {"ownship":1,"vessels":183,"base_stations":4,"atons":12,"stats":1}
}
```
### 6.2 SNAPSHOT_CHUNK
For `ownship`/`stats` (single object):
```json
{"snapshot_id":42,"section":"ownship","seq":1,"more":false,"item":{...}}
```
For `vessels`/`base_stations`/`atons` (batched array):
```json
{"snapshot_id":42,"section":"vessels","seq":7,"more":true,"items":[{...},{...}]}
```
`base_stations` items follow `GET /api/v1/base_stations`.
`atons` items follow `GET /api/v1/atons`.
### 6.3 SNAPSHOT_END
```json
{"snapshot_id":42,"ok":true}
```
## 7. STATUS characteristic payload (UTF-8 JSON)
`STATUS.ReadValue` returns compact JSON, example:
```json
{
"proto": 1,
"server_time": 1710000000.123,
"gps_fix": null,
"vessels_active": null,
"ws_source_alive": true,
"snapshot_in_progress": false,
"tx_queue": 0,
"tx_dropped": 0
}
```
## 8. Examples (real frame bytes)
Notation: header is `struct <BBHHHH>` = 10 bytes.
### 8.1 HELLO_ACK example (single chunk)
Assume:
- `protocol_version = 1``01`
- `msg_type = HELLO_ACK (0x01)``01`
- `session_msg_id = 0x002A (42)``2A 00`
- `chunk_index = 0``00 00`
- `chunk_count = 1``01 00`
Payload JSON:
```json
{"ok":true,"proto":1,"server":"ais_ble","server_time":1710000000.0,"features":{"snapshot":true,"live_events":true,"filters":true,"compression":false}}
```
Let `payload_len = 0x0096 (150)` (example value; actual length depends on JSON formatting) → `96 00`.
Frame hex:
```text
01 01 2A 00 00 00 01 00 96 00 7B 22 6F 6B 22 3A 74 72 75 65 ...
```
Where `7B 22 6F 6B ...` is the UTF-8 JSON (`{ "ok":true ... }`).
### 8.2 EVENT example (chunked)
Assume:
- `msg_type = EVENT (0x05)``05`
- `session_msg_id = 0x0042 (66)``42 00`
- payload does not fit → split into 3 chunks (`chunk_count = 3``03 00`)
Chunk #0 header (payload length `0x0078 (120)`):
```text
01 05 42 00 00 00 03 00 78 00 <120 payload bytes>
```
Chunk #1 header:
```text
01 05 42 00 01 00 03 00 78 00 <120 payload bytes>
```
Chunk #2 header (last chunk shorter, e.g. `0x0034 (52)`):
```text
01 05 42 00 02 00 03 00 34 00 <52 payload bytes>
```
Reassembled JSON becomes the original event:
```json
{"type":"target.update","ts":1700000000.123,"data":{"mmsi":506140446,...}}
```
Other live event payloads use the same envelope:
```json
{"type":"base_station.update","ts":1700000000.123,"data":{"mmsi":2570001,"lat":59.9,"lon":10.7,"epfd":1}}
```
```json
{"type":"aton.update","ts":1700000000.123,"data":{"mmsi":992570001,"lat":59.9,"lon":10.7,"type":3,"name":"FLAKFORTET","virtual":false}}
```