5 Commits
Author SHA1 Message Date
Cort BuffingtonandClaude Opus 4.8 ad4e0ed13a reporting: mirror dashboard normalization to dmrlink3 (Step 3)
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>
2026-07-12 10:00:33 -05:00
Cort BuffingtonandClaude Sonnet 4.6 2a99e359b7 tests: fix 33 pre-existing failures caused by missing ACL keys and stale routing indexes
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>
2026-06-28 17:55:33 -05:00
Cort BuffingtonandClaude Sonnet 4.6 94e145ec7c trunk: STATUS is {}; routing core skips TX/RX writes for trunk systems
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>
2026-06-28 17:42:24 -05:00
Cort BuffingtonandClaude Sonnet 4.6 09c9258226 Fix bridge activation edge + timer init; add system-level TS activity pills
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>
2026-06-26 20:51:22 -05:00
Cort BuffingtonandClaude Sonnet 4.6 b41e3f13b6 Add test suite: 49 unit tests for IPSC protocol and bridge routing
tests/helpers.py       — MockDatagramTransport, config factories, GV packet builder
tests/test_protocol.py — registration, de-reg, keepalive, peer-list unicast,
                         timeslot detection (the TS_CALL_MSK bug fix), unknown opcodes
tests/test_bridge.py   — forwarding, packet rewriting (peer-id, TGID, TS, burst-type),
                         per-TS call_start tracking, Talker Alias seq_id churn immunity,
                         contention handling (hangtime, TRUNKS bypass)
tests/fake_ipsc_peer.py — standalone asyncio tool for live integration testing;
                         registers with dmrlink3, sends keepalives, supports --auto-call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:53:54 -05:00