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>
17 KiB
Configuring HBlink3
HBlink3's conference bridge (bridge.py) is driven by two files:
| File | What it defines |
|---|---|
hblink.cfg |
The program itself: global timers, reporting, logging, alias lookups, and one stanza per system (a Server you host, an Outbound system you dial out as, or an OpenBridge link). |
rules.py |
The routing: which systems talk to which, on what timeslot/talkgroup, and the talkgroup triggers/timers that turn bridges on and off. |
This document is the full field reference. The sample files (hblink-SAMPLE.cfg, rules_SAMPLE.py) carry only terse inline notes and point back here.
Both are read at startup; there is no live reload — change a file, restart the service.
hblink.cfg
INI format (configparser). Section names in [BRACKETS]; KEY: value pairs. The four fixed sections ([GLOBAL], [REPORTS], [LOGGER], [ALIASES]) must each appear once. Every other section is a system, named by you and identified by its MODE.
[GLOBAL]
| Field | Meaning |
|---|---|
PATH |
Working directory for runtime files. ./ (the program's own directory) is normal. |
PING_TIME |
Seconds between keepalive pings on Server and Outbound connections. |
MAX_MISSED |
Missed pings before a connection is declared dead and torn down. |
PING_LOSS_WINDOW |
Optional (default 5, range 1–60). Minutes over which the dashboard measures each repeater's ping loss — the share of expected keepalive pings that went missing. Larger = smoother / longer memory. Surfaces a repeater that stays connected but has a lossy link (choppy audio). A repeater that merely pings less often is not counted as lossy. |
PING_LOSS_WARN |
Optional (default 5). Ping-loss percent at or above which the dashboard turns a repeater's callsign gold. 0 disables the flag. |
USE_ACL |
True/False top-level on/off switch for the global ACLs below. System-level ACLs are independent of this. |
REG_ACL |
Registration ACL — which repeater radio IDs may log in to any Server you host. ACL syntax below. |
SUB_ACL |
Subscriber ACL — which subscriber IDs (the person keying up) may pass traffic, globally. |
TGID_TS1_ACL / TGID_TS2_ACL |
Talkgroup ACLs applied to timeslot 1 / timeslot 2 traffic, globally. |
ACL syntax
Every ACL is one string: ACTION:list, where ACTION is PERMIT or DENY and list is comma-separated IDs and ranges, or the word ALL.
PERMIT:ALL # allow everything
DENY:1 # block ID 1, allow the rest
DENY:1-5,3120101 # block the range 1–5 and the single ID 3120101
PERMIT:3100-3199,9 # allow only this range and ID 9; deny everything else
A global ACL and a system ACL both apply — traffic must pass both. Registration ACLs are only meaningful for SERVER systems (an Outbound or OpenBridge link never accepts registrations). See ACLS.md for the full ACL model — PERMIT/DENY precedence, how ranges are matched, and worked examples.
[REPORTS]
Feeds the real-time dashboard. See dashboard/.
| Field | Meaning |
|---|---|
REPORT |
True/False — emit the event stream at all. |
REPORT_INTERVAL |
Seconds between periodic status reports. |
REPORT_TRANSPORT |
Optional (default tcp). tcp = listen on a TCP port (dashboard may be remote); unix = listen on a local Unix-domain socket instead. A same-box dashboard on a Unix socket can't be broken by a NIC flap or connection-tracking eviction, so use unix when the dashboard runs on the same host. |
REPORT_PORT |
TCP port the report service listens on (when REPORT_TRANSPORT = tcp). |
REPORT_SOCKET |
Filesystem path for the listening socket (when REPORT_TRANSPORT = unix), e.g. /tmp/hblink3-report.sock. Access is governed by filesystem permissions; REPORT_CLIENTS is not used on a Unix socket. |
REPORT_CLIENTS |
Comma-separated client IPs allowed to connect (e.g. 127.0.0.1) — TCP only. |
The dashboard has matching
HBLINK_TRANSPORT(tcp/unix) andHBLINK_SOCKETsettings — see dashboard/README.md. Point the dashboard at the same transport the daemon serves.
[LOGGER]
| Field | Meaning |
|---|---|
LOG_FILE |
Path to the log file. Empty ⇒ /dev/null. |
LOG_HANDLERS |
Handler set, e.g. console-timed, file-timed, or both comma-joined. |
LOG_LEVEL |
DEBUG, INFO, WARNING, ERROR, CRITICAL. |
LOG_NAME |
Name tag prefixed to log lines. |
Running under systemd? You can skip the log file entirely and rely on the journal: set
LOG_FILEto/dev/null(or leave it empty) and use aconsolehandler — systemd captures stdout into the journal (journalctl -u hblink3). Most installs are perfectly fine doing exactly this.
[ALIASES]
Maps DMR IDs to callsigns/names so logs read as N0MJS instead of 3120001. The files come from radioid.net.
| Field | Meaning |
|---|---|
USE_ALIASES |
True/False. When False, no alias files are loaded and logs show raw numeric IDs. Default False. |
TRY_DOWNLOAD |
True fetches fresh files from the URLs below on startup and refreshes them past STALE_DAYS. False uses whatever is already on disk. |
PATH |
Directory holding the alias JSON files. Must end in /. Point this at the dashboard's directory to share one copy. |
PEER_FILE / SUBSCRIBER_FILE / TGID_FILE |
Filenames for the peer, subscriber, and talkgroup maps. TGID_FILE is optional. |
PEER_URL / SUBSCRIBER_URL |
radioid.net download URLs used when TRY_DOWNLOAD is True. |
STALE_DAYS |
Re-download after this many days. |
⚠️ Memory warning — read before enabling aliases here. The full radioid.net subscriber database, expanded into HBlink's in-memory Python dictionary, balloons to roughly 500 MB of RAM.
bridge.py(anddmrlink) can load these files just to print callsigns in the log — but if you also run the dashboard with aliases, you load the whole thing twice: over 1 GB of RAM, purely for prettier logs.Recommendation: enable aliases in the dashboard only, and leave
USE_ALIASES: Falsehere (or omit the files) forbridge.py/dmrlink. Turn them on in the main program only if the subscriber file has been heavily filtered to a much smaller size first — use the dashboard'sFILTER_COUNTRIES(indashboard/config.py), which writes only the selected countries to disk; both programs then load that smaller file. Filtered to a country or two, the footprint drops to roughly 150 MB; it's the unfiltered global load in two processes that costs the gigabyte and is almost never worth it.
System stanzas
Any section that isn't one of the four above is a system. It is loaded only if ENABLED: True, and its MODE selects one of three types:
MODE |
Role |
|---|---|
SERVER |
You are the Server: Clients — repeaters, hotspots, or another HBlink acting as a Client — connect in to you. (Older DMR docs call a Server a "master"; same thing — a master is a Server.) |
OUTBOUND |
You are a Client: HBlink masquerades as a repeater/hotspot and dials out to someone else's Server. |
OPENBRIDGE |
A Server-to-Server OpenBridge link — no Client role, no login handshake. |
🛑 The section name is load-bearing — get it exactly right. The name you give a system (e.g.
[SERVER-1]) is the exact stringrules.pyreferences in itsSYSTEMfield. If anySYSTEMinrules.pydoes not match a system name inhblink.cfg,bridge.pywill not start at all. Keep the two files in sync, character for character.
MODE: SERVER (be a Server)
You are the Server; Clients (repeaters, hotspots, or another HBlink acting as a Client) connect in.
| Field | Meaning |
|---|---|
ENABLED |
False ⇒ the stanza is skipped entirely. |
REPEAT |
True repeats traffic between Clients connected to this Server (normal). False makes it a pure bridge feed. |
MAX_REPEATERS |
Cap on simultaneously logged-in Clients. |
IP |
Local bind address (0.0.0.0 = all interfaces). |
PORT |
Local UDP port Clients connect to. |
PASSPHRASE |
Shared login secret Clients must present. |
GROUP_HANGTIME |
Seconds a slot stays reserved to the current talkgroup after a transmission, so quick replies aren't lost to contention. |
USE_ACL |
Enable this system's own ACLs. |
REG_ACL |
Which radio IDs may register to this Server. |
SUB_ACL |
Subscriber ACL for this system. |
TGID_TS1_ACL / TGID_TS2_ACL |
Per-timeslot talkgroup ACLs for this system. |
MODE: OUTBOUND (be a Client)
HBlink masquerades as a repeater/hotspot and dials out to another Server. Most fields are the homebrew configuration/login packet this Client presents to the far Server — cosmetic to HBlink, but visible on the far end.
🛑 BrandMeister will not accept this. BrandMeister prohibits inbound Client (
OUTBOUND) connections for bridging and requires OpenBridge instead. To link HBlink to BrandMeister, useMODE: OPENBRIDGE, notOUTBOUND.
| Field | Meaning |
|---|---|
ENABLED |
False ⇒ skipped. |
LOOSE |
True relaxes inbound validation: frames are accepted even if the source radio ID in the header doesn't match this connection's RADIO_ID. Needed for some Servers; leave False when the far end is strict. |
IP / PORT |
Our own local bind address/port. |
SERVER_IP / SERVER_PORT |
The far Server's address/port we dial. |
PASSPHRASE |
Login secret for the far Server. |
RADIO_ID |
The DMR ID we log in as. |
CALLSIGN |
Our callsign (8 chars, space-padded). |
RX_FREQ / TX_FREQ |
Reported RX/TX frequencies (Hz, 9 digits). |
TX_POWER |
Reported TX power. |
COLORCODE |
Reported DMR color code. |
LATITUDE / LONGITUDE / HEIGHT / LOCATION / DESCRIPTION / URL |
Reported location/metadata. |
SLOTS |
Slot capability reported to the Server. |
SOFTWARE_ID / PACKAGE_ID |
Reported software/package identifiers. |
OPTIONS |
Free-form options string some Servers parse for per-connection settings. Leave empty if unused. |
GROUP_HANGTIME |
As above. |
USE_ACL / SUB_ACL / TGID_TS1_ACL / TGID_TS2_ACL |
This system's ACLs. (No REG_ACL — a Client doesn't accept registrations.) |
MODE: OPENBRIDGE (server-to-server link)
OpenBridge is a Server-to-Server (both ends equal), always-TS1 link authenticated by a shared HMAC key and the source socket — no login/registration handshake.
| Field | Meaning |
|---|---|
ENABLED |
False ⇒ skipped. |
IP / PORT |
Our own local bind address/port. |
TARGET_IP / TARGET_PORT |
The far OpenBridge server's address/port. |
PASSPHRASE |
Shared HMAC-SHA1 key. Must match exactly on both ends — this plus the source socket is the authentication. |
NETWORK_ID |
A DMR-ID-shaped number identifying this server. By convention it is stamped into every outgoing frame's "Repeater ID" field. |
PRESERVE_SOURCE_PEER |
True forwards the originating peer ID in that Repeater-ID field instead of overwriting it with NETWORK_ID. The field is unvalidated (auth is the HMAC + source socket) and used only for logging/reporting, so this simply preserves a call's true source across the link. Default False (spec-conventional). Most useful when both ends enable it. |
BOTH_SLOTS |
True lets unit (private) calls use both slots; group traffic is always TS1. 🛑 Only HBlink is known to accept this. No other OpenBridge server (BrandMeister, DMR+, etc.) accepts both-slots traffic — set True only on HBlink-to-HBlink links, and leave it False everywhere else. |
USE_ACL / SUB_ACL |
This link's subscriber ACL. |
TGID_ACL |
Talkgroup ACL (TS1 only — note the single-slot name, unlike the SERVER/OUTBOUND TGID_TS1_ACL/TGID_TS2_ACL). |
rules.py
Defines the conference bridges — the actual routing. Think "bridge groups" or "reflectors": every system marked active on a bridge exchanges traffic with every other active system on that same bridge. It is not end-to-end; each system is activated on each bridge independently.
BRIDGES
A dict. Each key is an arbitrary bridge name ('WORLDWIDE', 'STATEWIDE', …). Each value is a list of system entries — one per participating system — each a dict of:
| Key | Meaning |
|---|---|
SYSTEM |
The system's section name from hblink.cfg. Must match exactly. |
TS |
Timeslot (1 or 2) this rule matches. (XLX links should always use TS 2.) |
TGID |
Talkgroup ID this rule matches. (XLX links should always use TG 9.) |
ACTIVE |
True/False — is this system currently passing traffic on this bridge? (Triggers below can flip it at runtime.) |
TO_TYPE |
Timeout behavior: 'ON' = auto-turn-off after TIMEOUT; 'OFF' = auto-turn-on after TIMEOUT; anything else (use 'NONE') = no timer. |
TIMEOUT |
Timer length in minutes. (Minutes only — timers cost performance.) |
ON |
List of talkgroup IDs that, when keyed, activate this system on the bridge. Always a list, even for one ([2]); use [] for none. |
OFF |
List of talkgroup IDs that deactivate it. |
RESET |
List of talkgroup IDs that reset a running timer without otherwise changing state. Only needed when your voice TGID differs from your trigger TGIDs; otherwise []. |
Example
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': []},
],
'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': []},
],
}
Here anything on SERVER-1 TS1/TG1 reaches CLIENT-1 TS1/TG3100 and vice-versa (talkgroups are rewritten per entry). On WORLDWIDE, keying TG2 turns a system on for 2 minutes; TG9 or TG10 turns it off. STATEWIDE has no timers.
OBP_BRIDGES
OpenBridge (MODE: OPENBRIDGE) systems are configured here, not as inline BRIDGES members. An OpenBridge is a point-to-point trunk that passes talkgroups by TGID — it has no RF user to key ON/OFF/TIMEOUT triggers and no meaningful timeslot — so it declares a compact per-OBP table mapping bridge → TGID instead:
OBP_BRIDGES = {
'VESTA_OBP': { # an OPENBRIDGE system from hblink.cfg
'WORLDWIDE': 1,
'STATEWIDE': 3129,
'ENGLISH': (13, 2), # optional (TGID, TS) form; TS defaults to 1 otherwise
},
}
Read one row as "on this OpenBridge, this TGID is this bridge." That single fact is the route in both directions and the filter: at startup bridge.py expands each row into a bridge member, so a TGID not listed for an OBP is simply never routed — dropped fail-closed, in and out. There is no separate ACL.
| Rule | Behavior |
|---|---|
| TS | Defaults to 1 (OpenBridge's "no timeslot" placeholder). Override with the (TGID, TS) tuple form. |
| Triggers | None — TO_TYPE/TIMEOUT/ON/OFF don't apply to a trunk and are omitted. |
OBP system left as an inline BRIDGES member |
Startup ERROR (move it into OBP_BRIDGES). |
| Same OBP maps one TGID → two bridges (ingress fork) | Startup ERROR — it would duplicate the stream. |
| A bridge carrying different TGIDs on two OBPs (renumber in transit) | Allowed, logs a WARNING — usually a typo, occasionally intentional at a network boundary. |
OBP_BRIDGES is optional — omit it (or leave it {}) if you run no OpenBridges.
Migrating from the old inline form
Earlier HBlink3 configured OpenBridge systems as inline BRIDGES members. To convert an existing rules.py, run the migration tool from the repo root:
python tools/migrate_obp_rules.py --config hblink.cfg --rules rules.py --out rules.py.migrated
It reads hblink.cfg to learn which systems are OPENBRIDGE, lifts each inline OBP member into OBP_BRIDGES, prints a summary of what moved, flags any ingress-fork ERROR or renumber WARNING, and writes a new file (it never overwrites your rules.py). Review the output — comments from the original are not carried over — then replace rules.py with it.
UNIT
UNIT = ['SERVER-1', 'CLIENT-2']
A flat list of system names that should bridge unit-to-unit (private) calls to each other. Systems not listed here pass group calls per BRIDGES but ignore private calls.
Validating
rules.py is executable Python. Run it directly to syntax-check before restarting the service:
python3 rules.py
It pretty-prints BRIDGES and UNIT if the file parses; a traceback means a syntax error to fix.