# 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 ` = 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}} ```