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
2026-08-16 10:14:30 -05:00
2020-03-14 10:56:16 -05:00
2026-08-16 10:14:30 -05:00
2026-07-12 13:43:25 -05:00
2026-08-16 10:14:30 -05:00
2026-08-16 10:14:30 -05:00
2026-08-16 10:14:30 -05:00
2020-03-06 07:31:12 -06:00
2026-08-16 10:14:30 -05:00
2020-03-06 07:31:12 -06:00
2019-03-01 11:30:45 -06:00
2019-03-01 11:30:45 -06:00
2026-08-16 10:14:30 -05:00

HBlink3

HBlink3 is an open-source implementation of the DMR HomeBrew Repeater Protocol (HBP) in Python 3. It acts as an HBP server and/or client and routes calls between MMDVM-based DMR systems, working as a transit / conference-bridge router — traffic is selectively routed from ingress systems to egress systems according to a rules file. It can also connect to other HBlink3 and similar systems via OpenBridge.

Which HBlink? HBlink3 is "system-oriented" and built for transit routing and conference bridging between networks. Its companion, HBlink4 (by the same author), is a repeater-oriented endpoint server for running a single end-point network with no granudlar transit routing. Pick the one that matches your role.

⚠️ Upgrading? Two breaking config changes to know about.

  1. OpenBridge routing moved — MODE: OPENBRIDGE systems are no longer inline BRIDGES members in rules.py; they now live in a dedicated OBP_BRIDGES table. Old configs stop with a clear startup error. Run python tools/migrate_obp_rules.py to convert your rules.py automatically, or see CONFIGURING.md.
  2. System modes renamed — MASTER→SERVER and PEER/CLIENT→OUTBOUND (OPENBRIDGE unchanged). Update the MODE: line of each stanza in hblink.cfg.

Your hblink.cfg/rules.py are git-ignored, so a git pull won't touch them — but the new code won't accept the old values. Non-OpenBridge configs need only change (2).

Applications

Program Purpose
bridge.py The main application: a configurable conference-bridge call router with dynamic on/off triggering, timeouts, and private (unit) call routing.
hblink.py The protocol core. Runs standalone as a server/outbound system for testing, and is the module the applications are built on.

Features

  • HBP server and outbound (client) modes for MMDVM repeaters and hotspots
  • OpenBridge connectivity to other transit call routers, including Brandmeister / DMR+ (IPSC2)
  • Rules-based conference-bridge routing with dynamic ON/OFF/RESET talkgroup triggers and timeouts, including the ability to use TGIDs as in-band signalling to trigger dynamic connections -- sometimes referred to as "dial-a-talkgroup".
  • Private (unit) call routing using a learned subscriber-to-system map that only floods systems configured for unit calls, and only until the target unit's system is located. It then prunes re-transmission to the system with the target unit.
  • Layered access control lists — registration, subscriber, and per-timeslot talkgroup
  • TCP reporting feed for external dashboards
  • Built on Python asyncio — no external networking framework

Quick start

git clone https://github.com/n0mjs710/hblink3.git
cd hblink3
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp hblink-SAMPLE.cfg hblink.cfg      # edit for your systems
cp rules_SAMPLE.py  rules.py         # edit your bridges (bridge.py only)
python bridge.py -c hblink.cfg -r rules.py

See INSTALL.md for full setup, configuration, and running as a systemd service.

Configuration

  • hblink.cfg — global settings, reporting, logging, alias downloads, and one stanza per system (SERVER, OUTBOUND, or OPENBRIDGE). Start from hblink-SAMPLE.cfg.
  • rules.py — (bridge.py only) defines the conference bridges, the systems/talkgroups/timeslots that belong to each, and the systems permitted to pass unit calls. Start from rules_SAMPLE.py.

Your hblink.cfg and rules.py are git-ignored, so git pull won't overwrite them.

Alias files

HBlink3 resolves numeric IDs to human-readable names in logs and in the dashboard. The [ALIASES] section of hblink.cfg controls where these files live and how they are obtained.

Peer and subscriber aliases (peer_ids.json, subscriber_ids.json) are downloaded automatically from RadioID.net when TRY_DOWNLOAD: True is set and the files are older than STALE_DAYS. No manual action is needed for these.

Talkgroup aliases (talkgroup_ids.json) have no automatic download source. This file must be created and maintained by the operator. Copy talkgroup_ids_SAMPLE.json to the path configured in [ALIASES] (default ./) and rename it to match TGID_FILE (default talkgroup_ids.json), then edit it to reflect the talkgroups used on your network.

The format is a JSON object with a single key containing a list of records, each with an id field (integer talkgroup number) and a callsign field (display name). The field is named callsign because the same parser in dmr_utils3 handles peer, subscriber, and talkgroup files alike — for talkgroups it simply holds the human-readable name. The outer key name can be anything, but it must be the first and only key in the object — the parser takes the value of the first key it finds:

{
    "talkgroups": [
        {"id": 3100,    "callsign": "Nationwide"},
        {"id": 3170001, "callsign": "KS Statewide"}
    ]
}

The outer key name ("talkgroups" above) can be anything — only the list it contains is used. Talkgroup IDs not present in the file are displayed as their raw numeric value.

Dashboard

A real-time web dashboard lives in dashboard/ — a separate program with a modern dark UI showing server/outbound/OpenBridge systems, conference-bridge state, and a live call log. Enable HBlink3's reporting feed ([REPORTS] in hblink.cfg) and see dashboard/README.md to run it. the dashboard is kept separate to keep performance of the forwarding hot path free from unecessary loads.

DMR Talker Alias is not preserved across bridges

When bridge.py routes a call to a system with a different TGID or timeslot, it rewrites the Link Control (LC) word in every forwarded DMR frame — voice header, voice terminator, and the embedded LC carried in voice bursts B–E.

The consequence is that DMR Talker Alias data embedded in voice bursts is destroyed during bridging. Talker Alias occupies the same embedded LC slots (FLCO 0x04–0x07 in bursts B–E) that the bridge overwrites with the translated call LC.

This was a consious design decision to ensure every superframe carries full LC information -- preferencing voice quality and integrity over all else.

Future: Currently, most hams appear to prefer/additinoally use a static database for ID to callsign mapping. Inclusion of Talker Alias may be considered in the future.

Companion programs

Separate repositories by the same author. All are small, dependency-free C programs that run alongside HBlink3 and connect to it over the network.

Program Purpose
dmr-talkback Voice test ("echo") endpoint. Connects as an ordinary HBP repeater, records a call and plays it back so the caller hears their own audio. Answers group calls, private (unit) calls, or both. Give it a dedicated bridge for its talkgroup in rules.py.
ipsc2hbpc IPSC ⇄ HBP translator. Connects Motorola IPSC systems (and c-Bridge IPSC peers) to an HBlink3 system.
cc2obp c-Bridge CC-CC ⇄ OpenBridge translator. Peers an HBlink3 OpenBridge system with a c-Bridge over its native CC-CC link.

Requirements

  • Python, at least, 3.8+ (Linux recommended)
  • bitarray, dmr_utils3 — see requirements.txt

License

Copyright (C) 2016-2026 Cortney T. Buffington, N0MJS — n0mjs@me.com

Licensed under the GNU GPLv3; see LICENSE.txt. You may use this software freely, but please credit the project somewhere public (club, organization, or project site) so we can see where it's in use.

No Support Is Provided

This is not commercial software. It is provided free of charge. The author(s) received no compensation for creating and maintaining it. Countless hours over many years have gone into the this. If you have problems, the author will try to help if possible, please have no expectations for support. There is no online group, such as DVSwitch or groups.io that is an "official" outlet for information. The only definitive source of information is me. Beware of others claiming to be authoritative. User-based mutual support is great, and I'm all for it. But please understand, this is what they are, and I have not sanctioned anyone to be the "home" of my software packages.

GitHub "Issues"

Do not use GitHub issues for support. Genuine bugs are accepted as issues. Before opening one, make sure that it is a true problem with the software and not merely a misconfiguration, or contention around a feature that was not supported. Isssues should never be used to ask for or recommend features. Issues that do not include complete details, relevent tracebacks, error messages, configuration snippets, operatrional conditions surrounding the event, etc. will be closed without action.

Acknowledgments

The HomeBrew Repeater Protocol is the work of Jonathan Naylor (G4KLX), Hans Barthen (DL5DI), and Torsten Schultze (DG1HT). This project is the author's clean-room interpretation of that protocol.

S
Description
No description provided
Readme
1.9 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 91.9%
HTML 8.1%