Ports hblink3's dashboard-reporting overhaul to dmrlink3's IPSC path so the
two converge (plan: ~/development/dashboard-reporting-normalization.md):
- Granular peer events: a centralized reconciliation sweep in both the master
and peer maintenance loops (report_maintenance) diffs currently-connected
peers vs the last-reported set and emits send_peer connected/disconnected
deltas only on change -- the dashboard reflects membership immediately
instead of waiting for the periodic full push. (The single master link-state,
in PEER mode, continues to ride the snapshot.)
- Demote the full push: reporting_loop now calls periodic_push, which re-pushes
the full config+bridge snapshot only every REPORT_RESYNC_SECONDS (60s) as a
reconciliation safety net and sends a tiny {'type':'ping'} heartbeat on the
other ticks. Idle feed stops re-serializing full state every REPORT_INTERVAL.
- Ping-loss quality metric (PING_LOSS): detects a peer/master that STAYS
CONNECTED but loses keepalives (lossy link -> choppy audio). One uniform
gap-inference path serves both roles -- MASTER mode observes peers'
MASTER_ALIVE_REQs, PEER mode observes the master's/peers' *_ALIVE_REPLYs to
our requests. IPSC's cadence is known (ALIVE_TIMER) so no self-calibration is
needed (unlike hblink3). A gap beyond the keepalive-timeout horizon is treated
as a session lapse (reconnect), not loss. Sliding window pruned to
PING_LOSS_WINDOW minutes; dashboard flags the row gold at >= PING_LOSS_WARN%.
Config: PING_LOSS_WINDOW / PING_LOSS_WARN added to GLOBAL (optional, defaults).
Dashboard: backend applies peer deltas + ignores the ping heartbeat; frontend
renders "N% ping loss" and golds the radio id. Tests: TestPeerEvents +
TestPingLoss (73 pass). Verified live over real sockets (peer delta fired,
heartbeat/resync split confirmed, PING_LOSS climbed to 30% under a lossy peer)
and deployed to the live dmrlink3 services clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two independent gaps in test infrastructure:
1. helpers.make_config() was missing GLOBAL.SUB_ACL and
helpers.make_system() was missing LOCAL.REG_ACL.
In production, process_acls() populates both at startup; the test
helpers never called it. Added PERMIT:ALL defaults matching what
acl_build('PERMIT:ALL', ID_MAX/PEER_MAX) returns.
2. BridgeBase.setUp set bridge.BRIDGES but never called index_bridges()
to populate BRIDGE_SRC_INDEX / BRIDGE_BY_SYSTEM. The routing loop
iterates those indexes (not BRIDGES directly), so all forwarding tests
silently found nothing to forward. Added a _set_bridges() helper that
sets BRIDGES and rebuilds the indexes atomically; updated every
callsite that was assigning bridge.BRIDGES directly. TestCallBoundary
now saves/restores the indexes alongside BRIDGES.
Also removed the _PERMIT_ALL workaround from test_trunk_status.py now
that make_config() supplies SUB_ACL by default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A TRUNK system carries an arbitrary number of simultaneous streams, so
the previous TS-keyed STATUS dict was semantically wrong and never
actually read by any code (contention checks are bypassed for trunk
targets via TRUNKS list).
- trunk.py TRUNK.__init__: self.STATUS = {} with explanatory comment
- bridge.py bridgeIPSC.group_voice: wrap post-transmit TX state writes
in `if _target['SYSTEM'] not in TRUNKS` — trunk targets have no TS
slots to write into, and the values would never be consulted
- bridge.py bridgeTRUNK.group_voice: same TX guard + remove the final
two RX state writes (self.STATUS[_ts]['RX_TGID/RX_TIME']) that would
KeyError against the now-empty STATUS dict
13 new tests in tests/test_trunk_status.py all pass. The 33
pre-existing failures in test_bridge.py / test_protocol.py (missing
SUB_ACL in test helpers) are unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bridge fixes:
- Move ON triggers from VOICE_TERM to VOICE_HEAD so bridge activates on
key-down and the VOICE_HEAD packet itself is forwarded immediately
- Initialize TIMER to time()+TIMEOUT instead of time() so TO_TYPE=ON entries
that start ACTIVE=True don't get killed by the first rule_timer_loop run
Dashboard:
- Replace per-peer slot pills with system-level TS1/TS2 pills showing RX/TX
state, source radio, peer, TGID, call type, and elapsed duration
- Self row removed from peer table; pills move to the Peers heading row
Base class (dmrlink.py):
- Move TS_CLEAR_TIME to dmrlink.py as single source of truth
- Add rx_start/tx_start per-TS call boundary tracking to IPSC base class
- Add transmit_group_voice() for TX call tracking with bridge events
- Add send_bridge_event() stub to base ReportServer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>