TG/ID 4000 is the dynamic-TG reset control code and can never appear in
_SUB_MAP, so the unknown-destination fallback in _pvt_repeat_targets treated
it as a private call to an unlocated radio and blasted it to every connected
peer. That fallback runs before dmrd_received's own dst_id == 4000 guard, so
the control code has to be filtered at the targeting step too.
The periodic security download ran blocking HTTP on the reactor thread, so a
dead selfcare server stalled RPTPING handling well past PING_TIME * MAX_MISSED
and every peer was timed out and forced to reconnect. Move the security and
alias downloads to the thread pool, bound the DNS and HTTP timeouts below that
budget, and skip a cycle when the previous one is still in flight.
Also drop the redundant interval guard that silently stretched the real retry
period to a multiple of the loop interval, restore the process-wide socket
timeout after a failed DNS lookup, keep existing files and cached passwords
when a download returns an empty or invalid payload, and remove the inherited
50Mb cap on subscriber_ids that production was already close to tripping.
PASS_SECURITY is no longer written to the log.
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: deliver TG on both slots when static on one, dynamic on the other
register_peer_ua_multi_tg silently dropped dynamic tracking on a slot
whenever the same TG was already static on the other slot, and the
REPEAT path never expanded to both slots at all.
* fix: strip NUL-padded CALLSIGN instead of showing raw bytes
Some peers NUL-pad instead of space-pad; reuse the existing
normalize_fixed_width_ascii helper instead of a plain .strip().
PR #53 (7ff3010c) collapsed iter_downlink_voice_slots to one delivery
slot for any peer, but peer_listen_slots only ever returns more than one
slot for a peer already confirmed duplex via peer_is_simplex -- so that
collapse could only suppress real duplex hotspots, not the unpaced
software bridges (ysf2dmr/adn-bridge) it was meant to fix, since those
already collapse correctly via their own simplex classification.
Trust peer_listen_slots directly again. Confirmed live: a duplex hotspot
with a TG on both static OPTIONS slots now receives it on both timeslots
regardless of which slot the source transmitted on.
Use SUB_MAP's stored peer id for delivery (repeat, unit-data, and
pvt_call_received), add Talker Alias support for private calls, and
report the receiving hotspot to the monitor.
Announcement/TTS injection reuses a real peer's DMR id as rf_src, which made
resolve_voice_peer_id's rf_src fallback (and the inject-only proxy's fuzzy
peer match) misattribute the call to that peer: monitor showed it TX/red and
learned bogus dynamic TGs. Guard both resolvers with a new
synthetic_announcement flag, and add a trailing is_announcement field to
every GROUP VOICE report (CSV and JSON voice_event) for monitor-side use.
Dedupe the INFO line when a peer keys into a busy TG during an active QSO,
bind RX_STREAM_ID on silent activation to avoid per-frame re-entry, and
clear the dedupe marker on suppressed-stream VTERM.
A BRIDGES scan can legitimately list the same MASTER on both TS1 and
TS2 for one TG (e.g. an inject-only proxy whose connected hotspots
collectively use both slots). SubscriptionRouter.resolve() kept both
as distinct legs, so dmrd_received called send_to_system twice for
that MASTER per source frame.
Legacy's dumb send_peers() broadcast made that harmless: each
hotspot's own radio silently dropped the slot it didn't want. This
server's send_peers() instead resolves every peer's real listen slot
from its own OPTIONS regardless of the wire slot
(iter_downlink_voice_slots), so the second leg delivered the same
audio a second time to every subscribed peer - doubling the downlink
rate and making unpaced bridges (e.g. ysf2dmr) sound slow. Only
OBP-sourced calls hit this: a MASTER never forwards to itself, so
locally-originated (HBP) calls never produced a redundant leg.
Collapse forward legs to a MASTER/PEER target down to one per
(target_system, target_tgid), keeping distinct legs for OpenBridge
targets and for legitimate TG-translation legs to the same target on
a different TGID.
* fix: accept NUL-padded RPTC callsigns in login check
Fixed-width RPTC fields may be padded with NUL (ipsc2hbp) or spaces
(MMDVM). str.rstrip() left trailing NULs so matching DB callsigns were
rejected with MSTNAK after a successful passphrase exchange.
* fix: strip NUL padding from RPTO OPTIONS in logs and storage
ipsc2hbp pads the fixed-width RPTO body with NULs; logs showed ^@ noise
and peer CALLSIGN as raw bytes on the options line. Normalize on ingest
and in redact_pass_in_options.
* fix: strip NUL padding from monitor export and shared HBP fields
Centralize fixed-width NUL/space trimming in domain helpers and use them
for dashboard_state peer fields, OPTIONS parsing, proxy self-service, and
remaining CALLSIGN log lines.
Skip ensure_obp_source_for_tg_store on OBP voice bursts when indexed
store lookup shows an ACTIVE TS1 source for the TG table, avoiding two
full subscription snapshots on every packet under concurrent OBP load.
Route announcements/TTS through synthetic PTT on the proxy MASTER (SERVER_ID
peer, normal dmrd_received forwarding). Emit START/END TX report events for
inject so monitor fans out to SYSTEM-N; configurable server voice DMR_ID.
Restrict stream trimmer END,RX/END,TX to ingress rows only so BCSQ-quenched
forward legs no longer cascade END to still-active peers; drop stale forward
STATUS before START,TX on stream_id reuse.
* feat: poll peer_dynamic_tgs.need_reload for dynamic TG purge
Proxy send_opts and fallback loop apply TG-4000-equivalent reset when the
monitor sets need_reload, with migration 006 and restore filter updates.
* chore: fix import order in voice subscription plan test
* 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.
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.
Drop duplicate bridge/parrot modules and develop-only slot contention tests
that do not match the subscription routing harness on trial/downlink-clean.
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.