# Report protocol v2 (JSON) **Status:** schema + TCP slim wire (`STATE_SND`) shipped on adn-server 2.x; monitor 2.x consumer required for production pairing. ## Goals Replace monitor snapshots that today use **pickle** (`CONFIG_SND`, `BRIDGE_SND`) and **CSV strings** (`BRDG_EVENT`) with **typed JSON** that any client can decode. **Release policy:** **adn-server 1.0.x** + **adn-monitor 1.0.x** = report v1 (frozen tag pair). **2.x** server sends **HELLO** with `report_protocol: 2` and v2 JSON on TCP. The monitor connect path is **`HELLO` → `STATE_SND` (`dashboard_state`)** plus async **`ROUTING_TABLE_SND` / `DELTA_SND`** for UA/SINGLE_TS chips and **`VOICE_EVENT_SND`** for live calls. **`topology` is internal-only** (used to build `dashboard_state`, never sent on TCP). **adn-monitor 2.x** decodes v1 wire from legacy peers and v2 JSON from **adn-server 2.x**. **adn-monitor 1.0.0** cannot decode v2 opcodes — upgrade the monitor line for **adn-server 2.x**. ## Transport (unchanged) - TCP **netstring** frames (Twisted `NetstringReceiver`), same port as v1 (`REPORTS.REPORT_PORT`). - Each frame: **1-byte opcode** + **UTF-8 JSON payload** (v2) or pickle/CSV (v1). ### Opcodes | Opcode | Hex | v1 payload | v2 payload | |--------|-----|------------|------------| | `HELLO` | `0xFF` | JSON hello (`protocol`: 1) | JSON hello (`report_protocol`: 2) | | `CONFIG_SND` | `0x01` | pickle SYSTEMS | — (use `STATE_SND`) | | `BRIDGE_SND` | `0x03` | pickle BRIDGES | — (use `ROUTING_TABLE_SND`) | | `BRDG_EVENT` | `0x07` | CSV text | — (use `VOICE_EVENT_SND`) | | `STATE_SND` | `0x14` | — | JSON `dashboard_state` | | `TOPOLOGY_SND` | `0x10` | — | *(internal schema only — not emitted on TCP)* | | `ROUTING_TABLE_SND` | `0x11` | — | JSON `routing_table` | | `VOICE_EVENT_SND` | `0x12` | — | JSON `voice_event` | | `DELTA_SND` | `0x13` | — | JSON `delta` | ```mermaid flowchart TB subgraph v1 [Report v1 — adn-server 1.0.x + monitor 1.0.x] H1[HELLO protocol 1] --> C1[CONFIG_SND pickle] C1 --> B1[BRIDGE_SND pickle] B1 --> V1[BRDG_EVENT CSV] end subgraph v2 [Report v2 — adn-server 2.x + monitor 2.x] H2[HELLO report_protocol 2] --> S2[STATE_SND dashboard_state] S2 --> R2[ROUTING_TABLE_SND JSON async] R2 --> V2[VOICE_EVENT_SND JSON] R2 -.-> D2[DELTA_SND optional] end ``` ## Handshake (`hello`) On connect the server sends **`HELLO` (`0xFF`)** first (same as today). v2 clients inspect `report_protocol`: ```json { "type": "hello", "server": "adn-server", "version": "2.0.0-alpha.1", "report_protocol": 2, "features": ["INGRESS", "END_TX_FORWARD", "PUSH_ON_CONNECT", "REPORT_V2", "ROUTING_TABLE_JSON", "VOICE_EVENT_JSON", "DELTA_UPDATES"], "systems": ["MASTER-A", "OBP-CL"] } ``` | Field | Notes | |-------|--------| | `report_protocol` | **2** for this schema. Distinct from legacy field `protocol: 1`. | | `features` | v1 tokens unchanged; v2 adds `REPORT_V2` and payload capabilities. | Monitor **1.0.x** does not speak this wire; use the **1.0.x** server tag for that pair. ## Message types | `type` | Replaces | Purpose | |--------|----------|---------| | `dashboard_state` | `CONFIG_SND` (slim) | Linked masters/peers/openbridges snapshot (`STATE_SND`). **Full snapshot** — monitor prunes absent masters. | | `topology` | `CONFIG_SND` (full) | Internal schema / ops only — **not** sent on TCP monitor wire. | | `routing_table` | `BRIDGE_SND` | Active bridge legs per talkgroup / reflector key (UA/SINGLE_TS chips). | | `voice_event` | `BRDG_EVENT` | Structured call start/end/ingress. | | `delta` | — | Incremental `routing_table` patch since `since_seq`. | ## Reference payloads Each frame payload is one JSON object with a required `type`. Examples below use anonymized IDs. ### `dashboard_state` ```json { "type": "dashboard_state", "ts": 1717555200.0, "server_id": "7302", "ctable": { "MASTERS": { "MASTER-A": { "mode": "MASTER", "ip": "10.0.0.1", "port": 62030, "peers": { "3120001": { "id": 3120001, "connected": true, "ip": "10.0.0.50", "port": 62031, "callsign": "CE5RPY" } } } }, "PEERS": {}, "OPENBRIDGES": { "OBP-CL": { "mode": "OPENBRIDGE", "network_id": 73010, "ip": "44.31.61.68", "port": 62999, "streams": {} } } } } ``` | Field | Type | Required | Notes | |-------|------|----------|-------| | `ts` | float | yes | Epoch time of snapshot. | | `server_id` | string | no | `GLOBAL.SERVER_ID` (MQTT retained state). | | `ctable.MASTERS` | object | yes | Masters with **connected** peers only. | | `ctable.PEERS` | object | yes | Connected upstream PEER/XLXPEER systems. | | `ctable.OPENBRIDGES` | object | yes | Enabled OpenBridge legs (`streams` empty; live chips from `voice_event`). | Committed example: `schemas/examples/dashboard_state.json`. ### `topology` ```json { "type": "topology", "seq": 1, "ts": 1717555200.0, "systems": [ { "name": "MASTER-A", "mode": "MASTER", "enabled": true, "ip": "10.0.0.1", "port": 62030, "repeat": true, "peers": [ { "id": 3120001, "connected": true, "ip": "10.0.0.50", "port": 62031 } ] }, { "name": "OBP-CL", "mode": "OPENBRIDGE", "enabled": true, "ip": "10.0.0.2", "port": 62044, "enhanced_obp": true, "peers": [] } ] } ``` | Field | Type | Required | Notes | |-------|------|----------|-------| | `seq`, `ts` | int, float | yes | Monotonic sequence and epoch time. | | `systems[].name` | string | yes | System key (matches config). | | `systems[].mode` | string | yes | `MASTER`, `PEER`, or `OPENBRIDGE`. | | `systems[].enabled` | bool | yes | Config enabled flag. | | `systems[].ip`, `port` | string, int | no | Listen/connect endpoint. | | `systems[].repeat` | bool | no | Master repeat flag. | | `systems[].enhanced_obp` | bool | no | OpenBridge enhanced mode. | | `systems[].peers[]` | array | no | `{id, connected, ip?, port?}` per peer radio. | No passwords or encryption material are included (unlike legacy pickle). ### `routing_table` ```json { "type": "routing_table", "seq": 42, "ts": 1717555260.5, "routes": [ { "relay_table_key": "52090", "legs": [ { "system": "MASTER-A", "ts": 2, "tgid": 52090, "active": true, "to_type": "ON", "timer_expires_at": 1717555320.0 }, { "system": "MASTER-B", "ts": 2, "tgid": 52090, "active": true, "to_type": "ON" } ] }, { "relay_table_key": "#310", "legs": [ { "system": "MASTER-A", "ts": 2, "tgid": 310, "active": false, "to_type": "NONE" } ] } ] } ``` | Field | Type | Required | Notes | |-------|------|----------|-------| | `routes[].relay_table_key` | string | yes | Talkgroup id or reflector key (`#nnn`). | | `legs[].system` | string | yes | Target system name. | | `legs[].ts` | 1 \| 2 | yes | Timeslot (legacy `TS`). | | `legs[].tgid` | int | yes | Talkgroup (1–16777215). | | `legs[].active` | bool | yes | Leg active in BRIDGES table. | | `legs[].to_type` | string | yes | `ON`, `OFF`, `STAT`, or `NONE`. | | `legs[].timer_expires_at` | float | no | `rule_timer` expiry (legacy `TIMER`). | ### `voice_event` Legacy CSV: ```text GROUP VOICE,START,RX,MASTER-A,2155905152,1001,3120001,2,52090 ``` v2 equivalent: ```json { "type": "voice_event", "ts": 1717555201.234, "call_family": "GROUP", "phase": "START", "direction": "RX", "system": "MASTER-A", "stream_id": 2155905152, "peer_id": 1001, "src_id": 3120001, "slot": 2, "dst_id": 52090, "duration_s": null, "is_announcement": false } ``` | Field | Type | Required | Values / notes | |-------|------|----------|----------------| | `call_family` | string | yes | `GROUP`, `PRIVATE`, `UNIT`, `VCSSBK`. | | `phase` | string | yes | `INGRESS`, `START`, `END`. | | `direction` | string | yes | `RX`, `TX`. | | `stream_id` | int | yes | 32-bit HBP stream id. | | `peer_id`, `src_id`, `dst_id` | int | yes | DMR ids (1–16777215). | | `slot` | int | yes | `1` or `2`. | | `duration_s` | float \| null | no | Set on `END`. | | `is_announcement` | bool | yes | `true` when the call is the announcement/TTS synthetic PTT, not a real connected peer — the reported `peer_id`/`src_id` must not be attributed to any logged-in bridge. | OpenBridge **INGRESS** vs **START** semantics match [Monitoring and reports](../user-guide/monitoring.md#openbridge-monitor-semantics): use `phase: "INGRESS"` for first sight, `phase: "START"` after loop control. ### `delta` ```json { "type": "delta", "seq": 43, "ts": 1717555261.0, "since_seq": 42, "patch": { "type": "routing_table", "seq": 43, "ts": 1717555261.0, "routes": [ { "relay_table_key": "52090", "legs": [ { "system": "MASTER-A", "ts": 2, "tgid": 52090, "active": false, "to_type": "ON" } ] } ] } } ``` | Field | Type | Required | Notes | |-------|------|----------|-------| | `since_seq` | int | yes | Last `seq` the client applied. | | `patch` | object | yes | Partial `topology` or `routing_table` (same shape). | ## Sequencing - `topology` and `routing_table` messages carry monotonic `seq` (uint) and `ts` (float epoch). - Clients track last applied `seq`; `delta` messages set `since_seq` to the client watermark. - Full snapshots may still be sent on connect and on `REPORTS.REPORT_INTERVAL` (same triggers as v1 CONFIG/BRIDGE). ## Configuration ```yaml REPORTS: REPORT: true REPORT_PORT: 4321 ``` No `PROTOCOL` switch on **2.x** — wire is always JSON (`infrastructure/twisted_adapters/report/wire.py`). Payload mapping: **`application/report/`**. ### Optional MQTT mirror By default the server sends reports **only** over TCP netstring (adn-monitor and other TCP clients). MQTT is **disabled** unless you explicitly enable it. **Enable** only when both are set: 1. `REPORTS.MQTT.ENABLED: true` (boolean `true`, not merely present) 2. `REPORTS.MQTT.URL` — broker URL (`mqtt://host:1883` or `mqtts://host:8883`) ```yaml REPORTS: REPORT: true REPORT_PORT: 4321 MQTT: ENABLED: true URL: mqtt://127.0.0.1:1883 TOPIC_PREFIX: adn/73010 # optional; default adn/{GLOBAL.SERVER_ID} USERNAME: my-mqtt-user # optional; overrides user in URL PASSWORD: my-mqtt-secret # optional; overrides password in URL CAFILE: /path/to/ca.pem # optional; broker TLS trust store (mqtts://) QOS: 0 # optional, 0–2 ``` **Client ID:** auto-generated at startup as `adn-server-{GLOBAL.SERVER_ID}-{random}` (not configurable; random suffix avoids broker session collisions on restart). **Authentication:** username/password via `USERNAME` and `PASSWORD`, or embedded in the URL (`mqtt://user:pass@host:1883`). YAML credentials override URL userinfo. Password may be empty if the broker allows it. With `mqtts://`, set `CAFILE` when the broker uses a private CA. Requires optional dependency: `pip install 'adn-server[mqtt]'` (paho-mqtt). If MQTT is enabled but the library is missing, the server logs an error and continues with TCP only. **MQTT wire (fixed, not configurable):** only **`voice_event`** (telemetry) and **`state`** (snapshot). TCP-only types (`topology`, `routing_table`, `delta`, `hello`) are **not** published on MQTT. **Topic convention** (shared under `{prefix}`): | Topic | Direction | JSON `type` | Retain | |-------|-----------|-------------|--------| | `voice_event` | server → broker | `voice_event` | no | | `state` | server → broker | `dashboard_state` | yes | `state` carries masters with connected peers, homebrew peers, and openbridges (monitor WebSocket `conf,lnksys` + `conf,opb` intent). It is **retained** so new subscribers receive the last snapshot without requesting it. **Topology-driven refreshes** republish `{prefix}/state` when the dashboard changes (dedup). **Triggers:** live `voice_event`; retained `{prefix}/state` on topology changes and after MQTT connect (dedup). **Example** (`SERVER_ID` 7302): ```bash mosquitto_sub -h BROKER -p 1883 -u USER -P PASS -t 'adn/7302/state' -v mosquitto_sub -h BROKER -p 1883 -u USER -P PASS -t 'adn/7302/voice_event' -v ``` **Broker ACL:** consumers need **subscribe** on `adn/7302/state` and `adn/7302/voice_event`; the server `client_id` needs **publish** on those two topics only (no server-side subscribe). **Reload (`systemctl reload` / SIGHUP):** when `REPORTS.MQTT` changes (enable/disable, URL, credentials, TLS, `TOPIC_PREFIX`, `QOS`), the server disconnects the old MQTT client and connects with the new settings, or stays offline if `ENABLED` becomes false. **QoS:** configurable via `MQTT.QOS` (default `0`). **`rule_timer`**, **`stat_trimmer`**, and **`bridgeDebug`** use **routing deltas** when only part of `BRIDGES` changed; connect, `REPORT_INTERVAL`, reload, and client `CONFIG_REQ` / `BRIDGE_REQ` send **full** snapshots. ## Version pairing (supported combinations) | Server | Monitor | Report wire | Notes | |--------|---------|-------------|--------| | **1.0.x** | **1.0.x** | v1 (pickle/CSV) | Frozen pair; no cross-upgrade of report protocol | | **2.0.0-alpha.\*** | **2.x** (dev) | v2 only | Current `develop` line | | **2.0.0** | **2.0.x** | v2 only | GA pair; monitor 1.0.x **not** supported | Do **not** run monitor 1.0.x against server 2.0.0 or monitor 2.x against server 1.0.x for production. See also [Monitoring and reports](../user-guide/monitoring.md).