diff --git a/docs/en/server/development/behaviour-and-timers.md b/docs/en/server/development/behaviour-and-timers.md index 16af023..1b73668 100644 --- a/docs/en/server/development/behaviour-and-timers.md +++ b/docs/en/server/development/behaviour-and-timers.md @@ -25,6 +25,18 @@ The following intervals are part of the current runtime behavior: If you change one of these intervals, document the operational impact for monitoring, loop behavior, and troubleshooting. +## Voice contention constants + +These constants define per-packet and per-session behaviour. They are +documented in detail in [Voice routing and contention](routing-and-contention.md). + +| Constant | Value | Role | +|---|---|---| +| `STREAM_TO` | **0.36 s** | Window to consider a stream "active" (between packets). | +| `_STALE_PEER_SESSION_TIMEOUT` | **5.0 s** | A per-peer session with no frames is considered dead (lost VTERM). | +| `GROUP_HANGTIME` | **5 s** (config default, per-system) | Blocking period after a QSO ends before another TG is accepted on that slot. | +| `DEFAULT_UA_TIMER` | configurable (minutes, per-system) | Duration of dynamic (User Activated) bridges. | + ## In-band VTERM scope In-band bridge signalling on voice terminator (VTERM) is intentionally scoped to: diff --git a/docs/en/server/development/routing-and-contention.md b/docs/en/server/development/routing-and-contention.md new file mode 100644 index 0000000..89eca0c --- /dev/null +++ b/docs/en/server/development/routing-and-contention.md @@ -0,0 +1,359 @@ +# Voice routing and contention rules + +This page documents **how voice frames travel through the server** and the +**contention / slot rules** that govern who hears what. It is the reference +for sysops and integrators who need to understand or troubleshoot routing +behaviour without reading source code. + +The server maintains **legacy parity** with `adn-dmr-server` (`bridge_master.py`) +except for the explicitly documented divergences at the end of this page. + +--- + +## Golden rule: one conversation per TG + +**Only one conversation per TG may exist on the server at a time.** This rule +is **global** — it applies regardless of slot, peer, or traffic origin (OBP or +HBP). It has the highest priority; all other rules are subordinate to it. + +A TG is **busy** when it has an **active** voice stream (frames within +`STREAM_TO` of the last packet) on any slot, from any source. + +| New traffic source | Behaviour | +|---|---| +| Another **OBP** sends the same TG | **Reject** — TG already busy | +| A **hotspot (HBP)** keys the same TG | **Reject**, *or* silent activation (see below) | + +The only exception is **silent activation**, which does **not** create a second +conversation — it only lets the user listen to the existing one. + +--- + +## End-to-end packet flow + +```mermaid +flowchart TD + PTT[User keys PTT
on TG 730500 slot 2] --> HBP[Hotspot sends DMRD
over UDP HBP to PROXY] + HBP --> INJ[PROXY injects into MASTER 'SYSTEM'] + INJ --> AUTH{HBP auth OK?} + AUTH -->|No| DROP1[Drop: peer not authenticated] + AUTH -->|Yes| ACL{ACL for SUB/TG
allows?} + ACL -->|No| DROP2[Drop: ACL] + ACL -->|Yes| DMRD_RX[dmrd_received
update STATUS slot] + DMRD_RX --> CONT{Global contention:
TG busy?} + CONT -->|Busy OBP/HBP| SILENT{Silent activation?
see below} + CONT -->|Free| SRC[source row ACTIVE?
bridge leg] + SRC -->|No| NODRV[No bridge leg
see: dynamic TG] + SRC -->|Yes| FWD[forward to to_target] + NODRV --> FWD + FWD --> FANOUT[Iterate target peers] + FANOUT --> P1{For each peer:
OPTIONS has TG?} + P1 -->|Yes or dynamic active| SLOT{Target slot
free?} + P1 -->|No| SKIP[Skip peer] + SLOT -->|Busy| SKIP + SLOT -->|Free| REWRITE[Rewrite LC
+ slot mapping] + REWRITE --> TX[Send DMRD
to peer on its slot] + TX --> NEXT[Next peer / frame] + + style DMRD_RX fill:#bfb,stroke:#333 + style FWD fill:#bbf,stroke:#333 + style TX fill:#fbb,stroke:#333 +``` + +### Decision points in order + +1. **HBP authentication** — the peer must be registered with a valid passphrase. +2. **ACL** — `SUB_ACL`, `TGID_TS1_ACL`, `TGID_TS2_ACL` (when `USE_ACL`). +3. **`dmrd_received`** — updates `STATUS[slot]`: `RX_TIME`, `RX_TGID`, + `RX_STREAM_ID`, `RX_TYPE`, `RX_PEER`. +4. **Global contention** — if the TG already has an active stream from another + source, it is blocked or silently activated. +5. **Source row ACTIVE** — a bridge leg must exist and be `ACTIVE` for that + `system/slot/TG`. If missing, a dynamic one is created. +6. **Fan-out** — for each peer on the MASTER, the downlink gate decides if it + receives the packet. +7. **LC rewrite + slot mapping** — the destination slot is the peer's, not the + source's. + +--- + +## The four contention rules (legacy parity) + +Evaluated **packet by packet** in `to_target` against the `STATUS[slot]` of the +target system. These match `bridge_master.py` lines ~2076–2104. + +| Rule | Condition | Action | +|---|---|---| +| **1. RX hangtime** | TG ≠ `RX_TGID` **and** `(now - RX_TIME) < GROUP_HANGTIME` | `continue` (do not forward) | +| **2. TX hangtime** | TG ≠ `TX_TGID` **and** `(now - TX_TIME) < GROUP_HANGTIME` | `continue` | +| **3. same TG RX active** | TG == `RX_TGID` **and** `(now - RX_TIME) < STREAM_TO` **and** different stream | `continue` | +| **4. same TG TX, other sub** | TG == `TX_TGID` **and** `(now - TX_TIME) < STREAM_TO` **and** other subscriber | `continue` | + +Key facts: + +- `RX_TIME`, `RX_TGID`, `TX_TIME`, `TX_TGID` are **not cleared on VTERM**. They + keep the last value until another QSO overwrites them — that is why hangtime + counts from the last packet. +- Contention is evaluated per-frame, not per-stream. If a stream was blocked by + hangtime and the hangtime then expires, subsequent frames of the same stream + **are** forwarded. +- The `CONTENTION` flag in legacy is only a log debounce; it does not block. + +--- + +## Critical timeouts and constants + +These values define observable behaviour. Changing them affects contention, +hangtime, and reconnect. + +| Constant | Value | Location | Role | +|---|---|---|---| +| `STREAM_TO` | **0.36 s** | `domain/hbp_protocol.py` | Window to consider a stream "active" (between packets). | +| `_STALE_PEER_SESSION_TIMEOUT` | **5.0 s** | `routing/helpers.py` | A per-peer session with no frames is considered dead (VTERM lost). | +| `GROUP_HANGTIME` | **5 s** (config default) | per-system YAML | Blocking period after a QSO ends before another TG is accepted on that slot. | +| `DEFAULT_UA_TIMER` | configurable (minutes) | per-system YAML | Duration of dynamic (User Activated) bridges. | + +See [Behaviour and timers](behaviour-and-timers.md) for the periodic loop +intervals (`rule_timer`, `stream_trimmer`, etc.). + +### Stream states + +```mermaid +stateDiagram-v2 + [*] --> IDLE: slot free + IDLE --> VHEAD: voice header arrives (VHEAD) + VHEAD --> ACTIVE: voice frames within STREAM_TO + ACTIVE --> ACTIVE: each frame renews RX_TIME/TX_TIME + ACTIVE --> VTERM: voice terminator arrives (VTERM) + ACTIVE --> TIMEOUT: no frames > STREAM_TO + VTERM --> HANGTIME: GROUP_HANGTIME seconds + TIMEOUT --> HANGTIME: GROUP_HANGTIME seconds + HANGTIME --> IDLE: hangtime expires + HANGTIME --> ACTIVE: same TG resumes (renews) +``` + +**Lost VTERM:** if the MMDVM never sends a terminator, the per-peer session +expires after `_STALE_PEER_SESSION_TIMEOUT` (5 s), freeing the slot. + +--- + +## SINGLE=1 vs SINGLE=0 (exclusive listen) + +`SINGLE` is set in the hotspot's `OPTIONS` line (e.g. `SINGLE=1;`). It controls +**how many dynamic TGs a peer can have active per slot**. + +```mermaid +flowchart LR + subgraph SINGLE1["SINGLE=1 (exclusive listen)"] + S1A[1 exclusive TG per slot] + S1B[PTT on new TG
= replaces the lock] + S1C[Timer DEFAULT_UA_TIMER
expires → releases] + S1D[TG 4000 → clears lock] + end + subgraph SINGLE0["SINGLE=0 (multi-dynamic)"] + S0A[Several dynamic TGs
accumulated per slot] + S0B[GROUP_HANGTIME governs
between TGs] + S0C[Only 1 downlink stream
at a time per slot] + end +``` + +| Aspect | SINGLE=1 | SINGLE=0 | +|---|---|---| +| Dynamic TGs per slot | **1 exclusive** | **Several accumulated** (`_PEER_UA_MULTI_TGS`) | +| Storage | `_PEER_UA_SESSIONS[peer][slot]` | `_PEER_UA_MULTI_TGS[peer][slot]` | +| Switching TG | PTT on new TG replaces the lock | Adds to the set; does not replace | +| TG 4000 | Clears the slot session | Clears all the peer's dynamics | +| Timer | `DEFAULT_UA_TIMER` expires → releases | No individual expiry; purged by `GROUP_HANGTIME` | +| In-band deactivation | Aggressive: OFF/RESET/TG4000/non-matching traffic | Conservative: mainly TG 4000 | + +### SINGLE exceptions + +- **TG 9990 (echo):** does **not** create a SINGLE lock. Echo always returns to + the caller. +- **TG 4000:** does **not** create a UA session. It is a reset command only. +- **TG 9991–9999 (on-demand):** do not create a SINGLE lock. +- **Own UA session:** a SINGLE=1 peer that activated TG T dynamically **must** + receive downlink for T (it does not self-block). + +--- + +## Static vs dynamic talkgroups + +### Static TG + +Defined in the hotspot's `OPTIONS` (`TS1_STATIC` / `TS2_STATIC`). The peer +always listens to that TG on that slot while connected. + +``` +OPTIONS: TS1=730500;TS2=730502,730508;SINGLE=1;TIMER=60; +``` + +Origins of static TGs: + +- **OPTIONS at login (RPTO)** — the hotspot reports its line. +- **Self-service (web panel)** — the user changes TGs from the browser; the + server sends an updated `RPTO` to the MASTER. +- **Startup/reload** — `apply_startup_bridges` applies TGs at start and on + `SIGHUP`. +- **D-28 (divergence):** there is **no** periodic 26 s loop + (`options_config_loop`). Refresh is event-driven (RPTO, startup, dmrd + no-source fallback). + +### Dynamic TG (User Activated) + +Activated when a user keys a TG that is **not** in their static OPTIONS. + +```mermaid +flowchart TD + PTT[PTT on non-static TG] --> CHECK{TG free?} + CHECK -->|Yes| ACT1[Activate dynamic TG
+ pass voice in 1 TX
divergence vs legacy] + CHECK -->|No, busy| SILENT[Silent activation] + ACT1 --> STORE1[_PEER_UA_SESSIONS or
_PEER_UA_MULTI_TGS] + STORE1 --> PERSIST[Async upsert
peer_dynamic_tgs MariaDB] + ACT1 --> BRIDGE1[Create dynamic bridge leg
ensure_dynamic_relay] + BRIDGE1 --> DL[Peer now listens
to that TG] +``` + +**Difference vs legacy (`adn-dmr-server`):** + +- **Legacy:** two TXs are needed — the 1st activates, the 2nd passes voice. +- **This server:** the 1st TX does both (activate + pass voice). + +See [Dynamic TG persistence (MariaDB)](../user-guide/bridges-and-talkgroups.md#dynamic-tg-persistence-mariadb) +for reconnect survival. + +--- + +## Silent activation (TG busy with active QSO) + +**Intentional divergence.** If a user keys a TG that has an **active** +conversation, the server: + +1. Does **not reject** the TX. +2. **Activates** that TG as dynamic. +3. Does **not forward the user's uplink** (does not disturb the active QSO). +4. **Does deliver the downlink** of the active QSO to the user immediately. + +```mermaid +flowchart TD + TX[User TX on busy TG] --> Q1{TG has active
QSO?} + Q1 -->|No| NORMAL[Normal flow] + Q1 -->|Yes| ACT[Activate dynamic TG] + ACT --> NOUL[Suppress uplink
do not forward user voice] + NOUL --> DL2[Deliver downlink
of active QSO to user] + DL2 --> HANG[Respect GROUP_HANGTIME
for other TGs on that slot] +``` + +Applies to: + +- **Non-static TG** the user wants to hear. +- **SINGLE=1** switching from an active TG to another with an ongoing QSO. + +--- + +## Slot mapping on downlink + +The slot on which a peer **receives** a TG is determined by its `OPTIONS` (if +static) or the slot where it activated it (if dynamic). It is **not** the slot +of the originating transmission. + +```mermaid +flowchart LR + OBP1[OBP sends TG 730500
on slot 1] --> MAP{Slot mapping} + PEA[HS-A has 730500
on TS2] --> MAP + PEB[HS-B has 730500
on TS1] --> MAP + MAP --> OUTA[HS-A receives on TS2] + MAP --> OUTB[HS-B receives on TS1] +``` + +- **OBP always travels on slot 1** of the packet. The slot is informational of + the origin; delivery goes to the peer's slot. +- Two peers with the same TG on different slots can **both** hear the same + transmission, each on its own slot. +- On **simplex (DMO)**, MMDVMHost drops packets with the TS1 bit set; the server + delivers downlink voice on **TS2** for simplex peers. + +--- + +## Downlink gate: does a peer receive the packet? + +For each peer and each group voice frame, the server evaluates a chain of +filters. **All** must pass for the packet to be delivered. + +```mermaid +flowchart TD + PKT[DMRD toward peer] --> F1{OPTIONS has TG
or dynamic active?} + F1 -->|No| DROP[Do not deliver] + F1 -->|Yes| F2{Special TG 9990-9999?} + F2 -->|Yes| PASS1[Bypass slot contention] + F2 -->|No| F3{Target slot
free?} + F3 -->|Peer TXing ingress| DROP + F3 -->|Bridge hold active
different TG| DROP + F3 -->|Active stream
on that slot, different TG| DROP + F3 -->|Free| F4{GROUP_HANGTIME
respected?} + F4 -->|No| DROP + F4 -->|Yes| F5{SINGLE lock
compatible?} + F5 -->|Blocked by other TG| DROP + F5 -->|OK| SEND[Deliver DMRD to peer] + PASS1 --> SEND +``` + +### Conditions that block downlink (per-peer) + +| Condition | Detail | +|---|---| +| **Active ingress** | The peer is transmitting on that slot → no receive until TX ends. | +| **Bridge hold** | The slot has a `bridge_hold` (own ingress or listener) blocking foreign TGs for `GROUP_HANGTIME`. | +| **Active stream, different TG** | A downlink stream is already active on that slot with another TG → wait for it to end. | +| **GROUP_HANGTIME** | If the last TG on that slot was different and is within hangtime → block. | +| **Incompatible SINGLE lock** | SINGLE=1 with a lock on another TG → block, unless it is the lock TG or the peer activated it. | +| **Stale session** | If the per-peer session has had no frames for `_STALE_PEER_SESSION_TIMEOUT`, it is purged and the slot frees. | + +### Mid-call join + +When a downlink stream ends (VTERM or timeout), the peer's slot becomes free. +The **next frame** of any other TG the peer has active (static or dynamic) and +that is currently in progress on the network **is delivered without requiring a +new PTT**. + +This does **not relax** any rule: GROUP_HANGTIME, SINGLE, and per-peer +contention are evaluated exactly as for any stream. It is simply the normal +behaviour when the slot becomes free. + +--- + +## OpenBridge and dynamic TGs + +If a hotspot on server 7302 activated TG 7300 dynamically and a hotspot on +7301 keys that TG, the OBP traffic must reach the 7302 hotspot even though 7300 +is not in its OPTIONS. The bridge leg is activated by the UA session (via +`master_dynamic_tg_slots`), which checks both `_PEER_UA_SESSIONS` (SINGLE=1) +and `_PEER_UA_MULTI_TGS` (SINGLE=0). + +See [OpenBridge protocol](../protocols/openbridge.md) for ingress filters, +BCSQ/BCKA, and DMRE v5 details. + +--- + +## Divergences vs legacy + +| Behaviour | Legacy (`adn-dmr-server`) | This server | +|---|---|---| +| Activate dynamic TG (free TG) | 2 TXs: 1st activates, 2nd passes voice | 1 TX: activates + passes voice | +| TX on busy TG | Blocked by contention (rules 1–4) | Silent activation: activates TG, no uplink, delivers downlink | +| Stream end → join active TG | Peer stays in hangtime; no mid-stream join | When the stream ends, the next active TG is delivered without a new PTT | +| OPTIONS refresh (26 s loop) | `options_config_loop` every 26 s | **D-28:** event-driven (RPTO, startup, dmrd fallback) — no periodic loop | +| `GROUP_HANGTIME` | RX + TX, per-system, no reset on VTERM | **Parity** (same behaviour) | +| `OPTIONS` overrides `GROUP_HANGTIME` | No | **Parity** (cannot) | + +--- + +## See also + +- [Bridges and talkgroups](../user-guide/bridges-and-talkgroups.md) — the + `BRIDGES` / subscription model. +- [Special numbers](../user-guide/special-numbers.md) — TG 4000, 999x, echo. +- [Behaviour and timers](behaviour-and-timers.md) — periodic loop intervals. +- [BRIDGES vs Subscriptions](bridges-vs-subscriptions.md) — internal model. +- [OpenBridge protocol](../protocols/openbridge.md) — DMRE v5, ingress filters. +- [Hotspot proxy](../user-guide/hotspot-proxy.md) — integrated PROXY / self-service. diff --git a/docs/en/server/user-guide/bridges-and-talkgroups.md b/docs/en/server/user-guide/bridges-and-talkgroups.md index 5a05de1..83525b2 100644 --- a/docs/en/server/user-guide/bridges-and-talkgroups.md +++ b/docs/en/server/user-guide/bridges-and-talkgroups.md @@ -57,4 +57,7 @@ For OpenBridge, the **DMR destination** in the packet may differ from the **TGID Group voice uses **hang time**, **`STREAM_TO`**, and slot `TX_*` / `RX_*` state to avoid colliding transmissions on the same resources. +For the full contention rule set, SINGLE behaviour, silent activation, and +slot mapping, see [Voice routing and contention](../development/routing-and-contention.md). + See also: [Special numbers](special-numbers.md), [OpenBridge protocol](../protocols/openbridge.md). diff --git a/docs/en/server/user-guide/introduction.md b/docs/en/server/user-guide/introduction.md index 7a49696..34dc643 100644 --- a/docs/en/server/user-guide/introduction.md +++ b/docs/en/server/user-guide/introduction.md @@ -34,6 +34,7 @@ Routing, timers, OpenBridge loop control, and protocol handling are implemented - [Configuration](configuration.md) — files, `GLOBAL`, **MASTER** / **PEER** / **OPENBRIDGE**, ACLs, **`DATABASE`**, reports, **`PROXY`**, **`SELF_SERVICE`**, aliases, voice merge. - [Bridges and talkgroups](bridges-and-talkgroups.md) — how `BRIDGES` works. +- [Voice routing and contention](../development/routing-and-contention.md) — the full packet flow, contention rules, SINGLE, slot mapping, and divergences. - [Special numbers](special-numbers.md) — TG 4000, information services, echo. - [Hotspot proxy](hotspot-proxy.md) — integrated **`PROXY`** / **`SELF_SERVICE`** in `adn-server.yaml`. - [ADN Monitor](../../monitor/index.md) — dashboard, `adn-monitor.yaml`, self-service UI (separate repo, deployed with the server). diff --git a/docs/en/server/user-guide/special-numbers.md b/docs/en/server/user-guide/special-numbers.md index f195bb9..0e3274b 100644 --- a/docs/en/server/user-guide/special-numbers.md +++ b/docs/en/server/user-guide/special-numbers.md @@ -98,7 +98,7 @@ The **audio** is sent with **source ID 5000** and **destination TG 9** in the ge **Purpose:** Bridge rows for **echo** often use **9990** with the **ECHO** system (see `BRIDGES` and options in your YAML). -**`SINGLE=1`:** Keying **9990** does **not** create an exclusive listen session (same as **4000**). Downlink echo always returns to the calling hotspot even when another TG holds the SINGLE lock. See [Hotspot proxy](hotspot-proxy.md#behaviour-with-multiple-hotspots). +**`SINGLE=1`:** Keying **9990** does **not** create an exclusive listen session (same as **4000**). Downlink echo always returns to the calling hotspot even when another TG holds the SINGLE lock. See [Hotspot proxy](hotspot-proxy.md#behaviour-with-multiple-hotspots) and [Voice routing and contention — SINGLE exceptions](../development/routing-and-contention.md#single-exceptions). **Note:** A **standalone echo** is also available as a separate process — [Echo](echo.md). diff --git a/docs/es/server/development/behaviour-and-timers.md b/docs/es/server/development/behaviour-and-timers.md index 314098c..c7584bc 100644 --- a/docs/es/server/development/behaviour-and-timers.md +++ b/docs/es/server/development/behaviour-and-timers.md @@ -25,6 +25,18 @@ Los siguientes intervalos forman parte del comportamiento actual en ejecución: Si cambias uno de estos intervalos, documenta el impacto operativo en monitorización, comportamiento de bucles y troubleshooting. +## Constantes de contención de voz + +Estas constantes definen el comportamiento por paquete y por sesión. Están +documentadas en detalle en [Enrutado de voz y contención](routing-and-contention.md). + +| Constante | Valor | Rol | +|---|---|---| +| `STREAM_TO` | **0.36 s** | Ventana para considerar un stream "activo" (entre paquetes). | +| `_STALE_PEER_SESSION_TIMEOUT` | **5.0 s** | Una sesión per-peer sin frames se considera muerta (VTERM perdido). | +| `GROUP_HANGTIME` | **5 s** (default config, por sistema) | Bloqueo tras fin de QSO antes de aceptar otro TG en ese slot. | +| `DEFAULT_UA_TIMER` | configurable (minutos, por sistema) | Duración de bridges dinámicos (User Activated). | + ## Alcance de VTERM in-band La señalización in-band en voice terminator (VTERM) está acotada a: diff --git a/docs/es/server/development/routing-and-contention.md b/docs/es/server/development/routing-and-contention.md new file mode 100644 index 0000000..3ab0af0 --- /dev/null +++ b/docs/es/server/development/routing-and-contention.md @@ -0,0 +1,362 @@ +# Enrutado de voz y reglas de contención + +Esta página documenta **cómo viajan los paquetes de voz por el servidor** y las +**reglas de contención / slot** que determinan quién escucha qué. Es la +referencia para sysops e integradores que necesitan entender o diagnosticar el +comportamiento de enrutado sin leer el código fuente. + +El servidor mantiene **paridad legada** con `adn-dmr-server` (`bridge_master.py`) +salvo las divergencias explícitamente documentadas al final de esta página. + +--- + +## Regla de oro: una conversación por TG + +**Sólo una conversación por TG puede existir en el servidor a la vez.** Esta +regla es **global** — aplica sin importar el slot, el peer o el origen del +tráfico (OBP o HBP). Tiene la prioridad más alta; todas las demás reglas se +subordinan a ella. + +Un TG está **ocupado** cuando tiene un stream de voz **activo** (frames dentro +de `STREAM_TO` del último paquete) en cualquier slot, venga de donde venga. + +| Origen del nuevo tráfico | Comportamiento | +|---|---| +| Otro **OBP** envía el mismo TG | **Rechazar** — el TG ya está ocupado | +| Un **hotspot (HBP)** transmite al mismo TG | **Rechazar**, *o* activación silenciosa (ver abajo) | + +La única excepción es la **activación silenciosa**, que **no** crea una segunda +conversación — sólo permite al usuario escuchar la existente. + +--- + +## Flujo de paquetes de extremo a extremo + +```mermaid +flowchart TD + PTT[Usuario pulsa PTT
en TG 730500 slot 2] --> HBP[Hotspot envía DMRD
por UDP HBP al PROXY] + HBP --> INJ[PROXY inyecta en MASTER 'SYSTEM'] + INJ --> AUTH{Auth HBP OK?} + AUTH -->|No| DROP1[Drop: peer no autenticado] + AUTH -->|Sí| ACL{ACL de SUB/TG
permite?} + ACL -->|No| DROP2[Drop: ACL] + ACL -->|Sí| DMRD_RX[dmrd_received
actualiza STATUS slot] + DMRD_RX --> CONT{Contención global:
TG ocupado?} + CONT -->|Ocupado OBP/HBP| SILENT{Activación silenciosa?
ver abajo} + CONT -->|Libre| SRC[source row ACTIVE?
bridge leg] + SRC -->|No| NODRV[No hay bridge leg
ver: TG dinámico] + SRC -->|Sí| FWD[forward a to_target] + NODRV --> FWD + FWD --> FANOUT[Iterar peers destino] + FANOUT --> P1{Para cada peer:
¿OPTIONS tiene TG?} + P1 -->|Sí o dinámico activo| SLOT{¿Slot destino
libre?} + P1 -->|No| SKIP[Skip peer] + SLOT -->|Ocupado| SKIP + SLOT -->|Libre| REWRITE[Rewrite LC
+ slot mapping] + REWRITE --> TX[Enviar DMRD
al peer en su slot] + TX --> NEXT[Próximo peer / frame] + + style DMRD_RX fill:#bfb,stroke:#333 + style FWD fill:#bbf,stroke:#333 + style TX fill:#fbb,stroke:#333 +``` + +### Puntos de decisión en orden + +1. **Autenticación HBP** — el peer debe estar registrado con passphrase válida. +2. **ACL** — `SUB_ACL`, `TGID_TS1_ACL`, `TGID_TS2_ACL` (si `USE_ACL`). +3. **`dmrd_received`** — actualiza `STATUS[slot]`: `RX_TIME`, `RX_TGID`, + `RX_STREAM_ID`, `RX_TYPE`, `RX_PEER`. +4. **Contención global** — si el TG ya tiene un stream activo desde otra + fuente, se bloquea o activa silenciosamente. +5. **Source row ACTIVE** — debe existir una pata de bridge `ACTIVE` para ese + `system/slot/TG`. Si no existe, se crea una dinámica. +6. **Fan-out** — para cada peer del MASTER, el gate de downlink decide si + recibe el paquete. +7. **Rewrite LC + slot mapping** — el slot destino es el del peer, no el de + origen. + +--- + +## Las cuatro reglas de contención (paridad legada) + +Evaluadas **paquete a paquete** en `to_target` contra el `STATUS[slot]` del +sistema destino. Coinciden con `bridge_master.py` líneas ~2076–2104. + +| Regla | Condición | Acción | +|---|---|---| +| **1. RX hangtime** | TG ≠ `RX_TGID` **y** `(now - RX_TIME) < GROUP_HANGTIME` | `continue` (no enruta) | +| **2. TX hangtime** | TG ≠ `TX_TGID` **y** `(now - TX_TIME) < GROUP_HANGTIME` | `continue` | +| **3. mismo TG RX activo** | TG == `RX_TGID` **y** `(now - RX_TIME) < STREAM_TO` **y** stream distinto | `continue` | +| **4. mismo TG TX, otro sub** | TG == `TX_TGID` **y** `(now - TX_TIME) < STREAM_TO` **y** otro suscriptor | `continue` | + +Hechos clave: + +- `RX_TIME`, `RX_TGID`, `TX_TIME`, `TX_TGID` **no se borran en VTERM**. + Conservan el último valor hasta que otro QSO los sobrescribe — por eso el + hangtime cuenta desde el último paquete. +- La contención se evalúa por frame, no por stream. Si un stream fue bloqueado + por hangtime y luego el hangtime expira, los siguientes frames del mismo + stream **sí** se reenvían. +- El flag `CONTENTION` del legado es sólo un debounce de log; no bloquea. + +--- + +## Timeouts y constantes críticas + +Estos valores definen el comportamiento observable. Cambiarlos afecta +contención, hangtime y reconexión. + +| Constante | Valor | Ubicación | Rol | +|---|---|---|---| +| `STREAM_TO` | **0.36 s** | `domain/hbp_protocol.py` | Ventana para considerar un stream "activo" (entre paquetes). | +| `_STALE_PEER_SESSION_TIMEOUT` | **5.0 s** | `routing/helpers.py` | Una sesión per-peer sin frames se considera muerta (VTERM perdido). | +| `GROUP_HANGTIME` | **5 s** (default config) | por system en YAML | Bloqueo tras fin de QSO antes de aceptar otro TG en ese slot. | +| `DEFAULT_UA_TIMER` | configurable (minutos) | por system en YAML | Duración de bridges dinámicos (User Activated). | + +Ver [Comportamiento y temporizadores](behaviour-and-timers.md) para los +intervalos de los bucles periódicos (`rule_timer`, `stream_trimmer`, etc.). + +### Estados de un stream + +```mermaid +stateDiagram-v2 + [*] --> IDLE: slot libre + IDLE --> VHEAD: llega voice header (VHEAD) + VHEAD --> ACTIVE: frames de voz dentro de STREAM_TO + ACTIVE --> ACTIVE: cada frame renueva RX_TIME/TX_TIME + ACTIVE --> VTERM: llega voice terminator (VTERM) + ACTIVE --> TIMEOUT: sin frames > STREAM_TO + VTERM --> HANGTIME: GROUP_HANGTIME segundos + TIMEOUT --> HANGTIME: GROUP_HANGTIME segundos + HANGTIME --> IDLE: expira hangtime + HANGTIME --> ACTIVE: mismo TG reanuda (renueva) +``` + +**VTERM perdido:** si el MMDVM nunca envía el terminator, la sesión per-peer +expira tras `_STALE_PEER_SESSION_TIMEOUT` (5 s), liberando el slot. + +--- + +## SINGLE=1 vs SINGLE=0 (escucha exclusiva) + +`SINGLE` se configura en la línea `OPTIONS` del hotspot (ej: `SINGLE=1;`). +Controla **cuántos TG dinámicos puede tener activos un peer por slot**. + +```mermaid +flowchart LR + subgraph SINGLE1["SINGLE=1 (escucha exclusiva)"] + S1A[1 TG exclusivo por slot] + S1B[PTT en nuevo TG
= reemplaza el lock] + S1C[Timer DEFAULT_UA_TIMER
expira → libera] + S1D[TG 4000 → borra lock] + end + subgraph SINGLE0["SINGLE=0 (multi-dinámico)"] + S0A[Varios TG dinámicos
acumulados por slot] + S0B[GROUP_HANGTIME rige
entre TGs] + S0C[Sólo 1 stream downlink
a la vez por slot] + end +``` + +| Aspecto | SINGLE=1 | SINGLE=0 | +|---|---|---| +| TGs dinámicos por slot | **1 exclusivo** | **Varios acumulados** (`_PEER_UA_MULTI_TGS`) | +| Almacenamiento | `_PEER_UA_SESSIONS[peer][slot]` | `_PEER_UA_MULTI_TGS[peer][slot]` | +| Cambiar de TG | PTT en nuevo TG reemplaza el lock | Se suma al set; no reemplaza | +| TG 4000 | Borra la sesión del slot | Borra todos los dinámicos del peer | +| Timer | `DEFAULT_UA_TIMER` expira → libera | No expira individualmente; purga por `GROUP_HANGTIME` | +| Desactivación in-band | Agresiva: OFF/RESET/TG4000/tráfico no matching | Conservadora: principalmente TG 4000 | + +### Excepciones a SINGLE + +- **TG 9990 (eco):** **no** crea lock SINGLE. El eco vuelve siempre al llamante. +- **TG 4000:** **no** crea sesión UA. Es sólo comando de reset. +- **TG 9991–9999 (bajo demanda):** no crean lock SINGLE. +- **UA session propia:** un peer SINGLE=1 que activó el TG T dinámicamente + **debe** recibir downlink de T (no se auto-bloquea). + +--- + +## Talkgroups estáticos vs dinámicos + +### TG estático + +Definido en la línea `OPTIONS` del hotspot (`TS1_STATIC` / `TS2_STATIC`). El +peer siempre escucha ese TG en ese slot mientras esté conectado. + +``` +OPTIONS: TS1=730500;TS2=730502,730508;SINGLE=1;TIMER=60; +``` + +Origen de los TG estáticos: + +- **OPTIONS al login (RPTO)** — el hotspot reporta su línea. +- **Self-service (panel web)** — el usuario cambia sus TG desde el navegador; + el servidor envía un `RPTO` actualizado al MASTER. +- **Startup/reload** — `apply_startup_bridges` aplica los TG al arranque y en + `SIGHUP`. +- **D-28 (divergencia):** **no** hay loop periódico de 26 s + (`options_config_loop`). El refresco es por evento (RPTO, startup, fallback + dmrd sin source). + +### TG dinámico (User Activated) + +Activado cuando un usuario transmite en un TG que **no** tiene estático. + +```mermaid +flowchart TD + PTT[PTT en TG no estático] --> CHECK{TG libre?} + CHECK -->|Sí| ACT1[Activar TG dinámico
+ pasar voz en 1 TX
divergencia vs legacy] + CHECK -->|No, ocupado| SILENT[Activación silenciosa] + ACT1 --> STORE1[_PEER_UA_SESSIONS o
_PEER_UA_MULTI_TGS] + STORE1 --> PERSIST[Upsert asíncrono
peer_dynamic_tgs MariaDB] + ACT1 --> BRIDGE1[Crear bridge leg dinámica
ensure_dynamic_relay] + BRIDGE1 --> DL[Peer queda escuchando
ese TG] +``` + +**Diferencia vs legacy (`adn-dmr-server`):** + +- **Legacy:** se necesitan 2 TX — la 1ª activa, la 2ª pasa voz. +- **Este servidor:** la 1ª TX hace ambas cosas (activar + pasar voz). + +Ver [Persistencia TG dinámicos (MariaDB)](../user-guide/bridges-and-talkgroups.md#persistencia-tg-dinamicos-mariadb) +para la supervivencia tras reconexión. + +--- + +## Activación silenciosa (TG ocupado con QSO activo) + +**Divergencia intencionada.** Si un usuario transmite en un TG que tiene una +conversación **activa**, el servidor: + +1. **No rechaza** la TX. +2. **Activa** ese TG como dinámico. +3. **No sube el uplink** del usuario (no molesta el QSO activo). +4. **Sí entrega el downlink** del QSO activo al usuario inmediatamente. + +```mermaid +flowchart TD + TX[Usuario TX en TG ocupado] --> Q1{TG tiene QSO
activo?} + Q1 -->|No| NORMAL[Flujo normal] + Q1 -->|Sí| ACT[Activar TG dinámico] + ACT --> NOUL[Suprimir uplink
no reenviar voz del usuario] + NOUL --> DL2[Entregar downlink
del QSO activo al usuario] + DL2 --> HANG[Respetar GROUP_HANGTIME
para otros TGs en ese slot] +``` + +Aplica a: + +- **TG no estático** que el usuario quiere escuchar. +- **SINGLE=1** cambiando de TG activo a otro con QSO en curso. + +--- + +## Mapeo de slot en downlink + +El slot por el que un peer **recibe** un TG lo determina su `OPTIONS` (si es +estático) o el slot donde lo activó (si es dinámico). **No** es el slot de +origen de la transmisión. + +```mermaid +flowchart LR + OBP1[OBP envía TG 730500
en slot 1] --> MAP{Slot mapping} + PEA[HS-A tiene 730500
en TS2] --> MAP + PEB[HS-B tiene 730500
en TS1] --> MAP + MAP --> OUTA[HS-A recibe en TS2] + MAP --> OUTB[HS-B recibe en TS1] +``` + +- **OBP siempre viaja en slot 1** del paquete. El slot es informativo del + origen; la entrega se hace al slot del peer destino. +- Dos peers con el mismo TG en slots distintos pueden **ambos** escuchar la + misma transmisión, cada uno en su slot. +- En **simplex (DMO)**, MMDVMHost descarta paquetes con el bit TS1; el servidor + entrega voz de downlink en **TS2** para peers simplex. + +--- + +## Gate de downlink: ¿un peer recibe el paquete? + +Para cada peer y cada frame de voz de grupo, el servidor evalúa una cadena de +filtros. **Todos** deben pasar para que el paquete se entregue. + +```mermaid +flowchart TD + PKT[DMRD hacia peer] --> F1{OPTIONS tiene TG
o dinámico activo?} + F1 -->|No| DROP[No entrega] + F1 -->|Sí| F2{TG especial 9990-9999?} + F2 -->|Sí| PASS1[Bypass slot contention] + F2 -->|No| F3{¿Slot destino
libre?} + F3 -->|Peer TXing ingress| DROP + F3 -->|Bridge hold activo
TG distinto| DROP + F3 -->|Stream activo
en ese slot, TG distinto| DROP + F3 -->|Libre| F4{GROUP_HANGTIME
respeta?} + F4 -->|No| DROP + F4 -->|Sí| F5{SINGLE lock
compatible?} + F5 -->|Bloqueado por otro TG| DROP + F5 -->|OK| SEND[Entregar DMRD al peer] + PASS1 --> SEND +``` + +### Condiciones que bloquean downlink (per-peer) + +| Condición | Detalle | +|---|---| +| **Ingress activo** | El peer está transmitiendo en ese slot → no recibe hasta terminar TX. | +| **Bridge hold** | El slot tiene un `bridge_hold` (ingress propio o listener) que bloquea TGs ajenos por `GROUP_HANGTIME`. | +| **Stream activo, TG distinto** | Ya hay un stream downlink activo en ese slot con otro TG → espera a que termine. | +| **GROUP_HANGTIME** | Si el último TG en ese slot fue distinto y está dentro del hangtime → bloquea. | +| **SINGLE lock incompatible** | SINGLE=1 con lock en otro TG → bloquea salvo que sea el TG del lock o el peer lo activara. | +| **Sesión stale** | Si la sesión per-peer lleva `_STALE_PEER_SESSION_TIMEOUT` sin frames, se purga y el slot se libera. | + +### Mid-call join + +Cuando un stream downlink termina (VTERM o timeout), el slot del peer queda +libre. El **siguiente frame** de cualquier otro TG que el peer tenga activo +(estático o dinámico) y que esté en curso en la red **se entrega sin requerir +nuevo PTT**. + +Esto **no relaja** ninguna regla: GROUP_HANGTIME, SINGLE y contención per-peer +se evalúan igual que para cualquier stream. Es simplemente el comportamiento +normal cuando el slot queda libre. + +--- + +## OpenBridge y TGs dinámicos + +Si un hotspot en el servidor 7302 activó el TG 7300 dinámicamente y un +hotspot en 7301 transmite ese TG, el tráfico OBP debe llegar al hotspot 7302 +aunque 7300 no esté en sus OPTIONS. La pata de bridge se activa por la sesión +UA (vía `master_dynamic_tg_slots`), que consulta tanto `_PEER_UA_SESSIONS` +(SINGLE=1) como `_PEER_UA_MULTI_TGS` (SINGLE=0). + +Ver [Protocolo OpenBridge](../protocols/openbridge.md) para filtros de ingreso, +BCSQ/BCKA y detalles de DMRE v5. + +--- + +## Divergencias vs legacy + +| Comportamiento | Legacy (`adn-dmr-server`) | Este servidor | +|---|---|---| +| Activar TG dinámico (TG libre) | 2 TX: 1ª activa, 2ª pasa voz | 1 TX: activa + pasa voz | +| TX en TG ocupado | Se bloquea por contención (reglas 1–4) | Activación silenciosa: activa TG, no sube uplink, entrega downlink | +| Fin de stream → join TG activo | El peer queda en hangtime; no hay join mid-stream | Al terminar el stream, el siguiente TG activo se entrega sin nuevo PTT | +| OPTIONS refresh (loop 26 s) | `options_config_loop` cada 26 s | **D-28:** por evento (RPTO, startup, fallback dmrd) — sin loop periódico | +| `GROUP_HANGTIME` | RX + TX, por sistema, sin reset en VTERM | **Paridad** (mismo comportamiento) | +| `OPTIONS` sobrescribe `GROUP_HANGTIME` | No | **Paridad** (no se puede) | + +--- + +## Ver también + +- [Bridges y talkgroups](../user-guide/bridges-and-talkgroups.md) — el modelo + `BRIDGES` / subscriptions. +- [Números especiales](../user-guide/special-numbers.md) — TG 4000, 999x, eco. +- [Comportamiento y temporizadores](behaviour-and-timers.md) — intervalos de + bucles periódicos. +- [BRIDGES vs Subscriptions](bridges-vs-subscriptions.md) — modelo interno. +- [Protocolo OpenBridge](../protocols/openbridge.md) — DMRE v5, filtros de + ingreso. +- [Proxy hotspot](../user-guide/hotspot-proxy.md) — PROXY / self-service + integrados. diff --git a/docs/es/server/user-guide/bridges-and-talkgroups.md b/docs/es/server/user-guide/bridges-and-talkgroups.md index 6c8fb65..f33e495 100644 --- a/docs/es/server/user-guide/bridges-and-talkgroups.md +++ b/docs/es/server/user-guide/bridges-and-talkgroups.md @@ -57,4 +57,7 @@ En OpenBridge, el **destino DMR** en el paquete puede diferir del **TGID** en un La voz de grupo usa **hang time**, **`STREAM_TO`** y estado de slot `TX_*` / `RX_*` para evitar transmisiones simultáneas en los mismos recursos. +Para el conjunto completo de reglas de contención, comportamiento SINGLE, +activación silenciosa y mapeo de slot, ver [Enrutado de voz y contención](../development/routing-and-contention.md). + Ver también: [Números especiales](special-numbers.md), [Protocolo OpenBridge](../protocols/openbridge.md). diff --git a/docs/es/server/user-guide/introduction.md b/docs/es/server/user-guide/introduction.md index a9a5758..ddb985b 100644 --- a/docs/es/server/user-guide/introduction.md +++ b/docs/es/server/user-guide/introduction.md @@ -34,6 +34,7 @@ Enrutado, temporizadores, control de bucle OpenBridge y manejo de protocolo est - [Configuración](configuration.md) — ficheros, `GLOBAL`, **MASTER** / **PEER** / **OPENBRIDGE**, ACL, **`DATABASE`**, informes, **`PROXY`**, **`SELF_SERVICE`**, alias, fusión de voz. - [Bridges y talkgroups](bridges-and-talkgroups.md) — cómo funciona `BRIDGES`. +- [Enrutado de voz y contención](../development/routing-and-contention.md) — el flujo completo de paquetes, reglas de contención, SINGLE, mapeo de slot y divergencias. - [Números especiales](special-numbers.md) — TG 4000, servicios de información, eco. - [Proxy hotspot](hotspot-proxy.md) — **`PROXY`** / **`SELF_SERVICE`** integrados en `adn-server.yaml`. - [ADN Monitor](../../monitor/index.md) — panel, `adn-monitor.yaml`, UI self-service (repo aparte, desplegado con el servidor). diff --git a/docs/es/server/user-guide/special-numbers.md b/docs/es/server/user-guide/special-numbers.md index ccabdf4..3934847 100644 --- a/docs/es/server/user-guide/special-numbers.md +++ b/docs/es/server/user-guide/special-numbers.md @@ -98,7 +98,7 @@ El **audio** se envía con **ID de fuente 5000** y **TG de destino 9** en el flu **Propósito:** las filas de bridge para **eco** suelen usar **9990** con el sistema **ECHO** (ver `BRIDGES` y opciones en tu YAML). -**`SINGLE=1`:** pulsar **9990** **no** crea sesión de escucha exclusiva (igual que **4000**). El downlink del eco vuelve siempre al hotspot llamante aunque otra TG tenga el bloqueo SINGLE. Ver [Proxy hotspot](hotspot-proxy.md#comportamiento-con-varios-hotspots). +**`SINGLE=1`:** pulsar **9990** **no** crea sesión de escucha exclusiva (igual que **4000**). El downlink del eco vuelve siempre al hotspot llamante aunque otra TG tenga el bloqueo SINGLE. Ver [Proxy hotspot](hotspot-proxy.md#comportamiento-con-varios-hotspots) y [Enrutado de voz y contención — Excepciones a SINGLE](../development/routing-and-contention.md#excepciones-a-single). **Nota:** Un **echo independiente** también está disponible como proceso aparte — [Echo](echo.md). diff --git a/mkdocs.es.yml b/mkdocs.es.yml index f5e97fa..d4a444c 100644 --- a/mkdocs.es.yml +++ b/mkdocs.es.yml @@ -75,6 +75,7 @@ nav: - Rendimiento (2.x): server/development/performance.md - BRIDGES vs Subscriptions: server/development/bridges-vs-subscriptions.md - Comportamiento y temporizadores: server/development/behaviour-and-timers.md + - Enrutado de voz y contención: server/development/routing-and-contention.md - Contribuir: - Traducciones: server/contributing/translations.md - Monitor: diff --git a/mkdocs.yml b/mkdocs.yml index f817ca8..8113f63 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,7 @@ nav: - Performance (2.x): server/development/performance.md - BRIDGES vs Subscriptions: server/development/bridges-vs-subscriptions.md - Behaviour and timers: server/development/behaviour-and-timers.md + - Voice routing and contention: server/development/routing-and-contention.md - Testing: server/development/testing.md - Contributing: - Translations: server/contributing/translations.md