Adds self-echo (a hotspot with the same TG on both slots, static or dynamic, hears its own TX on the other slot) and fixes four occurrences of the same slot-resolution bug that blocked or truncated it: peer_downlink_voice_slot always preferred an unambiguous static/SINGLE=1-locked slot over the caller's own wire slot, corrupting the busy-check and the parrot/echo anti-loopback guard alike. Also makes the "Downlink dropped" log reason specific instead of generic, and throttles repeated identical drop logs.
* fix: audit wave 1 server hygiene refactors
Inject call_later into PlaybackUseCases, move voice config mtime watch to
bootstrap, complete SubscriptionStore port methods, and relocate echo
routing seed to the application layer.
* fix: document dashboard_state in report-v2 schema
Add dashboard_state to report-v2.json with a two-master example fixture,
update public protocol docs for HELLO → STATE_SND connect flow, and drop
the unused TOPOLOGY_JSON HELLO feature token.
* fix: add ReportWire contract tests against report-v2 schema
Assert state_frames and bridge_event_frames output validates against
committed example fixtures and the report-v2 JSON schema.
Fix test imports for echo_seed module relocation.
* fix: align OPTIONS static validity checks across routing and report
Delegate subscription_table validation to peer_options_static_valid so empty
OPTIONS is valid and PASS-mixed strings are rejected consistently.
* fix: OBP DMRE source-server validation without ALLOW_UNREG_ID bypass
Port OPENBRIDGE.validate_id lookup for 6-7 digit source servers so OBP
ingress matches legacy production config (VALIDATE_SERVER_IDS=True).
* fix: audit items 11-13 coverage, infra tests, and warning logs
* fix: add tests/fakes shim for application test decoupling
* fix: per-stream OBP bridge TX legs for concurrent MASTER downlink
When two OBP voice streams share the same MASTER timeslot, stop
flip-flopping the flat TX row so per-peer downlink gates stay stable.
* fix: ruff lint in OBP concurrent streams downlink test
Remove dead code after return and unused start_tx_events variable.
Dynamic TGs (not in OPTIONS) now activate SYSTEM bridge legs through
master_dynamic_tg_slots, so OBP/HBP traffic for them reaches the peer that
activated the UA session — same path as static OPTIONS TGs.
Also fixes two contention regressions uncovered by the OBP round-trip
scenario:
- Stale ingress session (lost VTERM) no longer blocks downlink indefinitely;
it expires after the peer session timeout instead of holding the slot.
- Shared slot TX_TIME from bridge fan-out no longer trips the RX_PEER busy
check; only RX_TIME within STREAM_TO counts as active RX for that peer.
- SINGLE=1 UA session on its own TG no longer blocks same-TG downlink.
Enforce one active group QSO per peer/slot (SINGLE=0 and SINGLE=1) at the
downlink layer instead of treating the shared MASTER STATUS[slot] as a single
RF slot. Concurrent streams from different hotspots to different TGs are now
legitimate on ingress; contention is decided per-peer.
- Ingress: hbp_ingress_new_stream_collision honours per_peer=True so a second
hotspot is not silenced by the first; hbp_master_ingress_repeat_allowed
applies the same multi-hotspot rule and always passes VTERMs so listener
sessions close and mid-call join works.
- Silent activation: TX onto a TG with an active QSO activates the TG
dynamically, suppresses the uplink, and delivers the in-progress QSO
downlink. Only a TG in GROUP_HANGTIME (no live stream) is rejected.
- Downlink: peer_voice_slots tracks one listen TG per peer/slot with
GROUP_HANGTIME and bridge-hold semantics; stale sessions expire after
STREAM_TO; duplex slots stay independent.
- Non-regression tests for slot contention, silent activation, stale session
expiry, and duplex independence.
Seed GROUP_HANGTIME when ingress PTT ends without VTERM, allow VTERM when
the TG matches on another voice slot, and refine slot-busy rules for
cross-TG handoff versus concurrent overlap.
SINGLE=0 peers accept downlink on a different OPTIONS TG while ingress
TX is active on the slot. After local PTT, SINGLE=1 peers allow same static
TG downlink when the TG is listed in OPTIONS (cross-rx / sequential).
Remove the lab-witness exemption so every hotspot obeys one group QSO per RF
slot. Expire same-TG zombie peer_voice_slots after STREAM_TO, allow orphan
VTERM through the slot gate, and route ingress slot tracking via
track_peer_group_dmrd for consistent session lifecycle.
Enforce one QSO per RF slot for normal hotspots, clear peer_voice_slots
on disconnect, allow matching VTERM through slot gates, exempt lab
witnesses with many static TGs, and log ingress TG-busy drops once per stream.
Introduce downlink.py as the single authority for hotspot eligibility (OPTIONS,
slot busy, post-TX GROUP_HANGTIME). send_peer and BRDG fan-out share the same
gate; monitor events carry stream id from BRDG so new QSOs after hangtime
display without replaying calls blocked during the hangtime window.
Planned release: 2.2.0