Files
hblink3/rules_SAMPLE.py
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

105 lines
5.0 KiB
Python
Executable File

'''
THIS EXAMPLE WILL NOT WORK AS-IS - YOU MUST SPECIFY YOUR OWN VALUES!
Full reference: CONFIGURING.md (rules.py section).
BRIDGES defines "conference bridges" (think bridge groups / reflectors): every system
marked ACTIVE on a bridge exchanges traffic with every other ACTIVE system on that same
bridge. Not end-to-end -- each system is activated on each bridge independently.
Each bridge key is an arbitrary name; its value is one entry per participating system:
SYSTEM - system name from hblink.cfg -- MUST match EXACTLY, or bridge.py won't start
TS - timeslot 1 or 2 to match (XLX: always TS 2)
TGID - talkgroup ID to match (XLX: always TG 9)
ACTIVE - True/False: passing traffic now? (ON/OFF triggers below can flip it)
TO_TYPE - 'ON' = auto-off after TIMEOUT; 'OFF' = auto-on after TIMEOUT; else 'NONE'
TIMEOUT - timer length in MINUTES (minutes only; timers cost performance)
ON/OFF - lists of TGIDs that activate/deactivate this system (always lists; [] = none)
RESET - list of TGIDs that reset a running timer (only when voice TGID != trigger TGIDs)
'''
BRIDGES = {
'WORLDWIDE': [
{'SYSTEM': 'SERVER-1', 'TS': 1, 'TGID': 1, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10], 'RESET': []},
{'SYSTEM': 'CLIENT-1', 'TS': 1, 'TGID': 3100, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10], 'RESET': []},
],
'ENGLISH': [
{'SYSTEM': 'SERVER-1', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10], 'RESET': []},
{'SYSTEM': 'CLIENT-2', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10], 'RESET': []},
],
'STATEWIDE': [
{'SYSTEM': 'SERVER-1', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10], 'RESET': []},
{'SYSTEM': 'CLIENT-2', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10], 'RESET': []},
]
}
'''
OpenBridge (OBP) systems are NOT configured as inline BRIDGES members above.
An OBP is a point-to-point trunk that passes talkgroups by TGID -- it has no RF
user to key ON/OFF timers and no real timeslot -- so it gets its own table:
OBP_BRIDGES = { <OBP system from hblink.cfg> : { <bridge name> : <TGID> } }
Read a row as "on THIS OpenBridge, this TGID *is* this bridge." That one fact is
the route in BOTH directions and the filter: a TGID not listed for an OBP is
dropped (fail-closed). At startup bridge.py expands each row into a bridge member
for you. Notes:
* TS defaults to 1 (OBP "no timeslot" placeholder). To override: (TGID, TS).
* The OBP system name must be an enabled MODE: OPENBRIDGE system in hblink.cfg.
* Leaving an OBP system as an inline BRIDGES member above is a startup ERROR.
* On one OBP, a TGID may map to only one bridge (a fork duplicates the stream)
-> startup ERROR. A bridge carrying different TGIDs on two OBPs (renumber in
transit) is allowed but logs a WARNING.
* OBP_BRIDGES is optional -- omit it or leave it {} if you run no OpenBridges.
'''
OBP_BRIDGES = {
# 'BACKBONE-OBP': {
# 'WORLDWIDE': 1,
# 'STATEWIDE': 3129,
# 'ENGLISH': (13, 2), # TGID 13 pinned to TS 2 (override)
# },
}
'''
XLX reflector connections also get their own table. An XLX system is a MODE:
OUTBOUND system in hblink.cfg with an XLX_MODULE letter set; it is nailed up to
exactly one conference bridge:
XLX_BRIDGES = { <XLX system from hblink.cfg> : <bridge name> }
There is no TS or TGID column, and this is deliberate. Every XLX module presents
as TS2/TG9 on the wire -- there is no module identifier in any frame -- so those
values are protocol constants that bridge.py injects for you. They are not made
configurable because a wrong value produces no visible symptom: the reflector
never acknowledges anything, so the traffic would simply vanish. Notes:
* The module (A-Z) is set in hblink.cfg as XLX_MODULE, not here. One module
per connection; to bridge two modules, configure two systems.
* Leaving an XLX system as an inline BRIDGES member above is a startup ERROR.
* An XLX system in UNIT (below) is a startup ERROR -- a private call to the
reflector would change its module for everyone connected to it.
* No timers or ON/OFF/RESET triggers: there is no end user on the connection.
* XLX_BRIDGES is optional -- omit it or leave it {} if you run no reflectors.
'''
XLX_BRIDGES = {
# 'XLX950-D': 'WORLDWIDE',
}
'''
UNIT: system names that should bridge unit-to-unit (individual/private) calls to each other.
'''
UNIT = ['ONE', 'TWO']
'''
This is for testing the syntax of the file. It won't eliminate all errors, but running this file
like it were a Python program itself will tell you if the syntax is correct!
'''
if __name__ == '__main__':
from pprint import pprint
pprint(BRIDGES)
pprint(OBP_BRIDGES)
pprint(XLX_BRIDGES)
print(UNIT)