You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ADN-DMR-Peer-Server/docs/en/server/development/bridges-vs-subscriptions.md

209 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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).

Powered by TurnKey Linux.