Files
hblink3/hblink-SAMPLE.cfg
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

193 lines
6.6 KiB
INI
Executable File

# HBlink3 main configuration. Full field reference: CONFIGURING.md ACL guide: ACLS.md
#
# NOTE: every [SECTION] name below is referenced by rules.py. If a name in rules.py has
# no matching system here, bridge.py will NOT start. Keep the two files in sync.
#
# [GLOBAL] — program-wide timers plus the global ACLs (run before per-system ACLs;
# both must pass). ACL format: ACTION:list, e.g. PERMIT:ALL, DENY:1, DENY:1-2000,17.
# Leave PATH alone unless you must change it.
[GLOBAL]
PATH: ./
PING_TIME: 5
MAX_MISSED: 3
# Chronic connectivity monitoring (optional; these defaults apply if omitted).
# The dashboard reports each repeater's ping loss -- the percentage of its
# expected keepalive pings that went missing -- over the last PING_LOSS_WINDOW
# minutes (1-60; larger = smoother/longer memory), and turns a repeater's
# callsign gold at or above PING_LOSS_WARN percent. This catches a repeater that
# stays connected but has a lossy link (choppy/dropped audio). 0 disables the
# gold flag. A repeater that simply pings less often is not counted as lossy.
PING_LOSS_WINDOW: 5
PING_LOSS_WARN: 5
USE_ACL: True
REG_ACL: PERMIT:ALL
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
# [REPORTS] — newline-delimited JSON event/stat feed for the dashboard (dashboard/).
# Leave REPORT True; set REPORT_PORT to match the dashboard's HBLINK_PORT and point
# REPORT_CLIENTS at the dashboard host (comma-separated IPs, or a single * for all).
# Full reference: CONFIGURING.md.
[REPORTS]
REPORT: True
REPORT_INTERVAL: 10
REPORT_PORT: 4321
REPORT_CLIENTS: 127.0.0.1
# [LOGGER] — LOG_HANDLERS: null|console|console-timed|file|file-timed|syslog
# (comma-join, no spaces). LOG_LEVEL: DEBUG|INFO|WARNING|CRITICAL. Under systemd,
# set LOG_FILE: /dev/null with a console handler and just rely on the journal.
# Full reference: CONFIGURING.md.
[LOGGER]
LOG_FILE: /tmp/hblink.log
LOG_HANDLERS: console-timed
LOG_LEVEL: DEBUG
LOG_NAME: HBlink
# [ALIASES] — DMR-ID -> callsign lookups. The DASHBOARD owns these files: set
# TRY_DOWNLOAD: False here and point PATH at the dashboard's directory. Loading the full
# subscriber DB here costs ~500 MB RAM per process (>1 GB if the dashboard loads it too),
# so keep USE_ALIASES: False unless the DB has been filtered small (dashboard
# FILTER_COUNTRIES). Full reference incl. the memory warning: CONFIGURING.md.
[ALIASES]
USE_ALIASES: False
TRY_DOWNLOAD: False
PATH: dashboard/
PEER_FILE: peer_ids.json
SUBSCRIBER_FILE: subscriber_ids.json
TGID_FILE: talkgroup_ids.json
PEER_URL: https://www.radioid.net/static/rptrs.json
SUBSCRIBER_URL: https://www.radioid.net/static/users.json
STALE_DAYS: 7
# [OPENBRIDGE] — Server-to-Server link (e.g. BrandMeister, IPSC2). Duplicate for more.
# TARGET_IP/PORT = far server; PASSPHRASE = shared HMAC key (must match both ends);
# NETWORK_ID identifies this server. All traffic is TS1 (no registration).
# BOTH_SLOTS: True is HBlink-to-HBlink ONLY -- no other server is known to accept it.
# PRESERVE_SOURCE_PEER: True forwards the originating peer ID instead of NETWORK_ID
# (unvalidated field; improves call provenance; best when both ends enable it).
# Full reference: CONFIGURING.md.
[OBP-1]
MODE: OPENBRIDGE
ENABLED: True
IP: 0.0.0.0
PORT: 62035
NETWORK_ID: 3129100
PASSPHRASE: password
TARGET_IP: 1.2.3.4
TARGET_PORT: 62035
BOTH_SLOTS: True
PRESERVE_SOURCE_PEER: False
USE_ACL: True
SUB_ACL: DENY:1
TGID_ACL: PERMIT:ALL
# [SERVER] — you are the Server; Clients (repeaters/hotspots) connect in. Duplicate for
# more. IP may NOT be blank (0.0.0.0 or :: = all interfaces); PORT must be unique/unused.
# REPEAT: True repeats between connected Clients; MAX_REPEATERS caps connections.
# Full reference: CONFIGURING.md.
[SERVER-1]
MODE: SERVER
ENABLED: True
REPEAT: True
MAX_REPEATERS: 10
IP: 0.0.0.0
PORT: 54000
PASSPHRASE: s3cr37w0rd
GROUP_HANGTIME: 5
USE_ACL: True
REG_ACL: DENY:1
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
# [OUTBOUND] — you are a Client: HBlink masquerades as a repeater/hotspot and dials out
# to a Server. Duplicate for more. RX/TX_FREQ in Hz; LAT/LONG in decimal degrees; HEIGHT
# in meters. LOOSE: True relaxes validation for non-compliant Servers (XLXD, DMR+).
# NOTE: BrandMeister does NOT accept OUTBOUND/Client connections -- it requires OpenBridge.
# Full reference: CONFIGURING.md.
[OUTBOUND-1]
MODE: OUTBOUND
ENABLED: True
LOOSE: False
IP: 0.0.0.0
PORT: 54001
SERVER_IP: 172.16.1.1
SERVER_PORT: 54000
PASSPHRASE: homebrew
CALLSIGN: W1ABC
RADIO_ID: 312000
RX_FREQ: 449000000
TX_FREQ: 444000000
TX_POWER: 25
COLORCODE: 1
SLOTS: 1
LATITUDE: 38.0000
LONGITUDE: -095.0000
HEIGHT: 75
LOCATION: Anywhere, USA
DESCRIPTION: This is a cool repeater
URL: www.w1abc.org
SOFTWARE_ID: 20170620
PACKAGE_ID: MMDVM_HBlink
GROUP_HANGTIME: 5
OPTIONS:
USE_ACL: True
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
# [OUTBOUND to an XLX reflector] — identical to the Client stanza above plus one
# field. XLX's DMR interface *is* the homebrew protocol, so no special mode exists:
# adding XLX_MODULE is what marks the system as a reflector connection.
#
# XLX_MODULE is the module (room) letter A-Z. HBlink sends the in-band private call
# that selects it immediately after login, and again after every reconnect, because
# the binding lives on the reflector for the life of the session.
#
# * The module is fixed. There is no runtime or end-user module selection -- that
# would break the nailed-up systems/bridges model. Use DMRGateway at a repeater
# or hotspot if users need to pick modules.
# * One module per connection. To bridge two modules, define two systems.
# * SERVER_PORT is 62030 and PASSPHRASE is usually passw0rd. LOOSE: True, since
# xlxd is not strict about the login handshake.
# * The reflector never acknowledges the link, and no frame on the wire says which
# module you are in. If traffic does not flow, check the reflector's dashboard --
# HBlink cannot tell you. Note a stock xlxd is built with 10 modules (A-J).
# * Bridge membership goes in XLX_BRIDGES in rules.py, not in BRIDGES.
#
# [XLX-950-D]
# MODE: OUTBOUND
# ENABLED: True
# LOOSE: True
# IP: 0.0.0.0
# PORT: 54002
# SERVER_IP: 172.16.1.1
# SERVER_PORT: 62030
# PASSPHRASE: passw0rd
# CALLSIGN: W1ABC
# RADIO_ID: 312000
# RX_FREQ: 449000000
# TX_FREQ: 444000000
# TX_POWER: 25
# COLORCODE: 1
# SLOTS: 1
# LATITUDE: 38.0000
# LONGITUDE: -095.0000
# HEIGHT: 75
# LOCATION: Anywhere, USA
# DESCRIPTION: XLX950 module D
# URL: www.w1abc.org
# SOFTWARE_ID: 20170620
# PACKAGE_ID: MMDVM_HBlink
# GROUP_HANGTIME: 5
# OPTIONS:
# XLX_MODULE: D
# USE_ACL: True
# SUB_ACL: DENY:1
# TGID_TS1_ACL: PERMIT:ALL
# TGID_TS2_ACL: PERMIT:ALL