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

