docs: add voice routing and contention page (EN/ES)

Add a dedicated public page explaining routing, contention, timers,
SINGLE mode, static/dynamic TGs, silent activation, slot mapping and
OBP parity. Cross-link from behaviour-and-timers, bridges-and-talkgroups,
introduction and special-numbers, and register it in both mkdocs navs.
pull/29/head
Rodrigo Pérez 3 months ago
parent 928beb6ca7
commit 2ab98196b0

@ -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:

@ -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<br/>on TG 730500 slot 2] --> HBP[Hotspot sends DMRD<br/>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<br/>allows?}
ACL -->|No| DROP2[Drop: ACL]
ACL -->|Yes| DMRD_RX[dmrd_received<br/>update STATUS slot]
DMRD_RX --> CONT{Global contention:<br/>TG busy?}
CONT -->|Busy OBP/HBP| SILENT{Silent activation?<br/>see below}
CONT -->|Free| SRC[source row ACTIVE?<br/>bridge leg]
SRC -->|No| NODRV[No bridge leg<br/>see: dynamic TG]
SRC -->|Yes| FWD[forward to to_target]
NODRV --> FWD
FWD --> FANOUT[Iterate target peers]
FANOUT --> P1{For each peer:<br/>OPTIONS has TG?}
P1 -->|Yes or dynamic active| SLOT{Target slot<br/>free?}
P1 -->|No| SKIP[Skip peer]
SLOT -->|Busy| SKIP
SLOT -->|Free| REWRITE[Rewrite LC<br/>+ slot mapping]
REWRITE --> TX[Send DMRD<br/>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<br/>= replaces the lock]
S1C[Timer DEFAULT_UA_TIMER<br/>expires → releases]
S1D[TG 4000 → clears lock]
end
subgraph SINGLE0["SINGLE=0 (multi-dynamic)"]
S0A[Several dynamic TGs<br/>accumulated per slot]
S0B[GROUP_HANGTIME governs<br/>between TGs]
S0C[Only 1 downlink stream<br/>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<br/>+ pass voice in 1 TX<br/>divergence vs legacy]
CHECK -->|No, busy| SILENT[Silent activation]
ACT1 --> STORE1[_PEER_UA_SESSIONS or<br/>_PEER_UA_MULTI_TGS]
STORE1 --> PERSIST[Async upsert<br/>peer_dynamic_tgs MariaDB]
ACT1 --> BRIDGE1[Create dynamic bridge leg<br/>ensure_dynamic_relay]
BRIDGE1 --> DL[Peer now listens<br/>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<br/>QSO?}
Q1 -->|No| NORMAL[Normal flow]
Q1 -->|Yes| ACT[Activate dynamic TG]
ACT --> NOUL[Suppress uplink<br/>do not forward user voice]
NOUL --> DL2[Deliver downlink<br/>of active QSO to user]
DL2 --> HANG[Respect GROUP_HANGTIME<br/>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<br/>on slot 1] --> MAP{Slot mapping}
PEA[HS-A has 730500<br/>on TS2] --> MAP
PEB[HS-B has 730500<br/>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<br/>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<br/>free?}
F3 -->|Peer TXing ingress| DROP
F3 -->|Bridge hold active<br/>different TG| DROP
F3 -->|Active stream<br/>on that slot, different TG| DROP
F3 -->|Free| F4{GROUP_HANGTIME<br/>respected?}
F4 -->|No| DROP
F4 -->|Yes| F5{SINGLE lock<br/>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.

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

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

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

@ -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:

@ -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<br/>en TG 730500 slot 2] --> HBP[Hotspot envía DMRD<br/>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<br/>permite?}
ACL -->|No| DROP2[Drop: ACL]
ACL -->|Sí| DMRD_RX[dmrd_received<br/>actualiza STATUS slot]
DMRD_RX --> CONT{Contención global:<br/>TG ocupado?}
CONT -->|Ocupado OBP/HBP| SILENT{Activación silenciosa?<br/>ver abajo}
CONT -->|Libre| SRC[source row ACTIVE?<br/>bridge leg]
SRC -->|No| NODRV[No hay bridge leg<br/>ver: TG dinámico]
SRC -->|Sí| FWD[forward a to_target]
NODRV --> FWD
FWD --> FANOUT[Iterar peers destino]
FANOUT --> P1{Para cada peer:<br/>¿OPTIONS tiene TG?}
P1 -->|Sí o dinámico activo| SLOT{¿Slot destino<br/>libre?}
P1 -->|No| SKIP[Skip peer]
SLOT -->|Ocupado| SKIP
SLOT -->|Libre| REWRITE[Rewrite LC<br/>+ slot mapping]
REWRITE --> TX[Enviar DMRD<br/>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<br/>= reemplaza el lock]
S1C[Timer DEFAULT_UA_TIMER<br/>expira → libera]
S1D[TG 4000 → borra lock]
end
subgraph SINGLE0["SINGLE=0 (multi-dinámico)"]
S0A[Varios TG dinámicos<br/>acumulados por slot]
S0B[GROUP_HANGTIME rige<br/>entre TGs]
S0C[Sólo 1 stream downlink<br/>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<br/>+ pasar voz en 1 TX<br/>divergencia vs legacy]
CHECK -->|No, ocupado| SILENT[Activación silenciosa]
ACT1 --> STORE1[_PEER_UA_SESSIONS o<br/>_PEER_UA_MULTI_TGS]
STORE1 --> PERSIST[Upsert asíncrono<br/>peer_dynamic_tgs MariaDB]
ACT1 --> BRIDGE1[Crear bridge leg dinámica<br/>ensure_dynamic_relay]
BRIDGE1 --> DL[Peer queda escuchando<br/>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<br/>activo?}
Q1 -->|No| NORMAL[Flujo normal]
Q1 -->|Sí| ACT[Activar TG dinámico]
ACT --> NOUL[Suprimir uplink<br/>no reenviar voz del usuario]
NOUL --> DL2[Entregar downlink<br/>del QSO activo al usuario]
DL2 --> HANG[Respetar GROUP_HANGTIME<br/>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<br/>en slot 1] --> MAP{Slot mapping}
PEA[HS-A tiene 730500<br/>en TS2] --> MAP
PEB[HS-B tiene 730500<br/>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<br/>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<br/>libre?}
F3 -->|Peer TXing ingress| DROP
F3 -->|Bridge hold activo<br/>TG distinto| DROP
F3 -->|Stream activo<br/>en ese slot, TG distinto| DROP
F3 -->|Libre| F4{GROUP_HANGTIME<br/>respeta?}
F4 -->|No| DROP
F4 -->|Sí| F5{SINGLE lock<br/>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.

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

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

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

@ -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:

@ -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

Loading…
Cancel
Save

Powered by TurnKey Linux.