Files
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

96 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016-2018 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
'''
These are contants used by HBlink. Rather than stuff them into the main program
file, any new constants should be placed here. It makes them easier to keep track
of and keeps hblink.py shorter.
'''
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = ''
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
# DMR Related constants
ID_MIN = 1
ID_MAX = 16776415
# Timers
STREAM_TO = .360
# Seconds of no audio after which a stream is declared ended. On RF, a gap this
# long means sync is lost and any resumption is a new stream (late entry), so the
# stream is over -- we time it out and emit an END.
STREAM_TIMEOUT = 2
# Options from the LC - used for late entry
LC_OPT = b'\x00\x00\x20'
# HomeBrew Protocol Frame Types
HBPF_VOICE = 0x0
HBPF_VOICE_SYNC = 0x1
HBPF_DATA_SYNC = 0x2
HBPF_SLT_VHEAD = 0x1
HBPF_SLT_VTERM = 0x2
# HomeBrew Protocol Commands
DMRA = b'DMRA'
DMRC = b'DMRC'
DMRD = b'DMRD'
MSTCL = b'MSTCL'
MSTNAK = b'MSTNAK'
MSTPONG = b'MSTPONG'
MSTN = b'MSTN'
MSTP = b'MSTP'
MSTC = b'MSTC'
RPTL = b'RPTL'
RPTPING = b'RPTPING'
RPTCL = b'RPTCL'
RPTACK = b'RPTACK'
RPTK = b'RPTK'
RPTC = b'RPTC'
RPTP = b'RPTP'
RPTA = b'RPTA'
RPTO = b'RPTO'
# Higheset peer ID permitted by HBP
PEER_MAX = 4294967295
# XLX reflector constants. These are protocol values, NOT configuration -- every
# XLX module presents as TS2/TG9 on the wire and there is no module identifier in
# any frame. xlxd hardcodes the same pair (DMRMMDVM_REFLECTOR_SLOT = DMR_SLOT2,
# and dstId 9 in cdmrmmdvmprotocol.cpp), as does DMRGateway (XLX_SLOT/XLX_TG at
# DMRGateway.cpp:57-58). The "TG 6" seen in hotspot documentation is DMRGateway's
# configurable RF-side talkgroup, which it rewrites to TG9 before transmitting --
# HBlink peers directly with xlxd, so TG9 is what we use. Never make these
# settable: a wrong value produces traffic xlxd silently discards, with no ACK,
# no module identity on the wire, and therefore no observable symptom.
XLX_TS = 2
XLX_TGID = 9
# Module letter <-> reflector talkgroup. A-Z maps to 4001-4026; 4000 is UNLINK and
# is deliberately not a module.
XLX_UNLINK = 4000
XLX_TG_BASE = 4000