|
|
# BRIDGES (legacy) vs Subscriptions (new server)
|
|
|
|
|
|
**adn-dmr-server** and **adn-server 2.x** forward group voice the same way at the wire: a talkgroup “bridge table” decides which **systems** receive a copy of the stream, with **LC rewrite** per leg. What changed in 2.x is **how that table is represented in code** — not the operator-visible rules.
|
|
|
|
|
|
## For operators
|
|
|
|
|
|
Runtime bridge behaviour is unchanged: TGs, slots, OPTIONS, UA, TG 4000, OpenBridge. **Subscriptions** is the internal name for the 2.x routing engine — not a separate operating mode and not something you configure on its own.
|
|
|
|
|
|
**No operational change**
|
|
|
|
|
|
- Configuration: **`SYSTEMS`**, hotspot OPTIONS, **`SELF_SERVICE`** / MariaDB — same as **adn-dmr-server**. Neither stack loads a `BRIDGES` block from YAML.
|
|
|
- Rule parity: ACTIVE source row, `#…` tables, UA timers, `GEN_STAT_BRIDGES`, etc.
|
|
|
|
|
|
**Concrete gains in 2.x**
|
|
|
|
|
|
| Area | Effect |
|
|
|
|------|--------|
|
|
|
| **Routing stability** | Bridge state lives in a dedicated store; reports and timers no longer share the same mutable structure as voice forwarding. Fewer mismatches between what the server forwards and what the dashboard shows under load. |
|
|
|
| **Monitor** | **Report v2** (`routing_table`, `topology`) to **adn-monitor 2.x** replaces pickle/CSV; BTABLE tracks peer state more faithfully. |
|
|
|
| **Dynamic TGs** | With **`DATABASE`**, per-peer dynamics are **persisted** and restored on reconnect (≥ 2.0.0-rc.3). |
|
|
|
| **Maintenance** | Timer, OpenBridge, ACL, and self-service fixes do not go through one process-wide dict shared with every subsystem. |
|
|
|
|
|
|
The word **subscription** only matters when reading code or this guide; on the dashboard and on the air you still work with **bridges** and **talkgroups**.
|
|
|
|
|
|
---
|
|
|
|
|
|
## At a glance
|
|
|
|
|
|
| | **Legacy (`adn-dmr-server`)** | **New (`adn-server` 2.x)** |
|
|
|
|---|------------------------------|----------------------------|
|
|
|
| **Runtime authority** | Global `BRIDGES` dict (`bridge_master.py`) | **`SubscriptionStore`** (domain `Subscription` objects) |
|
|
|
| **Structure** | `bridge_key → [ row, row, … ]` | One **subscription** per system leg on a channel |
|
|
|
| **Forward resolution** | Scan rows, call `to_target` | **`SubscriptionRouter.resolve()`** → `ForwardLeg` |
|
|
|
| **Monitor / report wire** | Pickle `BRIDGE_SND` = `BRIDGES` | JSON `routing_table` (v2) or exported `BRIDGES` shim (v1 compat) |
|
|
|
| **YAML `BRIDGES:` block** | Not loaded from config in either stack; rows are built at runtime | Same — rows come from OPTIONS, UA, STAT, OpenBridge, echo bootstrap |
|
|
|
|
|
|
Observable behaviour (source-row guard, dynamic UA, static TG, reflector keys `#…`, OpenBridge TS1 match, timers) follows **legacy parity** with `bridge_master.py`.
|
|
|
|
|
|
## Legacy: `BRIDGES` dict
|
|
|
|
|
|
In **adn-dmr-server**, routing state is a **process-wide dictionary**:
|
|
|
|
|
|
```text
|
|
|
BRIDGES["52090"] = [
|
|
|
{ "SYSTEM": "MASTER-A", "TS": 2, "TGID": b'...', "ACTIVE": True, "TO_TYPE": "ON", "TIMER": …, … },
|
|
|
{ "SYSTEM": "MASTER-B", "TS": 2, "TGID": b'...', "ACTIVE": True, "TO_TYPE": "ON", … },
|
|
|
{ "SYSTEM": "OBP-UK", "TS": 1, "TGID": b'...', "ACTIVE": False, … },
|
|
|
]
|
|
|
BRIDGES["#310"] = [ … ] # reflector / marked TG tables
|
|
|
```
|
|
|
|
|
|
Each **row** is a leg. Important fields:
|
|
|
|
|
|
- **`SYSTEM`** — configured system name (HBP master or OpenBridge leg).
|
|
|
- **`TS`** — timeslot 1 or 2 (OpenBridge sources use **TS 1** in the match path).
|
|
|
- **`TGID`** — destination ID bytes used for **LC rewrite** toward that leg.
|
|
|
- **`ACTIVE`** — leg participates in forwarding when true.
|
|
|
- **`TIMEOUT` / `TIMER` / `TO_TYPE` / `ON` / `OFF` / `RESET`** — UA timers, static/stat, in-band VTERM rules.
|
|
|
|
|
|
**Voice path (`dmrd_received`):**
|
|
|
|
|
|
1. Derive **bridge key** from destination TG (and reflector `#…` tables when applicable).
|
|
|
2. Create a dynamic table if missing (UA / STAT / static OPTIONS — same triggers as legacy).
|
|
|
3. Find an **ACTIVE source row** matching current **system + slot + TGID**.
|
|
|
4. For each other **ACTIVE** row in **that same table**, call **`to_target`** (contention, ACL, LC/TA rewrite, OpenBridge loop control).
|
|
|
|
|
|
```mermaid
|
|
|
flowchart TB
|
|
|
IN[DMRD / DMRE ingress] --> DM[dmrd_received]
|
|
|
DM --> KEY{bridge key exists?}
|
|
|
KEY -->|no| CREATE[ensure dynamic / stat / static row]
|
|
|
CREATE --> BR
|
|
|
KEY -->|yes| BR[(BRIDGES dict)]
|
|
|
BR --> SRC{ACTIVE source row\nSYSTEM + TS + TGID?}
|
|
|
SRC -->|yes| SCAN[other ACTIVE legs\nsame table]
|
|
|
SCAN --> TT[to_target per leg]
|
|
|
TT --> OUT[HBP / OpenBridge egress]
|
|
|
SRC -->|no| DROP[no forward]
|
|
|
```
|
|
|
|
|
|
The monitor reads the **same dict** via pickle **`BRIDGE_SND`**.
|
|
|
|
|
|
## New server: `Subscription` + `SubscriptionStore`
|
|
|
|
|
|
In **adn-server 2.x**, the **domain model** replaces ad-hoc dict rows:
|
|
|
|
|
|
- **`AudioChannel`** — logical TG + slot `(tgid, slot)`.
|
|
|
- **`Subscription`** — one system’s participation: **role**, **activation policy**, **state** (phase, timer), **target_tgid** (LC rewrite), optional **`relay_table_key`** (reflector `#…` tables).
|
|
|
- **`SubscriptionStore`** — sole **runtime routing authority** (no parallel `BRIDGES` mutation).
|
|
|
|
|
|
**Voice path** (same semantics, different types):
|
|
|
|
|
|
1. `RoutingUseCases.dmrd_received` updates the store (create relay table, static TG, UA timeout — legacy hooks).
|
|
|
2. **`SubscriptionRouter.relay_tables_with_active_source`** — tables where the ingress system has an **ACTIVE** subscription matching slot/TG.
|
|
|
3. **`SubscriptionRouter.resolve`** — returns **`ForwardLeg`** targets (system, slot, tgid) for all other **ACTIVE** subscriptions in those tables.
|
|
|
4. Forward mixins send packets (`to_target` parity).
|
|
|
|
|
|
```mermaid
|
|
|
flowchart TB
|
|
|
IN[DMRD / DMRE ingress] --> RU[RoutingUseCases.dmrd_received]
|
|
|
RU --> SS[(SubscriptionStore)]
|
|
|
RU --> SR[SubscriptionRouter]
|
|
|
SS --> SR
|
|
|
SR --> LEGS[ForwardLeg list]
|
|
|
LEGS --> FWD[HBP / OBP forward mixins]
|
|
|
SS --> EXP[RoutingTableLegacyView\nexport shim only]
|
|
|
EXP --> MON[BRIDGE_SND pickle\nor routing_table JSON]
|
|
|
```
|
|
|
|
|
|
**Important:** `routing_table_for_report()` / **`BRIDGE_SND`** is a **one-way export** for dashboards (`RoutingTableLegacyView`). It is **not** used to decide forwards. That avoids the legacy pattern of mutating a global dict shared with reporting.
|
|
|
|
|
|
## Row → subscription mapping
|
|
|
|
|
|
| Legacy `BRIDGES` row | Domain `Subscription` |
|
|
|
|----------------------|-------------------------|
|
|
|
| Table key (`"52090"`, `"#310"`) | `relay_table_key` + channel TG |
|
|
|
| `SYSTEM` | `system` (`SystemId`) |
|
|
|
| `TS` + table TG context | `channel.slot` / `channel.tgid` |
|
|
|
| `TGID` (bytes) | `target_tgid` (LC rewrite) |
|
|
|
| `ACTIVE` | `state.phase` (`ACTIVE` / `IDLE`) |
|
|
|
| `TIMER` | `state.timer_expires_at` |
|
|
|
| `TIMEOUT` | `timeout_seconds` |
|
|
|
| `TO_TYPE` (`ON`, `OFF`, `STAT`, `NONE`) | `role` + `policy` (`ActivationPolicy`, `SubscriptionRole`) |
|
|
|
| `ON` / `OFF` / `RESET` | `triggers` (`InbandTriggers`) |
|
|
|
|
|
|
Import/export helpers: `routing_table_import.py`, `routing_table_export.py` (mirror of legacy `bridges_export`).
|
|
|
|
|
|
## End-to-end comparison (one voice frame)
|
|
|
|
|
|
```mermaid
|
|
|
sequenceDiagram
|
|
|
participant Radio
|
|
|
participant Server
|
|
|
participant Peer as Other system
|
|
|
|
|
|
Radio->>Server: Group voice TG 52090 TS2
|
|
|
|
|
|
rect rgb(40,40,50)
|
|
|
note right of Server: Legacy
|
|
|
Server->>Server: BRIDGES["52090"] source row ACTIVE?
|
|
|
Server->>Server: foreach ACTIVE leg in table
|
|
|
Server->>Peer: to_target (rewrite LC)
|
|
|
end
|
|
|
|
|
|
rect rgb(30,50,40)
|
|
|
note right of Server: New server (same rules)
|
|
|
Server->>Server: SubscriptionRouter.resolve()
|
|
|
Server->>Peer: forward ForwardLeg(s)
|
|
|
end
|
|
|
```
|
|
|
|
|
|
## What did **not** change
|
|
|
|
|
|
- Bridge **keys** (`52090`, `#reflector`, …) and **multi-leg tables**.
|
|
|
- **Source-row guard** — forward only from a table where **this** system is an ACTIVE source for that TG/slot context.
|
|
|
- **Dynamic UA**, **static OPTIONS**, **`GEN_STAT_BRIDGES`**, **TG 4000** clearing, **echo 9990** bootstrap.
|
|
|
- Timer passes (`rule_timer`, `bridgeDebug`, …) — still driven off the same logical table, implemented on the store in 2.x.
|
|
|
|
|
|
## Concrete example
|
|
|
|
|
|
**Bridge** (network concept): “TG 52090 connects these systems and forwards voice between them”.
|
|
|
**Subscription** (2.x code only): **one leg** of that table — e.g. “MASTER-A on TG 52090, slot 2, ACTIVE, with its LC and timer”.
|
|
|
|
|
|
It is not bridge *or* subscription: in 2.x a bridge **is** a set of subscriptions on the same channel (TG + slot).
|
|
|
|
|
|
### Scenario
|
|
|
|
|
|
Someone keys **TG 52090** and **MASTER-A**, **MASTER-B**, and an **OpenBridge** leg should hear it. On the dashboard and on the air that is a **bridge** (the TG 52090 table).
|
|
|
|
|
|
**Legacy (`adn-dmr-server`)** — everything in one global dict:
|
|
|
|
|
|
```text
|
|
|
BRIDGES["52090"] = [
|
|
|
{ SYSTEM: "MASTER-A", TS: 2, ACTIVE: True, TGID: …, TIMER: … },
|
|
|
{ SYSTEM: "MASTER-B", TS: 2, ACTIVE: True, TGID: … },
|
|
|
{ SYSTEM: "OBP-UK", TS: 1, ACTIVE: False, TGID: … },
|
|
|
]
|
|
|
```
|
|
|
|
|
|
When a voice frame arrives:
|
|
|
|
|
|
1. Find the row where **this** system is the **ACTIVE** source (same TG/slot).
|
|
|
2. Walk the **other ACTIVE rows** in the same table.
|
|
|
3. For each, call **`to_target`** → forward with rewritten LC.
|
|
|
|
|
|
The monitor reads **the same dict** (pickle **`BRIDGE_SND`**).
|
|
|
|
|
|
**New server (`adn-server` 2.x)** — same table, different internal shape:
|
|
|
|
|
|
```text
|
|
|
SubscriptionStore — TG 52090 / slot 2:
|
|
|
- subscription MASTER-A (ACTIVE, target_tgid, timer…)
|
|
|
- subscription MASTER-B (ACTIVE, …)
|
|
|
- subscription OBP-UK (IDLE, …)
|
|
|
```
|
|
|
|
|
|
On voice, **`SubscriptionRouter.resolve()`** applies the **same rules** (ACTIVE source, other ACTIVE legs) and returns **`ForwardLeg`** entries to forward. The monitor gets an **exported view** (`BRIDGE_SND` or JSON **`routing_table`**); that export does **not** drive forwarding.
|
|
|
|
|
|
## Where to read code
|
|
|
|
|
|
| Topic | Legacy | New |
|
|
|
|-------|--------|-----|
|
|
|
| Voice ingress | `adn-dmr-server/bridge_master.py` (`routerHBP.dmrd_received`) | `application/routing_use_cases.py` |
|
|
|
| Forward to leg | `to_target` | `application/routing/hbp_forward.py`, `obp_forward.py` |
|
|
|
| Table state | global `BRIDGES` | `application/subscription/` (`store`, `router`, ops) |
|
|
|
| Monitor export | `send_routing_table` / pickle | `routing_table_legacy_view.py`, report v2 `routing_table` |
|
|
|
|
|
|
See also: [Bridges and talkgroups](../user-guide/bridges-and-talkgroups.md), [Architecture](architecture.md), [Performance (2.x)](performance.md), [Report protocol v2](../protocols/report-v2.md#routing_table).
|