Define typed monitor payloads (hello, topology, routing_table, voice_event, delta) with JSON Schema, anonymized examples, pytest validation, and EN/ES docs.pull/1/head
parent
34448e4fe0
commit
af4264100e
@ -0,0 +1,25 @@
|
||||
{
|
||||
"type": "delta",
|
||||
"seq": 43,
|
||||
"ts": 1717555261.0,
|
||||
"since_seq": 42,
|
||||
"patch": {
|
||||
"type": "routing_table",
|
||||
"seq": 43,
|
||||
"ts": 1717555261.0,
|
||||
"routes": [
|
||||
{
|
||||
"bridge_key": "52090",
|
||||
"legs": [
|
||||
{
|
||||
"system": "MASTER-A",
|
||||
"ts": 2,
|
||||
"tgid": 52090,
|
||||
"active": false,
|
||||
"to_type": "ON"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "hello",
|
||||
"server": "adn-server",
|
||||
"version": "2.0.0-alpha.1",
|
||||
"report_protocol": 2,
|
||||
"features": [
|
||||
"INGRESS",
|
||||
"END_TX_FORWARD",
|
||||
"PUSH_ON_CONNECT",
|
||||
"REPORT_V2",
|
||||
"TOPOLOGY_JSON",
|
||||
"ROUTING_TABLE_JSON",
|
||||
"VOICE_EVENT_JSON",
|
||||
"DELTA_UPDATES"
|
||||
],
|
||||
"systems": ["MASTER-A", "MASTER-B", "OBP-CL"]
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "routing_table",
|
||||
"seq": 42,
|
||||
"ts": 1717555260.5,
|
||||
"routes": [
|
||||
{
|
||||
"bridge_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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bridge_key": "#310",
|
||||
"legs": [
|
||||
{
|
||||
"system": "MASTER-A",
|
||||
"ts": 2,
|
||||
"tgid": 310,
|
||||
"active": false,
|
||||
"to_type": "NONE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://adn.systems/schemas/report-v2.json",
|
||||
"title": "ADN Monitor Report Protocol v2",
|
||||
"description": "JSON payloads for the TCP report channel (netstring-framed). Legacy v1 uses pickle CONFIG_SND/BRIDGE_SND and CSV BRDG_EVENT; v2 replaces those with typed JSON. Wire opcodes are defined in docs/en/server/protocols/report-v2.md; implementation is V2-P1-002.",
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/hello" },
|
||||
{ "$ref": "#/$defs/topology" },
|
||||
{ "$ref": "#/$defs/routing_table" },
|
||||
{ "$ref": "#/$defs/voice_event" },
|
||||
{ "$ref": "#/$defs/delta" }
|
||||
],
|
||||
"$defs": {
|
||||
"dmr_id": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 16777215,
|
||||
"description": "DMR radio ID or talkgroup (24-bit)."
|
||||
},
|
||||
"stream_id": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295,
|
||||
"description": "HBP stream identifier (32-bit, unsigned)."
|
||||
},
|
||||
"slot": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2]
|
||||
},
|
||||
"system_mode": {
|
||||
"type": "string",
|
||||
"enum": ["MASTER", "PEER", "OPENBRIDGE"]
|
||||
},
|
||||
"to_type": {
|
||||
"type": "string",
|
||||
"enum": ["ON", "OFF", "STAT", "NONE"],
|
||||
"description": "Legacy BRIDGES TO_TYPE semantics until subscription model (Phase 2)."
|
||||
},
|
||||
"hello": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "server", "version", "report_protocol", "features"],
|
||||
"properties": {
|
||||
"type": { "const": "hello" },
|
||||
"server": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string", "minLength": 1, "description": "adn-server package version." },
|
||||
"report_protocol": {
|
||||
"type": "integer",
|
||||
"const": 2,
|
||||
"description": "Report payload schema version (distinct from legacy HELLO protocol field value 1)."
|
||||
},
|
||||
"features": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"uniqueItems": true,
|
||||
"description": "Capability tokens. v1: INGRESS, END_TX_FORWARD, PUSH_ON_CONNECT. v2 adds REPORT_V2, TOPOLOGY_JSON, ROUTING_TABLE_JSON, VOICE_EVENT_JSON, DELTA_UPDATES."
|
||||
},
|
||||
"systems": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Optional system names known at handshake (full topology follows in topology message)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"topology_system": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "mode", "enabled"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"mode": { "$ref": "#/$defs/system_mode" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"ip": { "type": "string" },
|
||||
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
||||
"repeat": { "type": "boolean" },
|
||||
"enhanced_obp": { "type": "boolean" },
|
||||
"peers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "connected"],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/dmr_id" },
|
||||
"connected": { "type": "boolean" },
|
||||
"ip": { "type": "string" },
|
||||
"port": { "type": "integer", "minimum": 0, "maximum": 65535 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"topology": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "seq", "ts", "systems"],
|
||||
"properties": {
|
||||
"type": { "const": "topology" },
|
||||
"seq": { "type": "integer", "minimum": 0 },
|
||||
"ts": { "type": "number", "description": "Unix epoch seconds (fractional allowed)." },
|
||||
"systems": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/topology_system" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"routing_leg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["system", "ts", "tgid", "active", "to_type"],
|
||||
"properties": {
|
||||
"system": { "type": "string" },
|
||||
"ts": { "$ref": "#/$defs/slot" },
|
||||
"tgid": { "$ref": "#/$defs/dmr_id" },
|
||||
"active": { "type": "boolean" },
|
||||
"to_type": { "$ref": "#/$defs/to_type" },
|
||||
"timer_expires_at": {
|
||||
"type": "number",
|
||||
"description": "Unix time when rule_timer deactivates leg (legacy TIMER field)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"routing_table": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "seq", "ts", "routes"],
|
||||
"properties": {
|
||||
"type": { "const": "routing_table" },
|
||||
"seq": { "type": "integer", "minimum": 0 },
|
||||
"ts": { "type": "number" },
|
||||
"routes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["bridge_key", "legs"],
|
||||
"properties": {
|
||||
"bridge_key": {
|
||||
"type": "string",
|
||||
"description": "Talkgroup id string or reflector key (#xxx)."
|
||||
},
|
||||
"legs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/routing_leg" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"voice_event": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"ts",
|
||||
"call_family",
|
||||
"phase",
|
||||
"direction",
|
||||
"system",
|
||||
"stream_id",
|
||||
"peer_id",
|
||||
"src_id",
|
||||
"slot",
|
||||
"dst_id"
|
||||
],
|
||||
"properties": {
|
||||
"type": { "const": "voice_event" },
|
||||
"ts": { "type": "number" },
|
||||
"call_family": {
|
||||
"type": "string",
|
||||
"enum": ["GROUP", "PRIVATE", "UNIT", "VCSSBK"],
|
||||
"description": "Replaces BRDG_EVENT prefix (e.g. GROUP VOICE)."
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"enum": ["INGRESS", "START", "END"]
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"enum": ["RX", "TX"]
|
||||
},
|
||||
"system": { "type": "string" },
|
||||
"stream_id": { "$ref": "#/$defs/stream_id" },
|
||||
"peer_id": { "$ref": "#/$defs/dmr_id" },
|
||||
"src_id": { "$ref": "#/$defs/dmr_id" },
|
||||
"slot": { "$ref": "#/$defs/slot" },
|
||||
"dst_id": { "$ref": "#/$defs/dmr_id" },
|
||||
"duration_s": {
|
||||
"type": ["number", "null"],
|
||||
"minimum": 0,
|
||||
"description": "Present on END events."
|
||||
}
|
||||
}
|
||||
},
|
||||
"delta": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "seq", "ts", "since_seq", "patch"],
|
||||
"properties": {
|
||||
"type": { "const": "delta" },
|
||||
"seq": { "type": "integer", "minimum": 1 },
|
||||
"ts": { "type": "number" },
|
||||
"since_seq": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Client last applied seq; server sends changes after this."
|
||||
},
|
||||
"patch": {
|
||||
"description": "Partial topology or routing_table payload (same shape, may omit unchanged branches).",
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/topology" },
|
||||
{ "$ref": "#/$defs/routing_table" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
"""Validate report v2 JSON Schema against committed examples (V2-P1-001)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
|
||||
_SCHEMA_PATH = Path(__file__).resolve().parents[2] / "schemas" / "report-v2.json"
|
||||
_EXAMPLES_DIR = _SCHEMA_PATH.parent / "examples"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def report_v2_schema() -> dict:
|
||||
with _SCHEMA_PATH.open(encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def validator(report_v2_schema: dict) -> jsonschema.Draft202012Validator:
|
||||
return jsonschema.Draft202012Validator(report_v2_schema)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", sorted(_EXAMPLES_DIR.glob("*.json")), ids=lambda p: p.stem)
|
||||
def test_example_validates(path: Path, validator: jsonschema.Draft202012Validator) -> None:
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
doc = json.load(fh)
|
||||
validator.validate(doc)
|
||||
Loading…
Reference in new issue