277 Commits
Author SHA1 Message Date
Cort BuffingtonandClaude Opus 5 3fa1b32f16 Add XLX reflector support
An XLX system is a MODE: OUTBOUND system with an XLX_MODULE letter set. XLX's
DMR interface is the homebrew protocol unmodified -- DMRGateway uses the same
CDMRNetwork class for XLX and BrandMeister, and there are no XLX conditionals in
its network layer -- so this adds no new mode, subclass, or data path. The only
protocol difference is one in-band private call at connect that selects the
module, plus pinning the connection to TS2/TG9.

Module binding is static by design. Runtime or end-user module selection would
break the nailed-up systems/bridges model; DMRGateway is the right tool for
user-selectable modules at a repeater or hotspot.

TS2/TG9 is injected by expand_xlx_bridges rather than read from rules.py, so
XLX_BRIDGES is a flat {system: bridge} map with no TS or TGID column. This is
not a default -- it makes a wrong value inexpressible. That matters more here
than elsewhere because every XLX failure is silent: the reflector never
acknowledges a link, and no frame on the wire carries a module identity, so a
mistyped talkgroup would leave HBlink looking healthy while traffic vanished.

The existing conference-bridge retarget path already rewrites the header dst,
flips the slot bit, and regenerates the full and embedded LC, so no XLX-specific
rewrite code is needed -- an XLX system is just a bridge member with a fixed
TS/TGID.

An XLX system in UNIT is a startup error. Reflector module selection *is* a
private call to 4001-4026, so forwarding a unit call to one could silently move
the reflector into a different room for every user connected to it.

Behaviour verified against xlxd source (src/cdmrmmdvmprotocol.cpp):

  * Five acceptance gates at :636-655, each of which silently drops the packet:
    exactly 55 bytes, DATA_SYNC frame type, TS2, slot type VOICE_LC_HEADER, and
    a valid BS/MS data sync. The 55-byte requirement is why the packet carries
    two trailing pad bytes; a bare 53-byte DMRD is rejected.
  * Only the three header frames are parsed for the link command; the two
    terminators have slot type 2 and are ignored, but are sent to frame the call.
  * Unlink-then-link is required, not defensive. At :297-325 a link naming a
    different module while already linked is discarded and rewritten to the
    module already held, so a reconnect that skipped the unlink would silently
    stay in the previous room.
  * xlxd reads the destination from DMRD header bytes 8-10 and never decodes the
    LC (its BPTC decode is commented out at :657-660). The LC is still built
    honestly from the same src/dst: a packet whose payload contradicts its header
    is a debugging trap for anything that does decode it.

Note that a stock xlxd is built with NB_OF_MODULES = 10, so only modules A-J
exist unless the operator rebuilt it. A-Z is accepted here since the protocol
allows it, but an absent module produces no link and no error.

tests/test_xlx_link.py adds 35 tests: the acceptance gates transcribed from
xlxd's parser, packet structure, XLX_BRIDGES expansion and its guards, and
XLX_MODULE validation. One test reproduces the canned bursts from the 2019
reference implementation byte for byte, from their own decoded src/dst and
colour code, which pins the burst builder against six years of field-proven
behaviour.

No new dependencies: bitarray and dmr_utils3 were already required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:14:30 -05:00
Cort BuffingtonandClaude Opus 5 60d67405a2 docs: add Companion programs section to README
Points at the separate C companion repos: dmr-talkback (voice test
endpoint), ipsc2hbpc (IPSC<->HBP translator), and cc2obp (c-Bridge
CC-CC<->OpenBridge translator).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:45:14 -05:00
Cort BuffingtonandClaude Opus 4.8 f81872b60c Stop tracking site-specific dashboard logo images
KS-DMR*.png are the operator's own branding (referenced by the gitignored
dashboard/config.py LOGO_FILE), not project assets -- they were swept in by a
`git add -A` in the previous commit. Untrack them and gitignore dashboard/*.png
so they stay local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 14:17:50 -05:00
Cort BuffingtonandClaude Opus 4.8 aae53890f7 archive: move legacy/unported modules out of the live tree
Nothing in the asyncio core imports any of these; they are moved to archive/
(history preserved) and dropped from pyproject.toml py-modules:

  * bridge_all.py -- forward-everything proxy, never ported to the asyncio core
    (imports the Twisted-era reportFactory -> ImportError today).
  * playback.py -- talkback/parrot app (connects like a repeater, plays a stream
    back). Twisted-based; earmarked to return as an OmniLink adapter.
  * play_ambe.py / mk_voice.py / voice_lib.py -- Twisted-era voice test utilities.
  * reporting_const.py -- REPORT_OPCODES for the old pickle/opcode reporting
    protocol, superseded by the NDJSON feed; it was imported but never used.

Also removes the now-dead `from reporting_const import *` (hblink.py) and the
unused REPORT_OPCODES name from bridge.py's hblink import. Docs/service updated
(README app table, INSTALL run + legacy note, hblink3.service comment,
dashboard/FIELDS.md); archive/README.md explains what's there and why. Suite
still 74/74.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 14:17:02 -05:00
Cort BuffingtonandClaude Opus 4.8 f21eca42b0 changelog: v3.0.0 baseline
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v3.0.0
2026-07-12 13:43:25 -05:00
Cort BuffingtonandClaude Opus 4.8 a3e99a2da9 docs: refresh for the SERVER/OUTBOUND rename, OBP_BRIDGES, and Unix-socket feed
Documentation audit for the recent changes:
  * MODE terminology MASTER->SERVER, PEER/CLIENT->OUTBOUND corrected across
    README, INSTALL, CONFIGURING, ACLS, and the dashboard docs (the code only
    accepts SERVER/OUTBOUND/OPENBRIDGE, so the old names in docs were wrong).
  * README gains a top-of-file breaking-change warning (OBP routing moved to
    OBP_BRIDGES; system-mode rename) with a pointer to the migration tool.
  * CONFIGURING documents REPORT_TRANSPORT/REPORT_SOCKET (Unix-socket feed) and
    a "Migrating from the old inline form" section for tools/migrate_obp_rules.py.
  * dashboard/README documents HBLINK_TRANSPORT/HBLINK_SOCKET and corrects the
    stale "changes appear on the next snapshot" claim (connect/disconnect are
    granular pushes now).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:42:08 -05:00
Cort BuffingtonandClaude Opus 4.8 b43ea79c48 tools: OBP config migration tool (inline members -> OBP_BRIDGES)
tools/migrate_obp_rules.py reads hblink.cfg (to learn which systems are
OPENBRIDGE) and an old-form rules.py, lifts every inline OBP member into an
OBP_BRIDGES table, drops any bridge left empty (OBP_BRIDGES recreates it at
load), flags ingress-fork ERRORs and renumber WARNINGs, prints a summary, and
writes a new file (never overwrites rules.py). Comments are not preserved.

tests/test_obp_bridges.py gains a round-trip proof: an old inline config
migrated then loaded the new way yields identical routing indexes, plus
fork/renumber/TS-override/empty-bridge cases (74 tests total, all green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:42:08 -05:00
Cort BuffingtonandClaude Opus 4.8 cbe405c879 rules: per-OBP TGID<->bridge table (OBP_BRIDGES) replacing inline OBP members
OpenBridge systems are point-to-point trunks that pass talkgroups by TGID: no RF
user to key ON/OFF/TIMEOUT triggers, no meaningful timeslot. Configuring them as
inline BRIDGES members was verbose and made every trunk TGID carry dead trigger
machinery. They now live in a compact per-OBP table in rules.py:

    OBP_BRIDGES = { <OPENBRIDGE system> : { <bridge name> : <TGID> } }   # TS 1, or (TGID, TS)

expand_obp_bridges() (bridge.py) expands each row into a synthetic bridge member
with triggers hard-wired inert (TO_TYPE='NONE') and appends it to the named
bridge, so the forwarding path and indexes run UNCHANGED. Because an unmapped
TGID is never a routing key, the fail-closed ingress/egress filter falls out for
free -- no separate ACL. A TGID-heavy trunk now costs zero rule-timer work.

Load-time validation (design spec §3.3/§3.5):
  * OBP system left as an inline BRIDGES member -> hard ERROR
  * OBP_BRIDGES entry that isn't an enabled OPENBRIDGE system -> hard ERROR
  * one TGID -> two bridges on a single OBP (ingress fork) -> hard ERROR
    (the mirror, a bridge with two TGIDs on one OBP, is unrepresentable in the
    {bridge: tgid} dict, so it needs no check)
  * a bridge carrying different TGIDs across two OBPs (renumber-in-transit)
    -> non-blocking WARNING

OBP_BRIDGES is optional (getattr fallback), so non-OBP configs are unaffected.
tests/test_obp_bridges.py covers expansion, TS default/override, and every
validation branch (10 tests). rules_SAMPLE.py + CONFIGURING.md document the model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:56:56 -05:00
Cort BuffingtonandClaude Opus 4.8 405aad466b reporting: emit repeater_connected/_disconnected (not peer_*) in hblink3
Consistency fix: hblink3 calls this entity a REPEATER everywhere -- the
REPEATERS dict, json_repeater(), and the dashboard UI -- and so does hblink4
(repeater_connected). The lone peer_* event name was the odd one out. Rename
the wire vocabulary to repeater_connected / repeater_disconnected so the same
thing isn't called "peer" in one place and "repeater" in another.

dmrlink3 keeps peer_* deliberately: IPSC genuinely has "peers" (PEERS dict,
_peer_snapshot, UI says Peer). The internal send_peer/report_peer_deltas method
names stay as-is in both daemons -- shared ReportServer plumbing kept symmetric
for the deferred Step-4 extraction; only the wire/UI vocabulary changed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:32:39 -05:00
Cort BuffingtonandClaude Opus 4.8 8a11af25ed tests: add lossy-repeater simulator for the ping-loss visuals
Logs into a running SERVER as a HomeBrew repeater and sends keepalive pings
with a deliberate, bounded drop rate (clean warmup first so the server
calibrates the cadence; never more than --max-consec dropped in a row so it
can't trip MAX_MISSED). Lets you watch the per-repeater ping-loss % climb and
the callsign turn gold without a genuinely marginal RF link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:14:30 -05:00
Cort BuffingtonandClaude Opus 4.8 1c463fb52b reporting: normalize event vocabulary + add Unix-socket transport (Step 5)
Canonical event names across the reporting feed, matching hblink4's emit()
vocabulary (the OmniLink north star):
  * peer            -> peer_connected / peer_disconnected
  * stream          -> stream_start / stream_end
The lifecycle now rides the message `type`; the `action` field is retained
where consumers still use it (log label, END duration/hang handling).

Unix-socket transport for a same-box dashboard, alongside the existing TCP:
  * daemon: REPORT_TRANSPORT=unix binds start_unix_server on REPORT_SOCKET
    (stale socket file cleared first); TCP stays the default for remote dashboards.
  * dashboard: HBLINK_TRANSPORT=unix connects via open_unix_connection(HBLINK_SOCKET).
A local Unix socket can't NIC-flap or be conntrack-evicted, retiring the whole
silent-severance class for local dashboards (filesystem perms replace the IP ACL;
TCP keepalive is moot).

Also: ping-cadence calibration now takes the MEDIAN of the first PING_WARMUP
gaps (robust to an odd first interval), counting loss only once the cadence
is known.

Tests updated for the new vocabulary; fixes an unbounded-loop test bug in
test_loss_registers_then_ages_out (the window-fill loop's bound tracked the
list it was appending to -> infinite append -> OOM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:14:23 -05:00
Cort BuffingtonandClaude Opus 4.8 5d9ae6b19f ping-loss: switch to a plain sliding-window % over the last N minutes; make it configurable
The exponential-decay (tau) metric was hard to explain. Replace it with a simple,
exact 'ping loss % over the last PING_LOSS_WINDOW minutes' -- easy to reason about
('3% loss over the last 5 minutes'). The gap-based, self-calibrated miss detection
is unchanged (a repeater that just pings slower still isn't read as lossy); only
the aggregation is now a pruned event window instead of a decaying counter.

Two [GLOBAL] knobs (optional, sane defaults keep existing configs working):
- PING_LOSS_WINDOW: minutes, default 5, clamped 1-60.
- PING_LOSS_WARN: gold-threshold %, default 5, advertised to the dashboard.

Documented in hblink-SAMPLE.cfg and CONFIGURING.md. Dashboard now reads the gold
threshold from config (was hard-coded 5). Dropped PING_QUALITY_TAU and the math.exp
import. Tests: clean/slower-cadence -> 0, dropped -> loss, and old loss ages out of
the window -> 0. 59 tests pass. Verified live at a picky 1min/1% (local hblink.cfg):
healthy repeaters read 0%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 07:51:00 -05:00
Cort Buffington 9826937487 dashboard demo: seed PING_LOSS on demo repeaters to show the ping-loss visuals
W0ABC=0% (clean), KD0XYZ=3% (loss tag, below gold threshold), N0MJS=24%
(gold callsign + loss tag) so the chronic-connectivity visuals are visible in
demo mode without simulating a lossy link.
2026-07-11 21:29:55 -05:00
Cort Buffington eb7f4d61a7 ping-quality: set decay time constant to 5 min (was 15) 2026-07-11 21:23:44 -05:00
Cort BuffingtonandClaude Opus 4.8 8b0f8906b1 reporting: per-repeater ping-loss quality metric (chronic connectivity)
Surfaces the case the retired warn light was a poor proxy for: a repeater that
stays connected but loses pings over time (lossy link -> choppy/dropped audio).

Server-side, at each received RPTPING we infer how many pings were lost in the
gap since the previous one, against a per-repeater self-calibrated cadence (so a
repeater that simply pings slower is not read as lossy). Received and missed
counts decay exponentially (PING_QUALITY_TAU, ~15 min) VU-meter style, so the
value reflects recent/chronic loss and rises/falls slowly. Exposed as PING_LOSS%
in each repeater's snapshot (rides the 60s resync -- it moves slowly, so that
cadence is fine); the dashboard shows '· N% ping loss' and ambers the callsign
past a threshold. History is keyed by radio id and survives reconnects, so a
flapping lossy repeater still accumulates a meaningful figure.

Tests: clean cadence -> 0%, slower cadence -> 0% (not mistaken for loss),
dropped pings -> clear loss. Verified live: real repeaters read 0%. 58 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:18:36 -05:00
Cort BuffingtonandClaude Opus 4.8 8c59c5e1d8 reporting: demote the full push to a slow resync + heartbeat (Step 2)
Now that peer connect/disconnect travel as granular events (Step 1) and bridge
changes propagate on-change via rule_timer_loop, the full config+bridge push no
longer needs to run every REPORT_INTERVAL. It sent ~3.3 KB every 10s of mostly-
unchanged state -- the redundancy N0MJS flagged.

- periodic_push now sends a full resync only every REPORT_RESYNC_SECONDS (60s) as
  a reconciliation safety net; between resyncs it sends a tiny {'type':'ping'}
  heartbeat so the dashboard's link-liveness timeout stays fed. _resync_due() in
  the base ReportServer; BridgeReportServer resyncs config+bridge together.
- Dashboard treats 'ping' as a no-op (liveness only).
- Retired the point-in-time 'missed ping' warn light: it was a poor proxy and
  would false-fire on stale last_ping between resyncs. The genuinely useful thing
  it hinted at -- chronic connectivity offenders -- will be a cumulative
  indicator built from missed/disconnect events (next).

Verified live: full config dropped from ~every-10s to on-connect + ~60s; pings
every 10s; peer events immediate; dashboard clean, no false timeouts. 55 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:02:19 -05:00
Cort BuffingtonandClaude Opus 4.8 deb1d55f04 reporting: granular 'peer' connect/disconnect events (Step 1 of dashboard normalization)
The dashboard learned about repeater connect/disconnect only via the periodic
full-config push (up to REPORT_INTERVAL late), because there were no granular
peer events -- the full push WAS the change-propagation mechanism. Add proper
deltas so the dashboard updates immediately, as the first step toward demoting
that redundant full push.

- json_repeater() extracted (shared by the snapshot and the new event).
- ReportServer.send_peer() emits {'type':'peer', system, radio_id, action, info}.
- HBSYSTEM.report_peer_deltas(): centralized reconciliation sweep -- diffs
  currently-connected peers vs last reported and emits only deltas, so it can't
  miss any of the scattered add/remove paths and never floods. Called from the
  server maintenance loop (every PING_TIME) and immediately on config completion
  for snappy connect feedback.
- Dashboard backend applies the delta and re-broadcasts config (no browser-JS
  change yet). Full push kept as fallback this step; Step 2 demotes it.
- Tests: connect emits one 'connected' w/ info, no duplicate on re-sweep,
  timeout emits 'disconnected'. 55 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:27:53 -05:00
Cort BuffingtonandClaude Opus 4.8 4731cabbf7 dashboard: size feed read-timeout to the advertised push interval
Same latent bug dmrlink3 hit: a fixed 60s feed read-timeout false-trips if
REPORT_INTERVAL is raised to >=~20s (hblink3's default 10s masked it). The daemon
now advertises report_interval in its config push, and the dashboard sizes the
'link dead' read timeout to 3x that (min 30s), so it can't race the periodic push
at any interval. TCP keepalive remains the primary detector of a dead socket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:13:14 -05:00
Cort BuffingtonandClaude Opus 4.8 313cd1de8e tests: migrate harness off the dropped MASTER/PEER terminology
The test harness still used the old config MODE vocabulary (MASTER/PEER) that the
current schema no longer accepts, so build_config() silently dropped those
systems and ~33 tests failed (config not loaded). Migrate the fixtures and test
code to the current terms; the whole suite (52 tests) now passes.

- harness.cfg: MODE MASTER->SERVER, PEER->OUTBOUND; MAX_PEERS->MAX_REPEATERS;
  MASTER_IP/PORT->SERVER_IP/PORT. System labels MASTER-1/-2 -> SERVER-1/-2
  (REPEATER-1 kept; 'repeater' is not a dropped term).
- test code: _peers->_repeaters, send_master->send_server,
  MASTER_SOCKADDR->SERVER_SOCKADDR, send_bridgeEvent->send_bridge_event mocks.
- harness.py: the 'mark connected' gate keyed on mode=='PEER' -> 'OUTBOUND', so
  the router again forwards to outbound systems (fixes the routing/baseline
  scenarios).
- Comments/identifiers describing our system roles moved off master/peer;
  genuine HBP wire-protocol terms (peer-id, the DMRD peer field, the homebrew
  login handshake) are left as-is.
- baseline_emissions.json regenerated consistently for the renamed systems.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:07:00 -05:00
Cort Buffington b8e43c1474 Merge fix/reporting-link-keepalive: detect silently-severed dashboard link 2026-07-11 17:53:31 -05:00
Cort BuffingtonandClaude Opus 4.8 8d18436aab tests: rewrite stale reporting tests for the asyncio ReportServer
The reporting tests referenced the removed Twisted reportFactory/send_clients/
bridgeReportFactory API. Rewrite them against the current ReportServer /
BridgeReportServer, and add coverage for the dead-client shedding added in the
keepalive fix (is_closing() and over-limit write buffer). Self-contained mocks,
no dependency on config fixtures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 17:53:31 -05:00
Cort BuffingtonandClaude Opus 4.8 a41572e89d reporting: detect silently-severed dashboard link (TCP keepalive + read timeout)
The hblink3<->dashboard reporting link (hblink3 = TCP server on REPORT_PORT,
dashboard = client) could wedge for hours after any event that drops the flow
without a clean FIN/RST -- NIC/interface flap, DHCP renew, firewall/conntrack
eviction, router reboot -- leaving both processes running but delinked and
requiring a manual dashboard restart. Neither end could detect it:

- hblink3's _send_json() used StreamWriter.write() with no drain(), which
  practically never raises for a dead peer, so the client was never shed and
  no disconnect was logged. It now sheds writers that are is_closing() or whose
  write buffer has grown past a limit (unresponsive/stuck peer), and closes them.
- hblink3 now sets SO_KEEPALIVE (idle 60s / intvl 15s / cnt 4) on accepted
  reporting sockets so the OS errors a dead flow in ~2 min, unblocking the read
  loop that removes the client.
- The dashboard feed client blocked forever on readline() with no timeout, so
  its otherwise-correct 3s reconnect loop never ran. It now bounds readline()
  with a 60s timeout (hblink3 pushes config at least every REPORT_INTERVAL) and
  sets SO_KEEPALIVE on its side too.

Only affects non-loopback deployments (dashboard on a different host, or pointed
at the LAN IP); loopback connections were never exposed to this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 17:39:22 -05:00
Cort BuffingtonandClaude Opus 4.8 7eea4558d8 dashboard: collapsible sections + Active Calls view + compact outbound
- Last Heard now collapses to an "Active Calls" view (live streams only)
  and retitles the header, instead of hiding the section
- Every section is collapsible (servers, outbounds, openbridges,
  conference bridges, call log) via a shared initCollapse helper,
  persisted per-browser
- Outbound systems: fold the system name into the header row and move
  TS1/TS2 into the slot-column headers, dropping the redundant name line

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:11:56 -05:00
Cort BuffingtonandClaude Opus 4.8 01cd53456a dashboard: add Last Heard table and collapsible connection lists
- Last Heard: most-recent unique callers derived from the call log
  (User / Type / Source / TS / Talkgroup / When), collapsible with
  per-browser state; configurable via LAST_HEARD / LAST_HEARD_COUNT.
- Server systems: a Show/Hide Connections button collapses the
  connection table; when collapsed, system-level TS1/TS2 activity pills
  replace it so live calls stay visible. Default via SERVER_REPEATERS.
- Add a server timestamp to logged stream events so Last Heard 'time ago'
  is accurate after a page reload.
- Docs updated (dashboard/README.md, dashboard/FIELDS.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:31:42 -05:00
Cort BuffingtonandClaude Opus 4.8 40fda71210 OpenBridge: report the real source peer on timeout-cleanup stream ends
The OBP per-stream STATUS record did not retain the source peer, so the
timeout-cleanup END report substituted the system NETWORK_ID. Store the
peer at stream start and use it (falling back to NETWORK_ID) so the
timeout END matches the START event -- the true source repeater is now
reported when PRESERVE_SOURCE_PEER is enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:31:35 -05:00
Cort BuffingtonandClaude Opus 4.8 654730de9f Clarify alias RAM figures: ~150 MB filtered vs ~500 MB unfiltered
The dashboard sample cited ~150 MB and CONFIGURING.md cited ~500 MB for
the same subscriber DB; they describe different states. Make both say so:
~150 MB with FILTER_COUNTRIES applied, ~500 MB unfiltered global (and that
again if a second process loads it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:12:50 -05:00
Cort BuffingtonandClaude Opus 4.8 c9e2166aab Add CONFIGURING.md; slim sample-file comments to point to it
New CONFIGURING.md is the full field reference for hblink.cfg (GLOBAL,
REPORTS, LOGGER, ALIASES, and the SERVER/OUTBOUND/OPENBRIDGE system
modes) and rules.py (BRIDGES/UNIT), matching the cc2obp CONFIGURING.md
house style. hblink-SAMPLE.cfg and rules_SAMPLE.py comments are trimmed
to terse one-liners that point here.

Notable content:
- Server/Client terminology throughout (a "master" is a Server; Clients
  are repeaters/hotspots, or an OUTBOUND HBlink masquerading as one).
- Hard warning that a rules.py SYSTEM with no matching hblink.cfg system
  stops bridge.py from starting.
- ALIASES memory warning: full subscriber DB ~500 MB per process, >1 GB
  if the dashboard loads it too; use dashboard-only or filter via
  FILTER_COUNTRIES.
- BrandMeister prohibits OUTBOUND/Client links and requires OpenBridge.
- BOTH_SLOTS is HBlink-to-HBlink only; no other server accepts it.
- systemd/journal logging note; pointer to ACLS.md for the ACL model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:08:54 -05:00
Cort BuffingtonandClaude Opus 4.8 6c9c1be5bb Add PRESERVE_SOURCE_PEER option to OpenBridge systems
By OpenBridge convention, send_system overwrites the DMRD "Repeater ID"
field (bytes 11-14) with this server's own NETWORK_ID. That field is not
validated by the reference implementation -- authentication is the HMAC
(keyed by PASSPHRASE) plus the source socket -- and in the OBP path it is
used only for logging/reporting. Overwriting it therefore discards the
originating repeater/peer ID that a plain HBP/IPSC path would carry.

PRESERVE_SOURCE_PEER (default False, i.e. spec-conventional behavior)
skips the overwrite so the true source peer propagates end-to-end,
improving provenance when isolating a call's original RF source across
the network. Parsed with fallback=False so existing configs are
unaffected. Verified _peer_id is logging/reporting-only throughout the
OBP path (routerOBP/bridge.py): routing keys on (system,slot,dst_id),
loop-prevention on system name, dedup on stream_id -- none touch it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:30:41 -05:00
Cort Buffington 0a9f87ff52 Change GitHub Issues section header to H3
Corrected heading level for GitHub Issues section in README.
2026-07-03 08:25:10 -05:00
Cort Buffington 42ac87cb24 Add guidelines for using GitHub issues
Clarify the use of GitHub issues for support requests.
2026-07-03 08:23:24 -05:00
Cort Buffington ade5e78c76 Update support and contributing section in README
Clarify support policy and emphasize lack of commercial support.
2026-07-03 08:15:43 -05:00
Cort Buffington 5962fa032b Revise README for clarity and terminology updates
Updated terminology and clarified features in the README.
2026-07-03 07:54:43 -05:00
Cort BuffingtonandClaude Opus 4.8 5d36915fee Trim sample config verbosity; fix stale ACL performance caution
Replace the long ACL preamble with a concise summary that points to ACLS.md,
and drop the outdated 'ACLs consume packet processing time' warning that the
current bisect/set implementation makes moot. Tighten the OpenBridge, Server,
and Outbound section comments. Update ACLS.md to stop referencing the removed
caution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:13:35 -05:00
Cort BuffingtonandClaude Opus 4.8 0529830090 ACLS.md: drop code snippets and internals for a config-focused read
Remove the Python source excerpts, return-value structure, and the
'how matching works' section that would confuse most operators; keep the
config grammar, examples, performance guidance, and warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:08:07 -05:00
Cort BuffingtonandClaude Opus 4.8 bd90a8bb70 Add ACLS.md: full documentation of the ACL system
Explains the PERMIT/DENY grammar, the whitelist-vs-blacklist default-action
rule, the four ACL types (REG/SUB/TGID_TS1/TGID_TS2), GLOBAL+SYSTEM layering,
the USE_ACL switch (and why REG_ACL is always enforced), the OpenBridge TS1
case, complex worked examples, and honest performance guidance verified
against config.py/hblink.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 06:59:48 -05:00
Cort BuffingtonandClaude Opus 4.8 173bd5402a dashboard: raise active-stream stale timeout to 5 min
The dashboard dropped any active stream 30 s after its START event, since
HBlink3 emits only START/END (no mid-call keepalive) and the reaper anchored
to START without refreshing. A transmission longer than 30 s therefore had all
its pills reaped mid-call, leaving the dashboard looking idle until the next key-up.

The reaper is only a backstop for a lost/never-sent terminator on a still-
connected feed -- a dropped feed already clears all active streams on disconnect,
and HBlink3 synthesizes an END a couple seconds after audio stops. Raise both the
server (STREAM_STALE) and browser (STALE) thresholds to 300 s, well above the
usual 3-minute amateur time-out timer, so legitimate long calls are never clipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:32:25 -05:00
Cort Buffington bd7fbd1854 Correct memory usage comment in hblink-SAMPLE.cfg
Updated memory usage information for subscriber_ids.json.
2026-07-02 15:21:54 -05:00
Cort BuffingtonandClaude Sonnet 4.6 bebb3b1900 Fix OpenBridge stream pills stretching full cell width
.call has width:100% for use in fixed-width slot columns, but in the
OpenBridge table multiple spans share one <td>. Override with width:auto
so pills size to their content and flow inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 18:28:01 -05:00
Cort BuffingtonandClaude Sonnet 4.6 6c1c810b42 Add ?demo mode to dashboard with full coherent scenario
Loads a static pre-populated state (SERVER + OUTBOUND + OPENBRIDGE,
three bridge groups, live calls bridged across systems, a TX hang slot,
and a call log) without requiring a live HBlink3 connection. Skips
WebSocket connect() in demo mode so the state is never overwritten.

Documents ?demo in dashboard/README.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 18:24:53 -05:00
Cort BuffingtonandClaude Sonnet 4.6 982ff253e0 Dashboard: move TS1/TS2 labels to system name row, left-align with pill edge
- Server and outbound renderers now use a flex sys-row with system info
  on the left and TS1/TS2 labels on the right, on the same line
- Labels are left-justified at padding-left:10px, matching the left edge
  of the call pill in the table column below
- TS1/TS2 text removed from table <th> (columns kept for structure);
  outbounds restructured into per-system sections to match servers layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 12:47:49 -05:00
Cort BuffingtonandClaude Sonnet 4.6 894434014d hblink.py: include GROUP_HANGTIME in json_systems for SERVER and OUTBOUND
The dashboard hang-time pill reads this field; without it the value
was always undefined/0 and hang state was never recorded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 12:39:58 -05:00
Cort BuffingtonandClaude Sonnet 4.6 23d985a1a9 Dashboard: add TX/RX label, inherited color, and hang-time to slot pills
- Bottom line of active call now shows RX/TX · call type · src ID · elapsed
- Active call bottom line inherits the rx/tx color (opacity 0.85) instead
  of reverting to muted grey
- Hang-time state tracked in ST.hang on call END; displayed as a blue
  'hang' pill with countdown until the next call can interrupt
- Hang entries reaped by the 1-second ticker; callCell accepts hang as
  third arg so server and outbound renderers both show it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:57:56 -05:00
Cort BuffingtonandClaude Sonnet 4.6 99341676f9 Dashboard: move call type to bottom line of slot pill
Source → destination on top, call type · ID · elapsed on bottom,
matching DMRlink3 convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:52:53 -05:00
Cort BuffingtonandClaude Sonnet 4.6 255b803f00 Dashboard: use explicit call type labels — Group Voice, Unit Voice, etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:39:37 -05:00
Cort BuffingtonandClaude Sonnet 4.6 630f0dccc3 Stream-filter subscriber/peer downloads by country; move ID files to dashboard/
- Replace try_download calls with _stream_id_file() which uses ijson to
  stream-parse radioid.net JSON without ever loading the full document into
  memory, and filters to a configurable FILTER_COUNTRIES list on the fly
- Strip unused fields (only id+callsign written to disk) regardless of filter
- Atomic write via .tmp + os.replace() so an interrupted download never
  leaves a corrupt file that passes the staleness check
- Add ijson>=3.2 to requirements.txt; drop now-unused try_download import
- Add FILTER_COUNTRIES = ['United States', 'France'] to config_sample.py
- Move talkgroup_ids_SAMPLE.json into dashboard/ where PATH points

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 21:20:13 -05:00
Cort BuffingtonandClaude Sonnet 4.6 5e0d433670 Add USE_ALIASES flag to opt out of alias loading cleanly
Without this, omitting alias file paths produced log noise suggesting
an incomplete configuration. Setting USE_ALIASES: False skips all
alias loading and emits a single clean log line instead.
Defaults to False so new deployments are quiet by default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 20:22:22 -05:00
Cort BuffingtonandClaude Sonnet 4.6 b4c71b56c3 Hot-path data structure optimisation: ON/OFF/RESET lists → frozensets
make_bridges() now converts ON, OFF, and RESET trigger lists to frozensets
instead of mutating list entries in place. Membership tests fired on every
voice packet are now O(1) instead of O(n).

Also fixes a latent bug: RESET entries were not being converted to bytes_3
in make_bridges(), so any non-empty RESET list would silently never match
(bytes vs int comparison). frozenset conversion applies bytes_3 to all
three trigger sets uniformly.

RAM delta: negligible (~9 KB at typical config sizes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:46:31 -05:00
Cort BuffingtonandClaude Sonnet 4.6 c36ad14c47 Modernize reporting server to asyncio streams (match dmrlink3 style)
Replace the protocol/transport factory pair (report + reportFactory /
bridgeReportFactory) with a single ReportServer class using
asyncio.start_server (streams API), matching what dmrlink3 already uses.

hblink.py:
- Remove report(asyncio.Protocol) and reportFactory classes
- Add ReportServer: start() via asyncio.start_server, _client_connected()
  coroutine handles ACL, initial state push, read loop, and disconnect
  cleanup; _send_json() broadcasts; periodic_push() drives the timer
- Simplify config_reports() — no inner function, calls periodic_push

bridge.py:
- Remove duplicate config_reports() — now imported from hblink
- Replace bridgeReportFactory(reportFactory) with BridgeReportServer(ReportServer)
  - send_bridge() and send_bridge_event() use _send_json() directly
  - periodic_push() overrides base to push both config and bridge state
  - send_initial() removed — _client_connected() in base class calls
    send_config() + send_bridge() on every new connection
- Rename send_bridgeEvent → send_bridge_event (snake_case consistency)
- Update import: reportFactory → ReportServer, add config_reports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 17:26:44 -05:00
Cort BuffingtonandClaude Sonnet 4.6 5ea20623d7 Code cleanup: dead code, unused loop vars, debug scaffolding, stubs
bridge.py:
- enumerate loops: e → _ (variable was never read)
- Fix comment typo: dicationry/temporarly/entrys

hblink.py:
- Remove ~12 commented-out #logger.debug / #print lines and their
  "KEEP COMMENTED OUT" guard comments throughout OPENBRIDGE,
  HBSYSTEM server/outbound datagram receive paths and send paths
- Remove commented-out old-style byte concatenation alternatives
  (superseded by b''.join() lines immediately below them)
- Remove #_call_type one-liners superseded by if/elif/else blocks
- dmrd_received() stubs: replace bare pass with docstring clarifying
  the override contract for subclasses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 15:22:33 -05:00
Cort BuffingtonandClaude Sonnet 4.6 e5a9c197a5 Fix try_download argument order in dashboard server
Signature is (path, filename, url, stale) not (path, url, filename, stale);
the swap caused startup to fail with 'unknown url type: peer_ids.json'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 15:14:06 -05:00