Files
cortandClaude Opus 4.7 17a80a7eca feat: translation-aware logging and dashboard display
Surface DMRD translation state to sysops in two places where it was
previously invisible: log lines and the dashboard.

Logging:
- Add fmt_ts_tg() helper in utils that renders "TS/TGID: 2/9" for the
  untranslated case and "TS/TGID: 2/9 (rf: 1/3172)" when rf-side differs
  from net-side. Network side is always primary since ACLs, subscriptions,
  and routing all reason in that vocabulary.
- Update every stream-lifecycle log (RX started, TX started, contention,
  hang-time same-user / hijack, inbound routing denied, outbound RX,
  outbound forwarding). Source-RX and TX-assumed sites carry both sides
  when translation is in play; outbound and unit-call sites carry net-only.
- UNIT CALL log now uses TS/RID format since unit calls target a radio
  ID, not a talkgroup, and are never translated.
- _update_assumed_stream() gained optional net_slot/net_dst_id so target-
  side logs can annotate rf-local values with their net-side origin.

Dashboard:
- Emit translations as [rf_slot, rf_tgid, net_slot, net_tgid] tuples on
  repeater_connected / repeater_options_updated / repeater_details so the
  frontend can render an RF-side view without server-side rewrites.
- Repeater cards now display TGIDs in RF vocabulary (rebucketed by rf_slot).
  Translated TGIDs are gold and carry a tooltip back to the net-side value.
  A 🔀 badge sits between the connection-type icon and the callsign when a
  repeater has any translation rules active.
- Last Heard gains a per-transmission 🔀 emoji — only when THIS specific
  (slot, talkgroup) matched a translation rule on the source repeater.
  Tooltip shows the exact mapping. Outbound-connection ingresses skip the
  lookup (no local translation state to reference).
- source_repeater_id added to Last Heard entries for local repeater
  ingresses so the client can cross-reference translations.

Docs:
- dmrd_translation.md: new "Processing order" section spelling out that
  translation is the first thing on ingress and last on egress, so ACL /
  contention / hang-time / routing all operate in network vocabulary.
- configuration.md and routing.md: cross-reference the processing order
  in the ACL and contention sections so the network-vocabulary rule isn't
  easy to miss.

Zero overhead for untranslated repeaters: inbound_map / outbound_map are
empty-dict short-circuits, rfTgLists() passes through, and fmt_ts_tg()
collapses to a single-value render when rf == net.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:52:45 -05:00

4.7 KiB

Call Routing and Forwarding

Overview

HBlink4 implements configuration-based call routing using timeslot-specific talkgroup lists. This enables precise control over which calls are accepted from repeaters and forwarded to them.

Configuration Structure

Repeater Configuration

Each repeater configuration includes slot-specific talkgroup lists that define which TGIDs are allowed on each timeslot:

{
    "name": "Example Repeater",
    "match": {
        "ids": [312000]
    },
    "config": {
        "enabled": true,
        "timeout": 30,
        "passphrase": "passw0rd",
        "slot1_talkgroups": [8, 9],
        "slot2_talkgroups": [3120, 3122],
        "description": "KS-DMR Network Repeater"
    }
}

Routing Rules

The same talkgroup lists control both inbound and outbound routing (symmetric):

  • slot1_talkgroups: TGIDs allowed on timeslot 1

    • Traffic on TS1 FROM this repeater is only processed if TGID is in this list
    • Traffic on TS1 is only forwarded TO this repeater if TGID is in this list
  • slot2_talkgroups: TGIDs allowed on timeslot 2

    • Traffic on TS2 FROM this repeater is only processed if TGID is in this list
    • Traffic on TS2 is only forwarded TO this repeater if TGID is in this list

Symmetric routing ensures bidirectional communication - if a repeater can send a talkgroup to the network, it can receive that talkgroup from the network.

Forwarding Assumption

Forwarding is always enabled - that's the whole point of HBlink4! If a repeater has a TGID in its slot list, it will both accept traffic on that TS/TGID and receive forwarded traffic for that TS/TGID.

Default Behavior

If a repeater configuration does not include slot talkgroup lists (empty []):

  • Both directions: All talkgroups are accepted and forwarded (no filtering)

This allows new repeaters to participate fully in the network without explicit configuration.

Assumed Slot State

Since HBlink4 forwards calls to repeaters but doesn't receive real-time feedback about transmission state, we must assume the slot state on target repeaters:

Transmission Assumptions

When we forward a stream to a repeater:

  1. Assume the slot is now active - Track this as an "assumed active" stream
  2. Block the slot for other traffic - Don't forward other calls to this TS until clear
  3. Honor hang time - Keep slot blocked for stream_hang_time after stream ends
  4. Track stream lifecycle - Monitor for terminators to know when transmission ends

Slot State Tracking

For each repeater, we track:

  • Real streams: Streams originating from this repeater (we receive packets)
  • Assumed streams: Streams we're forwarding to this repeater (we send packets)

Both types of streams:

  • Block the slot from other traffic
  • Respect hang time after completion
  • Use terminator detection for immediate end recognition
  • Fall back to timeout if terminator is missed

Contention Handling

When a call needs to be forwarded:

  1. Check inbound filter: Does source repeater config allow this TS/TGID?

    • If no: Drop packet, don't process
  2. For each potential target repeater:

    • Check outbound filter: Does target config include this TS/TGID?
    • If no: Skip this target
  3. Check target slot state:

    • Is there an active stream (real or assumed) on this slot?
    • If yes: Check if it's the same call or different
      • Same call: Forward (continue existing stream)
      • Different call: Skip (contention - slot busy)
  4. Check hang time:

    • Has this slot recently ended a stream?
    • If within hang time: Skip (slot cooling down)
  5. Forward packet:

    • Send to target repeater
    • Create assumed stream state
    • Track for terminator/timeout

Interaction with DMRD translation

When a trusted repeater declares DMRD translation rules via RPTO, the translation layer wraps the routing logic above:

  • On ingress, the packet's (slot, dst_id) is translated from the source repeater's local vocabulary to the network vocabulary before any of the steps above. Every ACL check, contention check, hang-time decision, and target-selection step in this document operates on network-side values.
  • On egress, the per-target outbound_map translates network → target-local after all routing decisions have been made. The slot-busy check (step 3 above) is performed against the target-local slot the packet will actually occupy on-air, not the network slot.

slot1_talkgroups and slot2_talkgroups are always interpreted in network vocabulary. A repeater with no translation declared (the common case) sees no behavioral change — translation is a passthrough for untranslated TGIDs.

See dmrd_translation.md for the full grammar and the step-by-step processing order.