* 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.
* fix: raise UDP SO_RCVBUF on voice listeners (C-LOCAL)
Apply a 4 MB receive buffer on system and proxy UDP sockets to reduce
kernel RcvbufErrors under OBP load; size is configurable via GLOBAL.UDP_RCVBUF.
* fix: exclude byte-identical duplicates from HBP rate counter (A.2)
Check lastData before incrementing the ingress packet counter so compressed
duplicate bursts do not trigger legitimate RATE DROP on call start.
* fix: duplicate-safe TA embed phase and REPEAT VHEAD DMRA (B)
Ignore byte-identical B-E embed bursts so duplicate uplinks do not desync the
TA phase machine, and re-emit DMRA on every VHEAD on the REPEAT path.
* chore: document echo point-to-point and logged_in reconciliation (EN/ES)
Document multi-hotspot echo/service delivery via RX_PEER, the lst_seen
logged_in reconcile loop, and cross-links between user and dev guides.
Echo (TG 9990-9999) must be point-to-point: only the originating hotspot
receives the playback. Two bugs in the inject-only proxy broke this:
Data plane (udp_hbp.py): _peer_should_receive_dmrd had a fuzzy-match
fallback (peer_id // 100 == rf_src) for special TGs that leaked the
first VHEAD to every hotspot sharing the user's DMR-id prefix. Removed
the fallback so echo delivers only to the exact RX_PEER.
Report plane (monitor_topology.py): _echo_tx_target_peer resolved the
monitor chip peer via fuzzy rf_src matching, which picked the wrong
hotspot when a user has several radios sharing a base id (e.g. rf_src
7140023 matched peer 714002301 instead of the real originator
714000103). Now resolves from STATUS[slot].RX_PEER when available.
After a server restart, Clients.logged_in stayed 1 for peers no longer
connected, letting the monitor authenticate disconnected hotspots via
login-by-ip. Replace the hourly clean_tbl (24h idle sweep) with a
reconcile in the existing 120s lst_seen loop: connected peers get
logged_in=1, the rest 0. The lst_seen loop now starts with now=True so
its first tick at boot clears all stale flags immediately.
* fix: add shared PASS= redaction for OPTIONS log lines
Mask PASS= secrets in RPTO/OPTIONS logging to avoid leaking hotspot
passwords in log files.
* fix: rework self-service RPTO to distinguish PASS, empty, and OPTIONS
Three RPTO cases now handled explicitly:
- PASS=password: store hash, fetch OPTIONS from DB (unchanged)
- Empty payload: DB is the authority, fetch OPTIONS from DB
- OPTIONS with content: hotspot is the authority, pass through to master
When the hotspot does not send PASS, the stored password is cleared
(NULL) so only IP auto-login works — password login is impossible
with a NULL hash, matching the legacy dashboard proxy behaviour.
Also removes the noisy DEBUG log on RPTC ("not in mysql_option_peers")
that fired on every normal login, and adds clear_psswd action to the
MySQL repository for explicit NULL writes.
* fix: elevate proxy attach rejection to WARNING for RPTC/RPTO
Control commands (RPTC/RPTO) rejected by attach should be visible at
INFO level, not hidden behind a debug flag, since they indicate a peer
sending configuration before the login exchange completed.
* fix: remove noisy CALL RX log on OBP DMRE voice header
The log fired on every DMRE VHEAD packet from each OBP peer, producing
duplicate entries for the same stream (multiple OBP peers relay the
same call). The existing CALL RX logs for DMRD and OBP v0/v1 paths
already cover stream start diagnostics.
* fix: fetch DB options when hotspot sends no RPTO after RPTC
Some hotspots complete the RPTC (repeater configuration) step but never
send an RPTO packet, leaving the peer without any OPTIONS applied. The
legacy proxy used a 10s timer after RPTC to fetch OPTIONS from MySQL in
that case; restore that behaviour here.
When RPTC arrives and the peer is not already in _mysql_option_peers,
schedule a 10s fallback timer. If no RPTO arrives before it fires, treat
the peer the same as an empty RPTO (BD is the authority) and fetch
OPTIONS from MySQL. If an RPTO does arrive, the existing cancel paths
abort the timer.
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.
Echo/parrot/playback loopback TGs rely on RX_PEER matching in the downlink
gate; applying per-peer busy/hangtime on top blocks the peer's own playback.
Mirrors legacy bridge_master contention which compares TGID + time only.
Remove 24 functions/methods with zero production callers, including
helpers only exercised by tests. Delete the standalone pickle_legacy
module. Adapt affected tests to use production equivalents or inline
constructions. 569 tests pass.
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.
Remove inject-only bypass in _peer_should_receive_dmrd so peers with
empty OPTIONS (silent witness) never receive group DMRD. Align
peer_listen_slots for simplex with static TG lists.
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
* feat: persist peer dynamic TGs in MariaDB across reconnects
Add DATABASE config, async DynamicTgStore, and restore on RPTC so
SINGLE=0/1 dynamics survive hotspot disconnects and server restarts
without blocking the DMRD voice path.
* fix: ensure peer_dynamic_tgs table on server startup
Apply migration 004 idempotently at boot so the server does not depend
on adn-monitor db_bootstrap when peer_dynamic_tgs is missing.
* fix: import DynamicTgEntry for ruff F821 in subscription_table
* fix: log clear MariaDB startup failures to file and stderr
Validate DATABASE at config load and on connect; map common MySQL
errors to actionable messages so the server does not fail silently.
* fix: TG 4000 clears STATUS and bridge legs after dynamic reset
Clear RX slot state to stop RPTO re-seeding cleared sessions, run
in-band 4000 deactivation on inject-only paths, and mark downlink dirty.
* fix: complete TG 4000 reset for monitor and dynamic TG persistence
Emit INGRESS BRDG_EVENT so SINGLE=0 UA chips clear without stuck TX;
wipe all peer dynamic rows from memory and MariaDB on reset. Never store
TG 4000 as a UA session. Require DATABASE only for full peer-server configs.
When voice arrives on TS2 but the hotspot lists the TG on TS1 (or vice
versa), flip the slot bit on send_peer so duplex radios RX on the
configured timeslot.
Normalize all Python sources to the standard ADN copyright block with
complete GPLv3 notice. Add legacy attribution on routing and dmr_utils
ports; drop SemVer wording from changelog and fix an unused test import.
Narrow send_peers and REPEAT fan-out on inject-only proxies using a
precomputed OPTIONS index, cache connected peer count, and debounce
CONFIG_SND pushes to the monitor.
Match legacy routerHBP so ARS/LRRP replies to private subscribers
use pvt_call when SUB_MAP idle check fails, without marking unit data
ingress as voice-busy on the source hotspot.
Replace internal bridge terminology with routing (RoutingUseCases, AclRouter,
routing_table export). SubscriptionStore remains runtime authority with O(1)
indexes for router and downlink filters. Fix STATIC TG parity on OPTIONS/RPTO,
parrot in-band edge cases, and remove per-packet routing_table export from the
hot path that caused high CPU under multi-hotspot OBP load.
Require subscription_store in BridgeUseCases and route timer, OPTIONS,
static TG, and OBP mutations through store ops with export-only BRIDGES shim.
Fix dashboard YAML static TG fallback and sole-hotspot monitor remap for
dynamic UA when a bridge leg is active.
Move stat_trimmer, bridge_debug, bridge_reset, bridge_table (static TG,
reflectors, UA), and OBP source ensure to mutate SubscriptionStore first
and export to the BRIDGES shim; batch OPTIONS loops export once at the end.
Add InbandTriggers to Subscription with ON/OFF/RESET import/export parity.
apply_in_band_signalling mutates the store directly and exports to the
BRIDGES shim (SINGLE_MODE, TG 4000, reflector # rules unchanged).
Mutate SubscriptionStore in rule_timer_loop and export to the BRIDGES shim
without re-importing stale router state. Voice forward uses resolved ForwardLeg
list instead of scanning dict rows when the store is wired.
Subscription store and router are always active when wired at bootstrap;
drop YAML toggles and flag-off test paths. Sync router into store before
get_bridges() export and after make_static_tg mutations.
Sync subscription store after timer and in-band mutations, add optional
store authority with BRIDGES export shim, wire MeshCodecRegistry into
OBP udp_hbp paths, and extend harness parity tests.
Push STATE_SND when peers send RPTO so monitor chips update without reload.
Fetch and inject MySQL OPTIONS immediately on PASS= instead of a 10s delay.
Track per-peer dynamic TG sets for SINGLE=0 so keyed hotspots hear each other.
Enforce strict SINGLE=1 RX exclusivity, always filter inject-only downlink by
each peer's own OPTIONS, and push self-service DB options only after PASS=.
Stop merged SYSTEM static TG lists from leaking across hotspots in topology.