Files
AndroidAisMap/BLE_PROTOCOL_V2.md

246 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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}}
```