|
|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
"""Report wire: typed JSON topology / routing_table / voice_event / delta."""
|
|
|
|
|
"""Report wire: slim dashboard_state + voice_event to monitor (D-25)."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
@ -11,16 +11,12 @@ from adn_server.application.ports import ReportWireEncoder
|
|
|
|
|
from adn_server.application.report import (
|
|
|
|
|
REPORT_FEATURES,
|
|
|
|
|
REPORT_PROTOCOL,
|
|
|
|
|
build_routing_table,
|
|
|
|
|
build_topology,
|
|
|
|
|
build_dashboard_state,
|
|
|
|
|
hello_connected_system_names,
|
|
|
|
|
parse_bridge_event_csv,
|
|
|
|
|
routing_table_delta,
|
|
|
|
|
topology_delta,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from .opcodes import REPORT_OPCODES, SERVER_NAME, server_version
|
|
|
|
|
from .pickle_legacy import encode_config_snd_frame
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
@ -29,14 +25,19 @@ def _json_wire(opcode: bytes, payload: dict[str, Any]) -> bytes:
|
|
|
|
|
return opcode + json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _state_dedup_key(payload: dict[str, Any]) -> bytes:
|
|
|
|
|
return json.dumps(
|
|
|
|
|
{"ctable": payload.get("ctable"), "server_id": payload.get("server_id")},
|
|
|
|
|
separators=(",", ":"),
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReportWire(ReportWireEncoder):
|
|
|
|
|
"""Stateful report encoder — seq counters and delta patches."""
|
|
|
|
|
"""Slim monitor encoder — ``dashboard_state`` snapshots + ``voice_event`` only."""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self._topology_seq = 0
|
|
|
|
|
self._routing_seq = 0
|
|
|
|
|
self._last_topology: dict[str, Any] | None = None
|
|
|
|
|
self._last_routing: dict[str, Any] | None = None
|
|
|
|
|
self._last_state_key: bytes | None = None
|
|
|
|
|
|
|
|
|
|
def hello_frames(self, systems: dict[str, Any]) -> tuple[bytes, ...]:
|
|
|
|
|
names = hello_connected_system_names(systems)
|
|
|
|
|
@ -53,48 +54,27 @@ class ReportWire(ReportWireEncoder):
|
|
|
|
|
return (REPORT_OPCODES["HELLO"] + payload,)
|
|
|
|
|
|
|
|
|
|
def config_frames(self, systems: dict[str, Any], *, full_snapshot: bool) -> tuple[bytes, ...]:
|
|
|
|
|
return self.state_frames(systems, force=full_snapshot)
|
|
|
|
|
|
|
|
|
|
def state_frames(self, systems: dict[str, Any], *, force: bool = False) -> tuple[bytes, ...]:
|
|
|
|
|
ts = time.time()
|
|
|
|
|
if full_snapshot or self._last_topology is None:
|
|
|
|
|
self._topology_seq += 1
|
|
|
|
|
topology = build_topology(systems, seq=self._topology_seq, ts=ts)
|
|
|
|
|
self._last_topology = topology
|
|
|
|
|
logger.debug("(REPORT) CONFIG_SND pickle + TOPOLOGY_SND seq=%s", self._topology_seq)
|
|
|
|
|
return (
|
|
|
|
|
encode_config_snd_frame(systems),
|
|
|
|
|
_json_wire(REPORT_OPCODES["TOPOLOGY_SND"], topology),
|
|
|
|
|
)
|
|
|
|
|
self._topology_seq += 1
|
|
|
|
|
current = build_topology(systems, seq=self._topology_seq, ts=ts)
|
|
|
|
|
delta = topology_delta(self._last_topology, current, seq=self._topology_seq, ts=ts)
|
|
|
|
|
if delta is None:
|
|
|
|
|
self._topology_seq -= 1
|
|
|
|
|
payload = build_dashboard_state(systems, ts=ts)
|
|
|
|
|
key = _state_dedup_key(payload)
|
|
|
|
|
if not force and self._last_state_key == key:
|
|
|
|
|
logger.debug("(REPORT) STATE_SND unchanged, skip")
|
|
|
|
|
return ()
|
|
|
|
|
self._last_topology = current
|
|
|
|
|
logger.debug("(REPORT) DELTA_SND topology seq=%s", delta["seq"])
|
|
|
|
|
return (_json_wire(REPORT_OPCODES["DELTA_SND"], delta),)
|
|
|
|
|
self._last_state_key = key
|
|
|
|
|
logger.debug("(REPORT) STATE_SND ts=%s", payload.get("ts"))
|
|
|
|
|
return (_json_wire(REPORT_OPCODES["STATE_SND"], payload),)
|
|
|
|
|
|
|
|
|
|
def bridge_frames(self, bridges: dict[str, Any], *, full_snapshot: bool) -> tuple[bytes, ...]:
|
|
|
|
|
ts = time.time()
|
|
|
|
|
if full_snapshot or self._last_routing is None:
|
|
|
|
|
self._routing_seq += 1
|
|
|
|
|
routing = build_routing_table(bridges, seq=self._routing_seq, ts=ts)
|
|
|
|
|
self._last_routing = routing
|
|
|
|
|
logger.debug("(REPORT) ROUTING_TABLE_SND seq=%s", self._routing_seq)
|
|
|
|
|
return (_json_wire(REPORT_OPCODES["ROUTING_TABLE_SND"], routing),)
|
|
|
|
|
self._routing_seq += 1
|
|
|
|
|
current = build_routing_table(bridges, seq=self._routing_seq, ts=ts)
|
|
|
|
|
delta = routing_table_delta(self._last_routing, current, seq=self._routing_seq, ts=ts)
|
|
|
|
|
if delta is None:
|
|
|
|
|
self._routing_seq -= 1
|
|
|
|
|
return ()
|
|
|
|
|
self._last_routing = current
|
|
|
|
|
logger.debug("(REPORT) DELTA_SND routing seq=%s", delta["seq"])
|
|
|
|
|
return (_json_wire(REPORT_OPCODES["DELTA_SND"], delta),)
|
|
|
|
|
"""Routing is not exported to the monitor (D-25)."""
|
|
|
|
|
return ()
|
|
|
|
|
|
|
|
|
|
def bridge_event_frames(self, event: str) -> tuple[bytes, ...]:
|
|
|
|
|
voice = parse_bridge_event_csv(event)
|
|
|
|
|
if voice is None:
|
|
|
|
|
logger.warning("(REPORT) BRDG_EVENT not mapped to voice_event: %s", event[:120])
|
|
|
|
|
logger.warning("(REPORT) voice_event not emitted (unmapped CSV): %s", event[:120])
|
|
|
|
|
return ()
|
|
|
|
|
logger.debug(
|
|
|
|
|
"(REPORT) VOICE_EVENT_SND %s %s %s",
|
|
|
|
|
|