From f6aaa9cb0997092f62d5108fa178444a8375fa31 Mon Sep 17 00:00:00 2001 From: ce5rpy <169016246+ce5rpy@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:24:56 -0400 Subject: [PATCH] Release 2.0.0-rc.4 (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: expand server guide and add report-proxy for legacy dashboards Document ADN-report-proxy so adn-server 2.x can feed v1 legacy monitors, and sync EN/ES MkDocs with DATABASE, dynamic TGs, monitoring, and related topics. * docs: add bridges vs subscriptions, performance guide, and Mermaid diagrams (#6) Document 2.x routing model with a concrete TG 52090 example, operator-focused performance gains (indexes, reporting, integrated proxy), and MkDocs Mermaid support across server and monitor architecture pages. * fix: echo TG 9990 monitor parity and UA session exclusions (#7) Exclude 9990–9999 from SINGLE/UA locks, remap inject-only echo TX events, resolve bridge TX peer_id for monitor display, and fix dynamic TG restore callback. * chore: bump version to 2.0.0-rc.4 --- CHANGELOG.md | 15 ++ README.md | 2 +- docs/en/README.md | 4 +- docs/en/monitor/architecture.md | 26 ++- docs/en/monitor/configuration.md | 12 + docs/en/monitor/index.md | 4 +- docs/en/monitor/self-service.md | 17 ++ docs/en/server/development/architecture.md | 13 ++ .../development/behaviour-and-timers.md | 1 + .../development/bridges-vs-subscriptions.md | 208 ++++++++++++++++++ docs/en/server/development/performance.md | 99 +++++++++ docs/en/server/protocols/report-v2.md | 16 ++ .../user-guide/bridges-and-talkgroups.md | 26 +++ docs/en/server/user-guide/configuration.md | 39 +++- docs/en/server/user-guide/hotspot-proxy.md | 10 +- docs/en/server/user-guide/introduction.md | 8 +- docs/en/server/user-guide/monitoring.md | 27 +++ docs/en/server/user-guide/report-proxy.md | 100 +++++++++ docs/en/server/user-guide/special-numbers.md | 23 +- docs/es/README.md | 2 + docs/es/monitor/architecture.md | 26 ++- docs/es/monitor/configuration.md | 4 +- docs/es/monitor/index.md | 4 +- docs/es/monitor/self-service.md | 17 ++ docs/es/server/development/architecture.md | 13 ++ .../development/behaviour-and-timers.md | 1 + .../development/bridges-vs-subscriptions.md | 208 ++++++++++++++++++ docs/es/server/development/performance.md | 99 +++++++++ docs/es/server/protocols/report-v2.md | 16 ++ .../user-guide/bridges-and-talkgroups.md | 26 +++ docs/es/server/user-guide/configuration.md | 39 +++- docs/es/server/user-guide/hotspot-proxy.md | 10 +- docs/es/server/user-guide/introduction.md | 8 +- docs/es/server/user-guide/monitoring.md | 27 +++ docs/es/server/user-guide/report-proxy.md | 100 +++++++++ docs/es/server/user-guide/special-numbers.md | 21 +- mkdocs.es.yml | 10 +- mkdocs.yml | 10 +- pyproject.toml | 4 +- src/adn_server/__init__.py | 2 +- .../application/dynamic_tg_use_cases.py | 2 +- .../application/report/monitor_topology.py | 17 +- src/adn_server/application/routing/helpers.py | 10 +- .../application/routing_use_cases.py | 20 +- tests/application/test_dynamic_tg_persist.py | 46 ++++ tests/application/test_monitor_topology.py | 7 + .../application/test_peer_single_downlink.py | 21 ++ 47 files changed, 1353 insertions(+), 67 deletions(-) create mode 100644 docs/en/server/development/bridges-vs-subscriptions.md create mode 100644 docs/en/server/development/performance.md create mode 100644 docs/en/server/user-guide/report-proxy.md create mode 100644 docs/es/server/development/bridges-vs-subscriptions.md create mode 100644 docs/es/server/development/performance.md create mode 100644 docs/es/server/user-guide/report-proxy.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a819f..b181d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to **adn-server** are documented here. +## [2.0.0-rc.4] - 2026-06-18 + +### Added + +- **Docs (MkDocs EN/ES):** bridges vs subscriptions, performance notes, report-proxy guide for legacy dashboards, Mermaid diagrams in architecture/monitoring pages. + +### Fixed + +- **Echo TG 9990** — excluded from SINGLE/UA session locks; inject-only monitor remap for echo TX legs; bridge TX report field 5 resolves hotspot radio id for ECHO/hotspot live chips. +- **Dynamic TG restore** — `sync_restored_dynamic_tgs` passes `now=` as keyword (fixes TypeError on echo startup after DB restore). + +### Compatibility + +- **Monitor:** adn-monitor **2.0.0-rc.5**. + ## [2.0.0-rc.3] - 2026-06-17 ### Added diff --git a/README.md b/README.md index 174de73..b54f590 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ADN DMR Peer Server -**Version 2.0.0-rc.3** — pairs with **adn-monitor 2.0.0-rc.4** (report v2 slim wire + JSON HELLO). +**Version 2.0.0-rc.4** — pairs with **adn-monitor 2.0.0-rc.5** (report v2 slim wire + JSON HELLO). ADN DMR conference bridge server. Configuration is YAML; the codebase follows clean architecture (domain, application, infrastructure). v2 adds integrated **PROXY**, **SubscriptionStore** routing, report v2 to the monitor, and a unified **`adn-server.py`** entrypoint (`--echo`, `--doctor`, `--no-proxy`). See [CHANGELOG.md](CHANGELOG.md). diff --git a/docs/en/README.md b/docs/en/README.md index fa9ee37..1baeb9b 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -24,6 +24,7 @@ The **ADN DMR Peer Server** is a [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0. | TG 4000, 999x, echo | [Special numbers](server/user-guide/special-numbers.md) | | Private calls | [Private calls](server/user-guide/private-calls.md) | | Voice / TTS | [Voice, announcements, and TTS](server/user-guide/voice-and-tts.md) | +| Legacy dashboard + server 2.x | [Report proxy](server/user-guide/report-proxy.md) | | OpenBridge / DMRE | [OpenBridge](server/protocols/openbridge.md), [DMRE v5](server/protocols/dmre-v5.md) | | HBP | [HBP](server/protocols/hbp.md) | | Code layout | [Architecture](server/development/architecture.md), [Behaviour and timers](server/development/behaviour-and-timers.md) | @@ -34,6 +35,7 @@ The **ADN DMR Peer Server** is a [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0. ```bash pip install -r requirements.txt cp adn-server.example.yaml adn-server.yaml +# Edit DATABASE (MariaDB) and secrets before production start python adn-server.py -c adn-server.yaml ``` @@ -50,7 +52,7 @@ Dashboard, WebSocket live view, FastAPI API, **MySQL** self-service — see [Mon | `adn-server.yaml` — integrated `PROXY` / `SELF_SERVICE` | [Hotspot proxy (integrated)](server/user-guide/hotspot-proxy.md) | | `adn-monitor.yaml`, layout | [Monitor configuration](monitor/configuration.md) | | Integrated hotspot proxy | [Hotspot proxy](server/user-guide/hotspot-proxy.md) | -| Standalone hotspot proxy (UDP port range) | [Hotspot proxy — standalone](monitor/hotspot-proxy.md#standalone-proxy-legacy-adn-monitor-repo) | +| Standalone hotspot proxy (removed) | [Hotspot proxy — moved](monitor/hotspot-proxy.md) | | Self-service | [Self-service](monitor/self-service.md) | | How it connects to the server | [Monitoring and reports](server/user-guide/monitoring.md) | diff --git a/docs/en/monitor/architecture.md b/docs/en/monitor/architecture.md index 6d3cc33..cafde7c 100644 --- a/docs/en/monitor/architecture.md +++ b/docs/en/monitor/architecture.md @@ -10,6 +10,14 @@ Under `monitor/src/adn_monitor/`: Composition root: `infrastructure/fastapi/composition.py` (`build_monitor_api`). +```mermaid +flowchart TD + INF["Infrastructure
YAML · FastAPI /ws · TCP/MQTT ingest
MySQL repos · pickle/JSON decoders"] + APP["Application
MonitorState · report/dashboard use cases
auth · self-service · aliases"] + DOM["Domain
value objects · errors · opcodes
UserSession · Result"] + INF --> APP --> DOM +``` + ## Unified process (`monitor.py`) Single uvicorn/FastAPI process: @@ -41,15 +49,19 @@ The standalone **`adn-proxy`** process was removed from the **adn-monitor** repo ## Typical deployment topology -```text -[Hotspots] --UDP--> [adn-server PROXY] --UDP--> [peer MASTER] - | - v - MySQL (Clients) +```mermaid +flowchart TD + HS[Hotspots] -->|UDP HBP| PROXY[adn-server PROXY] + PROXY --> MASTER[MASTER inject] + PROXY --> DB[(MySQL Clients)] -[Peer server :REPORT_PORT] <--- TCP or MQTT --- [monitor.py ingest] + MASTER --> REPORT[REPORTS listener] + REPORT <-->|TCP or MQTT| ING[monitor.py ingest] + ING --> APP[FastAPI /api /ws] + APP --> DB -[Browser] --HTTPS--> [Nginx: static frontend + proxy /api,/ws --> MONITOR_APP.LISTEN_PORT] + BR[Browser] -->|HTTPS| FE[Nginx or SERVE_STATIC] + FE --> APP ``` --- diff --git a/docs/en/monitor/configuration.md b/docs/en/monitor/configuration.md index b58ec03..b170b73 100644 --- a/docs/en/monitor/configuration.md +++ b/docs/en/monitor/configuration.md @@ -102,6 +102,18 @@ Obsolete **`WEBSOCKET_SERVER`** YAML (Twisted on a separate port) is ignored; us --- +## Database schema and migrations + +**adn-monitor** ships SQL migrations in `monitor/src/adn_monitor/infrastructure/persistence/schema.py`. Apply with **`db_bootstrap --update`** (or equivalent) when upgrading. + +| Migration | Table / change | +|-----------|----------------| +| **`004_peer_dynamic_tgs`** | **`peer_dynamic_tgs`** — per-peer dynamic TG rows written by **adn-server 2.0.0-rc.3+** (shared schema). | + +**adn-server** also ensures **`peer_dynamic_tgs`** exists on startup (idempotent). Either path is sufficient; both can run against the same **`hbmon`** database. + +--- + ## Environment - **`ADN_CONFIG_PATH`**: Absolute path to **`adn-monitor.yaml`** for **`monitor.py`**. diff --git a/docs/en/monitor/index.md b/docs/en/monitor/index.md index 77f827d..e97bdad 100644 --- a/docs/en/monitor/index.md +++ b/docs/en/monitor/index.md @@ -18,7 +18,9 @@ This chapter documents the **adn-monitor** stack at the same level of detail as | **`adn-server.yaml`** | **`adn-server.py`** (integrated **`PROXY`** / **`SELF_SERVICE`**) | `-c` / default path next to binary | | **`monitor/adn-monitor.yaml`** | **`monitor.py`** | **`ADN_CONFIG_PATH`** | -**`SELF_SERVICE`** (MySQL / PBKDF2) must **match** between **`adn-server.yaml`** and **`adn-monitor.yaml`**. **`ADN_CONNECTION`**, dashboard, WebSocket, and aliases live in **`adn-monitor.yaml`**; integrated **`PROXY`** lives in **`adn-server.yaml`** — see [Hotspot proxy (integrated)](../server/user-guide/hotspot-proxy.md). +**`SELF_SERVICE`** (MySQL / PBKDF2) must **match** between **`adn-server.yaml`** and **`adn-monitor.yaml`**. On the server, MariaDB credentials are in **`DATABASE`** (shared pool for self-service and **`peer_dynamic_tgs`**). **`ADN_CONNECTION`**, dashboard, WebSocket, and aliases live in **`adn-monitor.yaml`**; integrated **`PROXY`** lives in **`adn-server.yaml`** — see [Hotspot proxy (integrated)](../server/user-guide/hotspot-proxy.md). + +**Recommended pairing:** **adn-server 2.0.0-rc.3** + **adn-monitor 2.0.0-rc.4** (dynamic TG persistence, TG 4000 monitor sync). ## Link to the peer server diff --git a/docs/en/monitor/self-service.md b/docs/en/monitor/self-service.md index a94ecca..6c6d7cb 100644 --- a/docs/en/monitor/self-service.md +++ b/docs/en/monitor/self-service.md @@ -41,6 +41,23 @@ Session lifetime is extended on activity (**SelfServiceController** uses a long ## End-to-end flow (why options reach the hotspot) +```mermaid +sequenceDiagram + participant UI as React /self-service + participant API as monitor FastAPI + participant DB as MySQL Clients + participant SRV as adn-server PROXY + participant HS as Hotspot + + UI->>API: POST /api/self-service/device/options + API->>DB: UPDATE options, modified = 1 + Note over SRV: send_opts loop ~every 10 s + SRV->>DB: read rows with modified = 1 + SRV->>SRV: RPTO on MASTER leg + SRV->>HS: OPTIONS via HBP path + SRV->>DB: clear modified +``` + 1. User saves options in the web UI → **monitor API** writes **`Clients.options`** and **`modified = 1`**. 2. The **hotspot proxy** runs **`send_opts`** on a loop (~every **10 s**). For rows with **`modified = 1`**, it reads options from the DB, sends **RPTO** (options) **to the peer server** at **`(MASTER, assigned_dest_port)`**, then clears **`modified`** in the DB. 3. The **ADN DMR Peer Server** receives **RPTO** on the MASTER leg and updates its **OPTIONS** / bridge state (same path as a normal hotspot registration refresh). diff --git a/docs/en/server/development/architecture.md b/docs/en/server/development/architecture.md index aa08227..087af46 100644 --- a/docs/en/server/development/architecture.md +++ b/docs/en/server/development/architecture.md @@ -8,6 +8,14 @@ **Dependency rule:** infrastructure → application → domain (inward only). +```mermaid +flowchart TD + INF["Infrastructure
Twisted UDP/TCP · YAML · voice
report_server · persistence · security"] + APP["Application
RoutingUseCases · VoiceUseCases
ports · SubscriptionStore · router"] + DOM["Domain
entities · value objects · errors · Result"] + INF --> APP --> DOM +``` + ## Entrypoint `main.py` wires configuration, **LoopingCall** timers, factories for **HBPProtocol**, report client, and injects use cases. @@ -16,6 +24,10 @@ Voice routing is driven by **`SubscriptionStore`** (domain subscriptions). `RoutingUseCases` orchestrates `dmrd_received` and delegates forward resolution to **`SubscriptionRouter`**. +Conceptual comparison with legacy **`BRIDGES`**: [BRIDGES vs Subscriptions](bridges-vs-subscriptions.md). + +Performance changes in 2.x (indexes, reporting, integrated proxy): [Performance (2.x)](performance.md). + - **`InMemoryAclRouter`** (`AclRouter` port) — ACL range checks only (`acl_check`). - **`routing_table_for_report()`** — export shim for monitor/report (legacy BRIDGE_SND shape); not used for runtime forwards. @@ -32,6 +44,7 @@ Wire opcodes and YAML keys may still say “bridge” for legacy monitor compati | Voice / TTS | `application/voice_use_cases.py`, `infrastructure/voice/` | | Hotspot proxy (fan-in) | `infrastructure/proxy/` (`udp_fanin.py`, `runtime.py`), `application/proxy/` use cases | | Self-service (MySQL) | `infrastructure/proxy/self_service_bridge.py`, `infrastructure/proxy/persistence/` | +| Dynamic TG persistence | `application/dynamic_tg_use_cases.py`, `infrastructure/persistence/dynamic_tg_repository.py`, `application/routing/dynamic_tg_restore.py` | ## Configuration as shared state diff --git a/docs/en/server/development/behaviour-and-timers.md b/docs/en/server/development/behaviour-and-timers.md index 182d359..16af023 100644 --- a/docs/en/server/development/behaviour-and-timers.md +++ b/docs/en/server/development/behaviour-and-timers.md @@ -20,6 +20,7 @@ The following intervals are part of the current runtime behavior: | `stream_trimmer` | **5s** | Stream cleanup, timeout handling, end-of-call state trimming. | | `bridge_reset` | **6s** | Bridge reset flag cleanup and pending reset completion. | | OPTIONS refresh | **event-driven** | Static TG / reflector from **RPTO**, **startup/reload** (`apply_startup_bridges`), **dmrd** no-source fallback. No periodic 26s loop (**D-28**). | +| `dynamic_tg_purge_loop` | **60s** | Purge expired **SINGLE=1** rows from `peer_dynamic_tgs` and in-memory `_PEER_UA_SESSIONS`. | | `statTrimmer` | **303s** | Trim stale STAT bridges and transient status entries. | If you change one of these intervals, document the operational impact for monitoring, loop behavior, and troubleshooting. diff --git a/docs/en/server/development/bridges-vs-subscriptions.md b/docs/en/server/development/bridges-vs-subscriptions.md new file mode 100644 index 0000000..8444a26 --- /dev/null +++ b/docs/en/server/development/bridges-vs-subscriptions.md @@ -0,0 +1,208 @@ +# 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). diff --git a/docs/en/server/development/performance.md b/docs/en/server/development/performance.md new file mode 100644 index 0000000..5dfdc50 --- /dev/null +++ b/docs/en/server/development/performance.md @@ -0,0 +1,99 @@ +# Performance (2.x) + +**adn-server 2.x** and **adn-monitor 2.x** include several changes that reduce CPU work and memory footprint compared with **adn-dmr-server** and the old monitor/proxy stack. This page lists **what** improves and **what causes it**. + +## At a glance + +| Area | Typical effect | Main cause | +|------|----------------|------------| +| **Voice downlink (inject proxy)** | Lower CPU under busy group traffic | **`PeerDownlinkIndex`** — fan-out to peers that match `(slot, TG)` instead of scanning every connected hotspot per packet | +| **Bridge source lookup** | Faster “am I the ACTIVE source?” | **`SubscriptionStore`** indexes (`relay_tables_with_active_source`) — O(1) by `(system, slot, tgid)` vs scanning table rows | +| **Background CPU** | Fewer wakeups | **Event-driven OPTIONS / static TG** — removed legacy **26 s** `options_config_loop` ([Behaviour and timers](behaviour-and-timers.md)) | +| **Mass peer login** | Less redundant CONFIG traffic | **`ConfigPushThrottle`** — adaptive debounce on CONFIG push to the monitor | +| **Reporting vs voice** | Voice path less blocked by reports | **`BoundedReportQueue`** — coalesced snapshots, bounded drain per tick | +| **Server → monitor wire** | Less serialize/send work | **Report v2** JSON (`routing_table`, `topology`, `voice_event`) instead of periodic full pickle of `CONFIG`/`BRIDGES` ([Report protocol v2](../protocols/report-v2.md)) | +| **Process count (RAM)** | One Python process instead of two | **Integrated `PROXY`** in `adn-server.py` — no separate **adn-proxy** process ([Hotspot proxy](../user-guide/hotspot-proxy.md)) | +| **Monitor RAM / WS load** | Smaller in-memory dashboard state | **Slim `dashboard_state` wire**, `clean_sys_dict`, lighter WebSocket fingerprints ([Monitor architecture](../../monitor/architecture.md)) | + +## Server: inject-only downlink index + +The largest **CPU** win on many ADN networks is on the **MASTER inject-only** path (`PROXY` with inject-only mode). + +**Legacy:** `send_peers` walks **every registered peer** for each downlink packet → cost grows as **O(peers × packets/s)**. + +**2.x:** `PeerDownlinkIndex` precomputes candidates from each peer’s **OPTIONS** (static TGs) and **UA session** state. For each group voice frame, only peers that **might** want that `(slot, TG)` are considered; each candidate still passes `peer_should_receive_group_voice`. + +```text +Legacy: every DMRD → try all N peers +2.x: every DMRD → index lookup → try k peers (k ≪ N on busy proxies) +``` + +OPTIONS parsing is **cached per peer** (`_CACHED_OPTIONS_STATIC`): if the OPTIONS blob is unchanged, already-parsed static TGs are reused instead of re-parsing on every packet. + +| Code | Role | +|------|------| +| `application/routing/peer_downlink_index.py` | Index build and `(slot, tgid) → candidates` | +| `infrastructure/twisted_adapters/udp_hbp.py` | `_iter_downlink_peers`, `send_peers` | +| `tests/infrastructure/test_peer_downlink_fanout.py` | Inject-only fan-out tests | + +**When it matters:** proxy with **tens to hundreds** of hotspots and steady group voice. On a small conference with few peers, the difference is minor. + +## Server: routing indexes + +On every group voice frame the server must find relay tables where **this system is the ACTIVE source**. + +**Legacy:** scan rows inside `BRIDGES[table_key]` (and related tables). + +**2.x:** `InMemorySubscriptionStore.relay_tables_with_active_source()` uses a maintained **`_source_tables`** index — lookup by `(system, slot, dst_tgid)` without walking all legs. + +This lives in the subscription store implementation; it is an **algorithmic index**, not a separate feature you configure. + +| Code | Role | +|------|------| +| `infrastructure/subscription_store.py` | `_source_tables`, `_by_table`, `_active_target_counts` | +| `application/subscription/router.py` | `SubscriptionRouter.resolve()` | + +## Server: less periodic and login-storm work + +| Change | What it avoids | +|--------|----------------| +| **No 26 s OPTIONS loop** | Timer firing every 26 s across all systems to refresh static bridges when RPTO/startup/reload already handle it | +| **`ConfigPushThrottle`** | Flooding the monitor with CONFIG snapshots when many peers connect within a few seconds (debounce widens from ~0.3 s to ~2 s during bursts) | +| **`BoundedReportQueue`** | Doing pickle/JSON encode and TCP send synchronously on the voice hot path; coalesces duplicate config/bridge snapshots | + +## Server: reporting and deployment + +- **Report v2** — structured JSON replaces opaque pickle snapshots for bridge/config state on the **2.x monitor** wire. See [Monitoring and reports](../user-guide/monitoring.md) and [Report protocol v2](../protocols/report-v2.md). +- **Integrated proxy** — `PROXY` runs **in-process**; dropping the standalone **adn-proxy** saves baseline **RAM** (one interpreter, shared config) and simplifies ops. + +## Monitor (adn-monitor 2.x) + +Pair **adn-server 2.x** with **adn-monitor 2.x** to get the reporting-side gains: + +| Change | Effect | +|--------|--------| +| **Slim wire / `dashboard_state`** | Monitor ingests compact JSON state instead of holding full duplicated pickle trees from v1 | +| **`clean_sys_dict`** | Periodic eviction of stale in-memory entries (caps runaway growth on long-lived panels) | +| **Last-heard row cache, lighter WS fingerprints** | Less work per dashboard refresh | +| **Unified FastAPI stack** | Removed separate PHP API and standalone monitor **proxy** process | + +Details: [Monitor architecture](../../monitor/architecture.md). + +## When you will notice a difference + +| Deployment | CPU | RAM | +|------------|-----|-----| +| Few masters, no inject proxy, light traffic | Small | Small | +| **Inject-only proxy, many hotspots, busy TG** | **Clear** (downlink index) | Moderate (single server process vs server+proxy) | +| Long-lived monitor + report v2 | Moderate (less serialize on wire) | **Clearer** on monitor (slim state, `clean_sys_dict`) | + +Crypto, AMBE, and OpenBridge MAC work still dominate on OpenBridge-heavy paths — routing-table optimizations do not remove that cost. + +## Related reading + +- [Architecture](architecture.md) — layers and entrypoint +- [BRIDGES vs Subscriptions](bridges-vs-subscriptions.md) — routing model (not a performance feature) +- [Behaviour and timers](behaviour-and-timers.md) — event-driven OPTIONS vs legacy 26 s loop +- [Hotspot proxy](../user-guide/hotspot-proxy.md) — integrated `PROXY` / inject-only +- [Report protocol v2](../protocols/report-v2.md) — JSON wire to monitor +- Release notes: `CHANGELOG.md` at the repository root (`Performance` under **2.0.0-rc.1**). diff --git a/docs/en/server/protocols/report-v2.md b/docs/en/server/protocols/report-v2.md index d78e3be..0200fb5 100644 --- a/docs/en/server/protocols/report-v2.md +++ b/docs/en/server/protocols/report-v2.md @@ -28,6 +28,22 @@ Replace monitor snapshots that today use **pickle** (`CONFIG_SND`, `BRIDGE_SND`) Proposed opcodes `0x10`–`0x13` are reserved in the schema phase; exact values may change before P1-002 ships. +```mermaid +flowchart TB + subgraph v1 [Report v1 — adn-server 1.0.x + monitor 1.0.x] + H1[HELLO protocol 1] --> C1[CONFIG_SND pickle] + C1 --> B1[BRIDGE_SND pickle] + B1 --> V1[BRDG_EVENT CSV] + end + + subgraph v2 [Report v2 — adn-server 2.x + monitor 2.x] + H2[HELLO report_protocol 2] --> T2[TOPOLOGY_SND JSON] + T2 --> R2[ROUTING_TABLE_SND JSON] + R2 --> V2[VOICE_EVENT_SND JSON] + R2 -.-> D2[DELTA_SND optional] + end +``` + ## Handshake (`hello`) On connect the server sends **`HELLO` (`0xFF`)** first (same as today). v2 clients inspect `report_protocol`: diff --git a/docs/en/server/user-guide/bridges-and-talkgroups.md b/docs/en/server/user-guide/bridges-and-talkgroups.md index cfbc2bb..5a05de1 100644 --- a/docs/en/server/user-guide/bridges-and-talkgroups.md +++ b/docs/en/server/user-guide/bridges-and-talkgroups.md @@ -12,11 +12,37 @@ The bridge table maps **talkgroup keys** (strings, e.g. `"26811"`, `"#reflector" The router scans `BRIDGES` for an **ACTIVE** row matching the **current source system**, **slot**, and **destination TG** before forwarding (`dmrd_received` → `to_target`). +In **adn-server 2.x** the same rules live in **`SubscriptionStore`** / **`SubscriptionRouter`**; `BRIDGES` is only an export shape for the monitor. See [BRIDGES vs Subscriptions](../development/bridges-vs-subscriptions.md). + ## Dynamic vs static - **User-activated** bridges are created when a user keys a TG without a pre-built row (subject to `DEFAULT_UA_TIMER` and options). - **Static** TGs and **STAT** bridges are created from **OPTIONS** / `make_static_tg` / `GEN_STAT_BRIDGES` flows. +## Dynamic TG persistence (MariaDB) {#dynamic-tg-persistence-mariadb} + +Since **2.0.0-rc.3**, user-activated dynamic TGs for each hotspot can be **persisted in MariaDB** (`peer_dynamic_tgs`) so they survive **hotspot disconnect/reconnect** without re-keying the TG. + +| Event | Server behaviour | +|-------|------------------| +| **Group voice header** (new dynamic TG on a slot) | Registers UA session in memory and **async upsert** to `peer_dynamic_tgs`. | +| **RPTC** (hotspot login OK) | **Restores** rows for that peer/system into memory and re-syncs bridge rows (`ensure_dynamic_relay`). | +| **TG 4000** | Clears **all** dynamic slots for that peer (memory + DB). See [Special numbers — TG 4000](special-numbers.md#tg--id-4000--deactivate-dynamic-bridges). | +| **Hotspot disconnect** | Clears per-peer **mirror** state only; persisted rows and global `_PEER_UA_*` maps are kept until expiry or TG 4000. | +| **Periodic purge** | Every **60 s**, expired **SINGLE=1** rows are removed from DB and memory. | + +**SINGLE=0** peers accumulate several dynamic TGs per slot in memory (`_PEER_UA_MULTI_TGS`). **SINGLE=1** stores one exclusive TG per slot with a timer. + +**TG 4000** is never stored as a dynamic session (reset command only). + +Requires **`DATABASE`** in `adn-server.yaml` — see [Configuration](configuration.md#database-mariadb). + +## Cross-slot static TG downlink (inject-only) + +On **inject-only** MASTER systems (integrated **`PROXY`**), group voice downlink respects **static TGs listed in either TS1 or TS2 OPTIONS**, even when the **wire timeslot** differs. This matches legacy REPEAT behaviour for hotspots that list a TG on one slot but transmit on another. + +The server does **not** rewrite the incoming DMRD slot; it filters **which peers receive** the repeated packet via `peer_should_receive_group_voice` and the downlink index. + ## Source-row guard and safe iteration Forwarding is allowed only when the current system has a matching **ACTIVE source row** for that TG/slot context. This prevents accidental forwarding from rows that are present but not currently eligible as source legs. diff --git a/docs/en/server/user-guide/configuration.md b/docs/en/server/user-guide/configuration.md index 143fc8a..728b7c8 100644 --- a/docs/en/server/user-guide/configuration.md +++ b/docs/en/server/user-guide/configuration.md @@ -29,9 +29,9 @@ kill -HUP $(pidof adn-server.py) # or: systemctl reload adn-server Example unit: **`examples/systemd/adn-server.service`** (copy to `/etc/systemd/system/`; includes `ExecReload` for `systemctl reload`). -**Reload applies:** `GLOBAL`, `REPORTS`, `ALIASES`, **`LOGGER.LOG_LEVEL`** (without process restart), **`PROXY`** (timeouts, debug, block lists — not bind or target), **`SELF_SERVICE`** (merged; enabling/disabling DB loops needs restart), per-system settings, **new/removed SYSTEMS** (including `GENERATOR` expansion/collapse and new OpenBridge legs), and updated bind addresses (listener restart for that system only). +**Reload applies:** `GLOBAL`, `REPORTS`, `ALIASES`, **`LOGGER.LOG_LEVEL`** (without process restart), **`PROXY`** (timeouts, debug, block lists — not bind or target), **`SELF_SERVICE`** (PBKDF2 flags merged; enabling/disabling DB loops needs restart), per-system settings, **new/removed SYSTEMS** (including `GENERATOR` expansion/collapse and new OpenBridge legs), and updated bind addresses (listener restart for that system only). -**Not reloaded:** `adn-voice.yaml` (separate 15 s loop), Python code, subscriber alias files (separate periodic reload). **BRIDGES** table is not rebuilt on reload — restart if bridge rules changed in a way that requires a full reset. **`PROXY.LISTEN_PORT`**, **`LISTEN_IP`**, and **`TARGET_SYSTEM`** require a **full restart** to take effect. +**Not reloaded:** **`DATABASE`** (MariaDB pool and `peer_dynamic_tgs` bootstrap), `adn-voice.yaml` (separate 15 s loop), Python code, subscriber alias files (separate periodic reload). **BRIDGES** table is not rebuilt on reload — restart if bridge rules changed in a way that requires a full reset. **`PROXY.LISTEN_PORT`**, **`LISTEN_IP`**, and **`TARGET_SYSTEM`** require a **full restart** to take effect. **Secrets:** Never commit real passphrases, security URLs, or `user_passwords.json` / `encryption_key.secret`. Use placeholders in templates and keep production files local. @@ -169,7 +169,7 @@ For the conceptual model (ACTIVE, TS, TGID, timeouts): [Bridges and talkgroups]( ## `REPORTS` -TCP report channel for **adn-monitor** (or compatible dashboards). +TCP report channel for **adn-monitor** (or compatible dashboards). **adn-server 2.x** speaks **report v2** only; legacy dashboards that expect **v1** need the optional [report-proxy](report-proxy.md) (separate package). | Key | Meaning | |-----|---------| @@ -178,7 +178,7 @@ TCP report channel for **adn-monitor** (or compatible dashboards). | **REPORT_PORT** | Local port the **server listens on** for report clients. | | **REPORT_CLIENTS** | Comma-separated or list of allowed client IPs (see example). | -Details: [Monitoring and reports](monitoring.md). +Details: [Monitoring and reports](monitoring.md). Legacy v1 monitors: [Report proxy](report-proxy.md). --- @@ -198,9 +198,38 @@ Do **not** run standalone **`adn-proxy`** on the same **`LISTEN_PORT`** when the --- +## `DATABASE` (MariaDB) + +**Required** for typical conference-server configs: any deployment with **`PROXY`**, or at least one **`MASTER`** / **`OPENBRIDGE`** system. **Not** required for minimal **echo-only** PEER fleets (`adn-server.py --echo`). + +| Key | Meaning | +|-----|---------| +| **DB_SERVER** | MariaDB/MySQL host. | +| **DB_USERNAME** / **DB_PASSWORD** | Credentials. | +| **DB_NAME** | Database name (often the same as **adn-monitor**, e.g. `hbmon`). | +| **DB_PORT** | TCP port (default **3306**). | + +**Uses one shared connection pool** for: + +- **Dynamic TG persistence** — table **`peer_dynamic_tgs`** (per-peer user-activated TGs across hotspot reconnects). The server **creates the table on startup** if missing (migration id **`004_peer_dynamic_tgs`**, same schema as adn-monitor). +- **Integrated self-service** — table **`Clients`** when **`SELF_SERVICE.USE_SELFSERVICE: true`**. + +Startup aborts with a clear log if MariaDB is unreachable or **`DATABASE`** is incomplete. Install **`mysqlclient`** (`pip install -e ".[selfservice]"` includes it). + +**Hot reload:** changing **`DATABASE`** requires a **full process restart**. + +Details: [Bridges and talkgroups — dynamic TG persistence](bridges-and-talkgroups.md#dynamic-tg-persistence-mariadb). + +--- + ## `SELF_SERVICE` (MySQL / dashboard options) -Optional; requires `pip install -e ".[selfservice]"` when **`USE_SELFSERVICE: true`**. Uses the same **`Clients`** table and PBKDF2 parameters as **adn-monitor**. Keys match the monitor docs — see [Self-service](../../monitor/self-service.md) and [Hotspot proxy](hotspot-proxy.md#self_service-keys). +Optional; requires `pip install -e ".[selfservice]"` when **`USE_SELFSERVICE: true`**. Uses the **`DATABASE`** block above (not separate DB keys in **`SELF_SERVICE`**). PBKDF2 parameters must **match** **adn-monitor**. See [Self-service](../../monitor/self-service.md) and [Hotspot proxy](hotspot-proxy.md#self_service-keys). + +| Key | Meaning | +|-----|---------| +| **USE_SELFSERVICE** | Enable MySQL-backed options sync from the dashboard (`true` / `false`). | +| **PBKDF2_SALT** / **PBKDF2_ITERATIONS** | Must match **`adn-monitor.yaml`** / password tooling. | --- diff --git a/docs/en/server/user-guide/hotspot-proxy.md b/docs/en/server/user-guide/hotspot-proxy.md index 0acc149..3650697 100644 --- a/docs/en/server/user-guide/hotspot-proxy.md +++ b/docs/en/server/user-guide/hotspot-proxy.md @@ -14,6 +14,13 @@ Configuration lives in **`adn-server.yaml`** under **`PROXY`** and optional **`S The integrated proxy uses **fan-in**: hotspots only need **`PROXY.LISTEN_PORT`** (e.g. **62031**). The target **MASTER** is **inject-only** — it does **not** bind its own UDP port for that system (no per-hotspot port range on the server host). +```mermaid +flowchart LR + HS1[Hotspot A] -->|UDP HBP| LP[PROXY LISTEN_PORT] + HS2[Hotspot B] -->|UDP HBP| LP + LP -->|inject| MASTER[TARGET_SYSTEM MASTER] +``` + --- ## Optional dependency (self-service) @@ -72,9 +79,10 @@ Same semantics as **`adn-monitor.yaml`** — shared **`Clients`** table, **`modi | Key | Role | |-----|------| | **USE_SELFSERVICE** | Enable MySQL-backed options sync (`true` / `false`). | -| **DB_SERVER**, **DB_USERNAME**, **DB_PASSWORD**, **DB_NAME**, **DB_PORT** | MySQL connection. | | **PBKDF2_SALT**, **PBKDF2_ITERATIONS** | Must **match** monitor/backend for password hashing. | +MariaDB connection settings live in the top-level **`DATABASE`** block (shared with dynamic TG persistence) — see [Configuration](configuration.md#database-mariadb). + On startup the server logs **`(SELF_SERVICE) Database connection test: OK`** and **`(SELF_SERVICE) Enabled`** when the pool connects. Self-service runs **asynchronously**; voice forwarding is not blocked on DB latency. Details of the dashboard flow: [Self-service](../../monitor/self-service.md). diff --git a/docs/en/server/user-guide/introduction.md b/docs/en/server/user-guide/introduction.md index 4fa25f5..7a49696 100644 --- a/docs/en/server/user-guide/introduction.md +++ b/docs/en/server/user-guide/introduction.md @@ -17,8 +17,8 @@ Routing, timers, OpenBridge loop control, and protocol handling are implemented | Subsystem | Role | |-----------|------| -| **Bridge router** | `BRIDGES` table: which systems forward which TG on which slot; dynamic bridges; static/stat bridges. | -| **HBP protocol** | Authentication, DMRD ingress/egress, repeat to peers, TG filters. | +| **Bridge router** | `BRIDGES` table: which systems forward which TG on which slot; dynamic bridges; static/stat bridges; **MariaDB dynamic TG restore** on reconnect. | +| **HBP protocol** | Authentication, DMRD ingress/egress, repeat to peers, TG filters, **per-peer UA session** tracking. | | **OpenBridge** | DMRE ingress, hop limit, loop control (`min(1ST)`), BCSQ/BCKA when enabled. | | **Voice** | AMBE files, scheduled announcements, TTS pipeline, on-demand playback (TG 9991–9999). | | **Reporting** | TCP netstring channel to **adn-monitor** (and compatible dashboards): config, bridge state, call events (report v2 JSON). | @@ -28,12 +28,14 @@ Routing, timers, OpenBridge loop control, and protocol handling are implemented - **Echo / playback** — `adn-server.py --echo` with minimal `adn-echo.yaml`; see [Echo](echo.md). - **Integrated hotspot proxy** — `PROXY` in **`adn-server.yaml`**; see [Hotspot proxy](hotspot-proxy.md). +- **Report proxy (legacy dashboards)** — optional **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** so **adn-server 2.x** can feed old HBMonitor / FDMR-style monitors (v1 wire); see [Report proxy](report-proxy.md). Not used with **adn-monitor 2.x**. ## Next steps -- [Configuration](configuration.md) — files, `GLOBAL`, **MASTER** / **PEER** / **OPENBRIDGE**, ACLs, reports, **`PROXY`**, **`SELF_SERVICE`**, aliases, voice merge. +- [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. - [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). +- [Performance (2.x)](../development/performance.md) — CPU/RAM improvements in this release and what causes them. - [Credits & license](attribution.md) — ADN → FreeDMR → hblink3, license. diff --git a/docs/en/server/user-guide/monitoring.md b/docs/en/server/user-guide/monitoring.md index fa7390f..696d6bd 100644 --- a/docs/en/server/user-guide/monitoring.md +++ b/docs/en/server/user-guide/monitoring.md @@ -12,12 +12,26 @@ When **`REPORTS`** is enabled in the server config, the **ADN DMR Peer Server** **Version pairing:** **server 1.0.x + monitor 1.0.x** = report v1 (frozen tags). **server 2.x** emits **report v2 only** — requires **monitor 2.x** on the same line. No `dual` wire; monitor 1.0.x will not decode this server. +### Legacy dashboards (report-proxy) + +If you keep an **old dashboard** whose backend monitor speaks **report v1** only (pickle/CSV, no HELLO v2), it **cannot** connect to **adn-server 2.x** on `REPORTS.REPORT_PORT`. Use the optional **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** to translate **v2 → v1**: the proxy connects to the server; the legacy monitor connects to the proxy. **adn-monitor 2.x** does **not** need this proxy — connect it directly to the server. + +See [Report proxy (legacy dashboards)](report-proxy.md) for topology, `REPORT_CLIENTS`, ports, and start order. + Older stacks (**legacy** `adn-dmr-server`-style) may **omit** HELLO. **adn-monitor** waits up to **`ADN_CONNECTION.HELLO_TIMEOUT_MS`** (see [Monitor configuration](../../monitor/configuration.md#adn_connection)); if no HELLO arrives, it assumes **legacy** reporting. The **monitor** decodes these messages, updates its **CTABLE** / **BTABLE**, and (when MySQL is configured) persists Last Heard / statistics. **Full stack:** [ADN Monitor overview](../../monitor/index.md) (FastAPI monitor, WebSocket, self-service). +```mermaid +flowchart LR + SRV[adn-server\nREPORTS.REPORT_PORT] -->|TCP netstring| ING[adn-monitor ingest] + ING --> STATE[CTABLE / BTABLE] + STATE --> WS[WebSocket /ws] + WS --> UI[React dashboard] +``` + ### Report channel log lines (`adn-monitor` logger) Python uses the logger name **`adn-monitor`** (see **`LOGGER.LOG_FILE`** in `adn-monitor.yaml`). Typical **INFO** lines for the TCP report client: @@ -40,6 +54,19 @@ At **WARNING**: invalid HELLO JSON (`(REPORT) HELLO payload not valid JSON`), or The dashboard shows **operational** state from **START** (canonical); the **Monitor** log shows **INGRESS** plus **START** for troubleshooting mesh duplicates. +### Dynamic UA chips (hotspot OPTIONS) + +The monitor tracks **user-activated** TGs per hotspot for dashboard indigo chips: + +| Peer OPTIONS | Monitor source | +|--------------|----------------| +| **SINGLE=1** | **`UA_SESSIONS`** in **CONFIG_SND** / `dashboard_state` (server source of truth). | +| **SINGLE=0** | Voice events (`BRDG_EVENT` / `voice_event`) — multiple dynamics per slot until cleared. | + +**TG 4000** clears UA state via **`GROUP VOICE,INGRESS,RX`** with destination **4000** (server sends this because the voice path returns early and never emits a normal **START**). The monitor must **not** register **4000** as a dynamic TG. + +**Version pairing:** **adn-server 2.0.0-rc.3** + **adn-monitor 2.0.0-rc.4** for dynamic TG persistence and TG 4000 monitor sync. + ## Log file rotation (logrotate) After **logrotate** renames or moves a log file (common pattern: **`create`** so the old path is rotated away and a **new empty file** appears at the configured path), the process may still hold an open file descriptor on the **previous inode**. Logs then appear “missing” from the current path until the process **reopens** its file handlers. diff --git a/docs/en/server/user-guide/report-proxy.md b/docs/en/server/user-guide/report-proxy.md new file mode 100644 index 0000000..2a7a403 --- /dev/null +++ b/docs/en/server/user-guide/report-proxy.md @@ -0,0 +1,100 @@ +# Report proxy (legacy dashboards) + +**ADN DMR Peer Server 2.x** emits **report wire v2** (JSON over TCP). **adn-monitor 2.x** understands that protocol and connects **directly** to the server — no extra component is required. + +Some **legacy dashboard stacks** still ship their own `dashboard.py` / `monitor.py` backend and speak **report wire v1** only (pickled `CONFIG_SND` / `BRIDGE_SND`, CSV `BRDG_EVENT`). Those monitors **cannot** connect to **adn-server 2.x** on the report port. + +The optional **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** package sits between the two: it connects **upstream** to the real server (v2), listens **downstream** where the legacy monitor expects the server (v1), and translates **v2 → v1**. + +| Stack | Upstream server | Works without proxy? | +|-------|-----------------|----------------------| +| **adn-monitor 2.x** (React) | **adn-server 2.x** | Yes — connect to `REPORTS.REPORT_PORT` | +| Legacy dashboard + bundled monitor (v1) | **adn-dmr-server** (v1) | Yes — direct to server report port | +| Legacy dashboard + bundled monitor (v1) | **adn-server 2.x** (v2) | **No** — use **report-proxy** | + +Typical legacy targets: old **ADN-Dashboard** forks, **HBMonitor** / **FDMR Monitor** deployments that still run a Python monitor process against `dashboard.cfg` / `monitor.cfg`. + +## Topology + +```text +┌───────────────────┐ +│ adn-server │ +│ LISTENS :4321 │ +└─────────▲─────────┘ + │ + │ TCP v2 JSON + │ (report-proxy is CLIENT) + │ +┌─────────┴─────────┐ +│ report-proxy │ +│ LISTENS :4322 │ +└─────────▲─────────┘ + │ + │ TCP v1 pickle + │ (legacy dashboard is CLIENT) + │ +┌─────────┴─────────┐ +│ legacy dashboard │ +│ monitor.py │ +└───────────────────┘ +``` + +| Component | Role | Default port | Config | Key setting | +|-----------|------|--------------|--------|-------------| +| **adn-server** | Listens for report clients | **4321** | `adn-server.yaml` | `REPORTS.REPORT_PORT` | +| **report-proxy** | Connects to the server | 4321 | `report-proxy.yaml` | `UPSTREAM.PORT` | +| **report-proxy** | Listens for the legacy monitor | **4322** | `report-proxy.yaml` | `LISTEN.PORT` | +| **Legacy dashboard** | Connects to the proxy | **4322** | `dashboard.cfg` | `SERVER_PORT` | + +**Do not** point the legacy dashboard at **4321** — that is the server’s v2 port. + +**Do not** set `UPSTREAM.PORT` to **4322** — that is the proxy’s own listen port. + +## Server side (`adn-server.yaml`) + +Reporting must be enabled and the **proxy host IP** must be in the allow list: + +```yaml +REPORTS: + REPORT: true + REPORT_INTERVAL: 60 + REPORT_PORT: 4321 + REPORT_CLIENTS: "127.0.0.1" # IP of the machine running report-proxy +``` + +If the proxy runs on another host, use **that host’s IP** in `REPORT_CLIENTS`, not only `127.0.0.1`. See [Configuration](configuration.md#reports) for all `REPORTS` keys. + +## Proxy and legacy dashboard + +Install and run the proxy from the **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** repository (`report-proxy.yaml`, `python3 report-proxy.py -c report-proxy.yaml`). Point `UPSTREAM` at the server’s `REPORT_PORT` and `LISTEN` at the port the legacy monitor uses (often **4322**). + +In legacy `dashboard.cfg` / `monitor.cfg`: + +```ini +[SERVER CONNECTION] +SERVER_IP = 127.0.0.1 +SERVER_PORT = 4322 +``` + +`SERVER_IP` is the host where **report-proxy** listens, not necessarily the adn-server host. + +**Start order:** adn-server → report-proxy → legacy monitor backend. + +Full step-by-step, multi-host examples, verification checks, and common mistakes: **[ADN-report-proxy README](https://github.com/ce5rpy/ADN-report-proxy#configuration-legacy-dashboard--adn-server-2x)**. + +## Wire translation (summary) + +| Upstream (v2 from adn-server) | Downstream (v1 to legacy monitor) | +|--------------------------------|-----------------------------------| +| `HELLO` (`report_protocol: 2`) | `HELLO` (`protocol: 1`) | +| `STATE_SND` / `dashboard_state` | `CONFIG_SND` (pickle) | +| `ROUTING_TABLE_SND` | `BRIDGE_SND` (pickle) | +| `TOPOLOGY_SND` | `CONFIG_SND` (pickle) | +| `VOICE_EVENT_SND` | `BRDG_EVENT` (CSV) | + +Schema detail for v2: [Report protocol v2 (JSON)](../protocols/report-v2.md). + +## See also + +- [Monitoring and reports](monitoring.md) — report channel, **adn-monitor** pairing, log lines. +- [ADN Monitor overview](../../monitor/index.md) — preferred dashboard for **adn-server 2.x** (no proxy). diff --git a/docs/en/server/user-guide/special-numbers.md b/docs/en/server/user-guide/special-numbers.md index 3f70ec1..f195bb9 100644 --- a/docs/en/server/user-guide/special-numbers.md +++ b/docs/en/server/user-guide/special-numbers.md @@ -56,19 +56,24 @@ In-band bridge activation/deactivation is applied on **voice terminator (VTERM)* - For reflector bridges (`#...`), in-band handling is evaluated only when the destination is **TG 9**. - This is why reflector prompts and dial wiring are tied to TG 9 while private calls do not trigger that bridge-timer logic. -## TG / ID 4000 — deactivate dynamic bridges +## TG / ID 4000 — deactivate dynamic bridges {#tg--id-4000--deactivate-dynamic-bridges} -**Purpose:** Clear **user-activated (dynamic) bridges** for the system that receives the call. +**Purpose:** Clear **user-activated (dynamic)** state for the hotspot that keys **4000**. **TG 4000 is not** a talkgroup to monitor or persist — it is a **reset command**. -**Behaviour:** +**Behaviour (group voice header):** + +- Clears per-peer UA sessions in memory (**all slots** for that peer). +- Deletes matching rows from **`peer_dynamic_tgs`** (MariaDB). +- Clears stale **STATUS** RX fields so a later **RPTO** does not re-seed the old TG. +- Runs **in-band bridge deactivation** on the slot (same as legacy). +- Sends **`GROUP VOICE,INGRESS,RX,…,4000`** to the monitor (not **START**) so **SINGLE=0** multi-dynamic chips clear **without** lighting a live TX chip. +- On **inject-only** MASTER, pushes updated **CONFIG_SND** to the monitor. -- Implemented for **group** traffic to destination **4000** (and related checks in the router). -- Runs **before** normal TG ACL in the OpenBridge path so the command is not blocked by allow lists. -- Invokes **`deactivate_all_dynamic_bridges`**: deactivates non-stat, non-reflector dynamic bridge rows. +**Inject-only vs global:** With integrated **`PROXY`**, reset is **per peer** (only that hotspot’s dynamics). Without inject-only filtering, legacy **`deactivate_all_dynamic_bridges`** still runs for the whole system. -Use this when operators need to **reset** dynamic routing without restarting the server. +**TG 4000 must never appear** as a dynamic UA chip on the monitor or in `peer_dynamic_tgs`. -### `SINGLE_MODE` impact on deactivation logic +### `SINGLE_MODE` impact on in-band deactivation When in-band rules evaluate deactivation on a MASTER slot: @@ -93,6 +98,8 @@ 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). + **Note:** A **standalone echo** is also available as a separate process — [Echo](echo.md). ## Private call to ID 4000 diff --git a/docs/es/README.md b/docs/es/README.md index aec91d2..8e1b060 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -23,6 +23,7 @@ El **ADN DMR Peer Server** es un puente de conferencia [GPL-3.0](https://www.gnu | TG 4000, 999x, eco | [Números especiales](server/user-guide/special-numbers.md) | | Llamadas privadas | [Llamadas privadas](server/user-guide/private-calls.md) | | Voz / TTS | [Voz, anuncios y TTS](server/user-guide/voice-and-tts.md) | +| Panel legacy + servidor 2.x | [Proxy de informes](server/user-guide/report-proxy.md) | | OpenBridge / DMRE | [OpenBridge](server/protocols/openbridge.md), [DMRE v5](server/protocols/dmre-v5.md) | | HBP | [HBP](server/protocols/hbp.md) | | Código | [Arquitectura](server/development/architecture.md), [Comportamiento y temporizadores](server/development/behaviour-and-timers.md) | @@ -33,6 +34,7 @@ El **ADN DMR Peer Server** es un puente de conferencia [GPL-3.0](https://www.gnu ```bash pip install -r requirements.txt cp adn-server.example.yaml adn-server.yaml +# Edita DATABASE (MariaDB) y secretos antes de producción python adn-server.py -c adn-server.yaml ``` diff --git a/docs/es/monitor/architecture.md b/docs/es/monitor/architecture.md index f72758c..dfef025 100644 --- a/docs/es/monitor/architecture.md +++ b/docs/es/monitor/architecture.md @@ -10,6 +10,14 @@ Bajo `monitor/src/adn_monitor/`: El **composition root** está en `infrastructure/fastapi/composition.py` (`build_monitor_api`). +```mermaid +flowchart TD + INF["Infraestructura
YAML · FastAPI /ws · ingest TCP/MQTT
MySQL · decodificadores pickle/JSON"] + APP["Aplicación
MonitorState · informes/dashboard
auth · self-service · alias"] + DOM["Dominio
objetos de valor · errores · opcodes
UserSession · Result"] + INF --> APP --> DOM +``` + ## Proceso unificado (`monitor.py`) Un solo proceso uvicorn/FastAPI: @@ -39,15 +47,19 @@ El proceso **`adn-proxy`** independiente se eliminó del repositorio **adn-monit ## Topología típica -```text -[Hotspots] --UDP--> [adn-server PROXY] --UDP--> [peer MASTER] - | - v - MySQL (Clients) +```mermaid +flowchart TD + HS[Hotspots] -->|UDP HBP| PROXY[adn-server PROXY] + PROXY --> MASTER[MASTER inyectado] + PROXY --> DB[(MySQL Clients)] -[Peer :REPORT_PORT] <--- TCP o MQTT --- [monitor.py ingest] + MASTER --> REPORT[REPORTS listener] + REPORT <-->|TCP o MQTT| ING[monitor.py ingest] + ING --> APP[FastAPI /api /ws] + APP --> DB -[Navegador] --HTTPS--> [Nginx: frontend/dist + proxy /api,/ws --> :8080] + BR[Navegador] -->|HTTPS| FE[Nginx o SERVE_STATIC] + FE --> APP ``` --- diff --git a/docs/es/monitor/configuration.md b/docs/es/monitor/configuration.md index 8e76741..55906ed 100644 --- a/docs/es/monitor/configuration.md +++ b/docs/es/monitor/configuration.md @@ -120,7 +120,9 @@ No hay Alembic: el monitor usa **`schema_migrations`** y comprobaciones en **`in - **Replace:** carga en `{tabla}_import` con **commit cada 10 000 filas** (la tabla live sigue legible); swap atómico `RENAME TABLE` (bloqueo metadata breve). - **Merge** (ficheros locales): `INSERT IGNORE` con **commit cada 2 000 filas**. -Migraciones: `001_clients_callsign`, `002_clients_options_width`, `003_alias_pk_only`. +Migraciones: `001_clients_callsign`, `002_clients_options_width`, `003_alias_pk_only`, **`004_peer_dynamic_tgs`** (tabla compartida con **adn-server 2.0.0-rc.3+**). + +**adn-server** también asegura **`peer_dynamic_tgs`** al arrancar (idempotente). Cualquiera de los dos caminos basta; ambos pueden usar la misma base **`hbmon`**. --- diff --git a/docs/es/monitor/index.md b/docs/es/monitor/index.md index 42f9dd1..a989b1f 100644 --- a/docs/es/monitor/index.md +++ b/docs/es/monitor/index.md @@ -18,7 +18,9 @@ Este capítulo documenta la pila **adn-monitor** con el mismo nivel de detalle q | **`adn-server.yaml`** | **`adn-server.py`** (**`PROXY`** / **`SELF_SERVICE`** integrados) | `-c` / ruta por defecto junto al binario | | **`monitor/adn-monitor.yaml`** | **`monitor.py`** | **`ADN_CONFIG_PATH`** | -**`SELF_SERVICE`** (MySQL / PBKDF2) debe **coincidir** entre **`adn-server.yaml`** y **`adn-monitor.yaml`**. **`ADN_CONNECTION`**, panel, WebSocket y alias van en **`adn-monitor.yaml`**; **`PROXY`** integrado va en **`adn-server.yaml`** — ver [Proxy hotspot integrado](../server/user-guide/hotspot-proxy.md). +**`SELF_SERVICE`** (MySQL / PBKDF2) debe **coincidir** entre **`adn-server.yaml`** y **`adn-monitor.yaml`**. En el servidor, las credenciales MariaDB van en **`DATABASE`** (pool compartido con **`peer_dynamic_tgs`**). **`ADN_CONNECTION`**, panel, WebSocket y alias van en **`adn-monitor.yaml`**; **`PROXY`** integrado va en **`adn-server.yaml`** — ver [Proxy hotspot integrado](../server/user-guide/hotspot-proxy.md). + +**Emparejamiento recomendado:** **adn-server 2.0.0-rc.3** + **adn-monitor 2.0.0-rc.4** (persistencia TG dinámicos, sincronización TG 4000 en monitor). ## Enlace con el peer server diff --git a/docs/es/monitor/self-service.md b/docs/es/monitor/self-service.md index 96a8227..37b02fd 100644 --- a/docs/es/monitor/self-service.md +++ b/docs/es/monitor/self-service.md @@ -41,6 +41,23 @@ La sesión se prolonga con actividad (**SelfServiceController** usa un timeout l ## Flujo extremo a extremo (cómo llegan las opciones al hotspot) +```mermaid +sequenceDiagram + participant UI as React /self-service + participant API as monitor FastAPI + participant DB as MySQL Clients + participant SRV as adn-server PROXY + participant HS as Hotspot + + UI->>API: POST /api/self-service/device/options + API->>DB: UPDATE options, modified = 1 + Note over SRV: bucle send_opts ~cada 10 s + SRV->>DB: lee filas con modified = 1 + SRV->>SRV: RPTO en pata MASTER + SRV->>HS: OPTIONS vía HBP + SRV->>DB: limpia modified +``` + 1. El usuario guarda opciones en la web → la **API del monitor** escribe **`Clients.options`** y **`modified = 1`**. 2. El **proxy hotspot** ejecuta **`send_opts`** en bucle (~cada **10 s**). Para filas con **`modified = 1`**, lee opciones de la BD, envía **RPTO** **al peer server** en **`(MASTER, puerto_destino_asignado)`**, luego limpia **`modified`** en la BD. 3. El **ADN DMR Peer Server** recibe **RPTO** en la pata MASTER y actualiza su estado **OPTIONS** / bridge (mismo camino que un refresco normal de registro del hotspot). diff --git a/docs/es/server/development/architecture.md b/docs/es/server/development/architecture.md index 6190812..1db3780 100644 --- a/docs/es/server/development/architecture.md +++ b/docs/es/server/development/architecture.md @@ -8,6 +8,14 @@ **Regla de dependencias:** infraestructura → aplicación → dominio (solo hacia dentro). +```mermaid +flowchart TD + INF["Infraestructura
Twisted UDP/TCP · YAML · voz
report_server · persistencia · seguridad"] + APP["Aplicación
RoutingUseCases · VoiceUseCases
ports · SubscriptionStore · router"] + DOM["Dominio
entidades · value objects · errors · Result"] + INF --> APP --> DOM +``` + ## Punto de entrada `main.py` cablea configuración, temporizadores **LoopingCall**, fábricas para **HBPProtocol**, servidor de informes e inyecta casos de uso. @@ -16,6 +24,10 @@ El enrutado de voz lo gobierna **`SubscriptionStore`** (suscripciones de dominio). `RoutingUseCases` orquesta `dmrd_received` y delega la resolución de reenvío a **`SubscriptionRouter`**. +Comparación conceptual con **`BRIDGES`** legacy: [BRIDGES vs Subscriptions](bridges-vs-subscriptions.md). + +Cambios de rendimiento en 2.x (índices, informes, proxy integrado): [Rendimiento (2.x)](performance.md). + - **`InMemoryAclRouter`** (port `AclRouter`) — solo comprobaciones ACL (`acl_check`). - **`routing_table_for_report()`** — shim de exportación para monitor/informes (forma legacy BRIDGE_SND); no se usa para reenvíos en runtime. @@ -32,6 +44,7 @@ Los opcodes wire y claves YAML pueden seguir diciendo “bridge” por compatibi | Voz / TTS | `application/voice_use_cases.py`, `infrastructure/voice/` | | Proxy hotspot (fan-in) | `infrastructure/proxy/` (`udp_fanin.py`, `runtime.py`), casos de uso en `application/proxy/` | | Self-service (MySQL) | `infrastructure/proxy/self_service_bridge.py`, `infrastructure/proxy/persistence/` | +| Persistencia TG dinámicos | `application/dynamic_tg_use_cases.py`, `infrastructure/persistence/dynamic_tg_repository.py`, `application/routing/dynamic_tg_restore.py` | ## Configuración como estado compartido diff --git a/docs/es/server/development/behaviour-and-timers.md b/docs/es/server/development/behaviour-and-timers.md index b303041..314098c 100644 --- a/docs/es/server/development/behaviour-and-timers.md +++ b/docs/es/server/development/behaviour-and-timers.md @@ -20,6 +20,7 @@ Los siguientes intervalos forman parte del comportamiento actual en ejecución: | `stream_trimmer` | **5s** | Limpieza de streams, manejo de timeout y cierre de estado de llamada. | | `bridge_reset` | **6s** | Limpieza de flags de reset y cierre de resets pendientes. | | OPTIONS refresh | **por evento** | TG estáticas / reflector vía **RPTO**, **startup/reload** (`apply_startup_bridges`), fallback **dmrd** sin source. Sin loop periódico de 26s (**D-28**). | +| `dynamic_tg_purge_loop` | **60s** | Purga filas **SINGLE=1** expiradas de `peer_dynamic_tgs` y `_PEER_UA_SESSIONS` en memoria. | | `statTrimmer` | **303s** | Limpieza de bridges STAT obsoletos y estados transitorios. | Si cambias uno de estos intervalos, documenta el impacto operativo en monitorización, comportamiento de bucles y troubleshooting. diff --git a/docs/es/server/development/bridges-vs-subscriptions.md b/docs/es/server/development/bridges-vs-subscriptions.md new file mode 100644 index 0000000..4022fc1 --- /dev/null +++ b/docs/es/server/development/bridges-vs-subscriptions.md @@ -0,0 +1,208 @@ +# BRIDGES (legacy) vs Subscriptions (servidor nuevo) + +**adn-dmr-server** y **adn-server 2.x** reenvían voz de grupo igual en el wire: una “tabla de bridge” por talkgroup decide qué **systems** reciben copia del stream, con **reescritura LC** por pata. Lo que cambia en 2.x es **cómo se representa esa tabla en código** — no las reglas visibles para el operador. + +## Para el operador + +En runtime el comportamiento de bridges es el mismo: TG, slots, OPTIONS, UA, TG 4000, OpenBridge. **Subscriptions** es el nombre interno del motor de enrutado en 2.x; no es un modo de operación distinto ni algo que configures aparte. + +**Sin cambios operativos** + +- Configuración: **`SYSTEMS`**, OPTIONS del hotspot, **`SELF_SERVICE`** / MariaDB — igual que con **adn-dmr-server**. No hay bloque `BRIDGES` en YAML en ninguno de los dos. +- Paridad de reglas: fila origen ACTIVE, tablas `#…`, timers UA, `GEN_STAT_BRIDGES`, etc. + +**Mejoras reales en 2.x** + +| Área | Efecto | +|------|--------| +| **Estabilidad del enrutado** | El estado de bridges vive en un store dedicado; informes y timers ya no comparten la misma estructura mutable que el forward de voz. Menos desvíos entre lo que reenvía el servidor y lo que muestra el panel bajo carga. | +| **Monitor** | **Informe v2** (`routing_table`, `topology`) hacia **adn-monitor 2.x** sustituye pickle/CSV; el BTABLE refleja mejor el estado del peer. | +| **TG dinámicos** | Con **`DATABASE`**, los dinámicos por peer se **persisten** y se restauran al reconectar (≥ 2.0.0-rc.3). | +| **Evolución** | Parches de timers, OpenBridge, ACL o self-service no pasan por un dict global compartido con todo el proceso. | + +El término **subscription** solo importa si lees código o esta guía; en el panel y en el aire sigues hablando de **bridges** y **talkgroups**. + +--- + +## Resumen + +| | **Legacy (`adn-dmr-server`)** | **Nuevo (`adn-server` 2.x)** | +|---|------------------------------|------------------------------| +| **Autoridad en runtime** | Dict global `BRIDGES` (`bridge_master.py`) | **`SubscriptionStore`** (objetos `Subscription` de dominio) | +| **Estructura** | `bridge_key → [ fila, fila, … ]` | Una **subscription** por pata de system en un canal | +| **Resolución de reenvío** | Recorrer filas, llamar `to_target` | **`SubscriptionRouter.resolve()`** → `ForwardLeg` | +| **Wire monitor / informes** | Pickle `BRIDGE_SND` = `BRIDGES` | JSON `routing_table` (v2) o export `BRIDGES` (compat v1) | +| **Bloque YAML `BRIDGES:`** | No se carga desde config en ninguno; filas en runtime | Igual — filas desde OPTIONS, UA, STAT, OpenBridge, bootstrap echo | + +El comportamiento observable (guardia de fila origen, UA dinámico, TG estática, claves reflector `#…`, match OpenBridge TS1, timers) sigue **paridad legacy** con `bridge_master.py`. + +## Legacy: dict `BRIDGES` + +En **adn-dmr-server**, el estado de enrutado es un **diccionario global**: + +```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"] = [ … ] # tablas reflector / marcado +``` + +Cada **fila** es una pata. Campos importantes: + +- **`SYSTEM`** — nombre del system configurado (master HBP o pata OpenBridge). +- **`TS`** — slot 1 o 2 (fuentes OpenBridge usan **TS 1** en el match). +- **`TGID`** — bytes de destino para **reescritura LC** hacia esa pata. +- **`ACTIVE`** — la pata participa en el reenvío si es true. +- **`TIMEOUT` / `TIMER` / `TO_TYPE` / `ON` / `OFF` / `RESET`** — timers UA, static/stat, reglas VTERM in-band. + +**Camino de voz (`dmrd_received`):** + +1. Obtener **clave de bridge** desde la TG destino (y tablas `#…` cuando aplica). +2. Crear tabla dinámica si no existe (UA / STAT / OPTIONS estáticas — mismos disparadores que legacy). +3. Buscar **fila origen ACTIVE** que coincida con **system + slot + TGID** actuales. +4. Por cada otra fila **ACTIVE** en **esa misma tabla**, llamar **`to_target`** (contención, ACL, LC/TA, control de bucle OpenBridge). + +```mermaid +flowchart TB + IN[Ingreso DMRD / DMRE] --> DM[dmrd_received] + DM --> KEY{¿existe bridge key?} + KEY -->|no| CREATE[ensure dynamic / stat / static] + CREATE --> BR + KEY -->|si| BR[(dict BRIDGES)] + BR --> SRC{¿fila origen ACTIVE\nSYSTEM + TS + TGID?} + SRC -->|si| SCAN[otras patas ACTIVE\nmisma tabla] + SCAN --> TT[to_target por pata] + TT --> OUT[Salida HBP / OpenBridge] + SRC -->|no| DROP[sin reenvío] +``` + +El monitor lee el **mismo dict** vía pickle **`BRIDGE_SND`**. + +## Servidor nuevo: `Subscription` + `SubscriptionStore` + +En **adn-server 2.x**, el **modelo de dominio** sustituye filas ad hoc: + +- **`AudioChannel`** — TG lógica + slot `(tgid, slot)`. +- **`Subscription`** — participación de un system: **role**, **política de activación**, **state** (fase, timer), **target_tgid** (LC), **`relay_table_key`** opcional (tablas `#…`). +- **`SubscriptionStore`** — única **autoridad de enrutado** en runtime (sin mutar `BRIDGES` en paralelo). + +**Camino de voz** (misma semántica, otros tipos): + +1. `RoutingUseCases.dmrd_received` actualiza el store (crear relay, TG estática, timeout UA — hooks legacy). +2. **`SubscriptionRouter.relay_tables_with_active_source`** — tablas donde el system de ingreso tiene subscription **ACTIVE** para slot/TG. +3. **`SubscriptionRouter.resolve`** — devuelve **`ForwardLeg`** (system, slot, tgid) del resto de subscriptions **ACTIVE** en esas tablas. +4. Mixins de forward envían paquetes (paridad `to_target`). + +```mermaid +flowchart TB + IN[Ingreso DMRD / DMRE] --> RU[RoutingUseCases.dmrd_received] + RU --> SS[(SubscriptionStore)] + RU --> SR[SubscriptionRouter] + SS --> SR + SR --> LEGS[Lista ForwardLeg] + LEGS --> FWD[Mixins HBP / OBP] + SS --> EXP[RoutingTableLegacyView\nsolo export] + EXP --> MON[Pickle BRIDGE_SND\no JSON routing_table] +``` + +**Importante:** `routing_table_for_report()` / **`BRIDGE_SND`** es un **export unidireccional** para paneles (`RoutingTableLegacyView`). **No** decide reenvíos. Evita el patrón legacy de mutar un dict global compartido con informes. + +## Mapeo fila → subscription + +| Fila legacy `BRIDGES` | `Subscription` de dominio | +|-----------------------|---------------------------| +| Clave tabla (`"52090"`, `"#310"`) | `relay_table_key` + TG del canal | +| `SYSTEM` | `system` (`SystemId`) | +| `TS` + contexto TG | `channel.slot` / `channel.tgid` | +| `TGID` (bytes) | `target_tgid` (reescritura LC) | +| `ACTIVE` | `state.phase` (`ACTIVE` / `IDLE`) | +| `TIMER` | `state.timer_expires_at` | +| `TIMEOUT` | `timeout_seconds` | +| `TO_TYPE` (`ON`, `OFF`, `STAT`, `NONE`) | `role` + `policy` | +| `ON` / `OFF` / `RESET` | `triggers` (`InbandTriggers`) | + +Helpers import/export: `routing_table_import.py`, `routing_table_export.py` (espejo de `bridges_export` legacy). + +## Comparación extremo a extremo (un frame de voz) + +```mermaid +sequenceDiagram + participant Radio + participant Server + participant Peer as Otro system + + Radio->>Server: Voz grupo TG 52090 TS2 + + rect rgb(40,40,50) + note right of Server: Legacy + Server->>Server: ¿Fila origen ACTIVE en BRIDGES["52090"]? + Server->>Server: foreach pata ACTIVE en tabla + Server->>Peer: to_target (reescribe LC) + end + + rect rgb(30,50,40) + note right of Server: Servidor nuevo (mismas reglas) + Server->>Server: SubscriptionRouter.resolve() + Server->>Peer: forward ForwardLeg(s) + end +``` + +## Lo que **no** cambió + +- **Claves** de bridge (`52090`, `#reflector`, …) y tablas **multi-pata**. +- **Guardia de fila origen** — reenviar solo desde una tabla donde **este** system es origen ACTIVE para ese contexto TG/slot. +- **UA dinámico**, **OPTIONS estáticas**, **`GEN_STAT_BRIDGES`**, **TG 4000**, bootstrap **echo 9990**. +- Pasadas de timer (`rule_timer`, `bridgeDebug`, …) — misma tabla lógica, implementada sobre el store en 2.x. + +## Ejemplo concreto + +**Bridge** (concepto de red): “la TG 52090 une estos systems y reenvía voz entre ellos”. +**Subscription** (solo en código 2.x): **una pata** de esa tabla — p. ej. “MASTER-A en TG 52090, slot 2, ACTIVE, con su LC y timer”. + +No es bridge *o* subscription: en 2.x un bridge **es** un conjunto de subscriptions sobre el mismo canal (TG + slot). + +### Escenario + +Alguien habla en **TG 52090** y deben oírlo **MASTER-A**, **MASTER-B** y una pata **OpenBridge**. En el panel y en el aire eso es un **bridge** (tabla de la TG 52090). + +**Legacy (`adn-dmr-server`)** — todo en un dict global: + +```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: … }, +] +``` + +Cuando llega un frame de voz: + +1. Buscar la fila donde **este** system es origen **ACTIVE** (mismo TG/slot). +2. Recorrer las **demás filas ACTIVE** de la misma tabla. +3. Por cada una, **`to_target`** → reenvío con LC reescrito. + +El monitor lee **el mismo dict** (pickle **`BRIDGE_SND`**). + +**Servidor nuevo (`adn-server` 2.x)** — misma tabla, otro formato interno: + +```text +SubscriptionStore — TG 52090 / slot 2: + - subscription MASTER-A (ACTIVE, target_tgid, timer…) + - subscription MASTER-B (ACTIVE, …) + - subscription OBP-UK (IDLE, …) +``` + +Cuando llega voz, **`SubscriptionRouter.resolve()`** aplica las **mismas reglas** (origen ACTIVE, resto de patas ACTIVE) y devuelve **`ForwardLeg`** para reenviar. El monitor recibe una **vista exportada** (`BRIDGE_SND` o JSON **`routing_table`**); esa exportación **no** decide el reenvío. + +## Dónde leer código + +| Tema | Legacy | Nuevo | +|------|--------|-------| +| Ingreso voz | `adn-dmr-server/bridge_master.py` (`routerHBP.dmrd_received`) | `application/routing_use_cases.py` | +| Reenvío a pata | `to_target` | `application/routing/hbp_forward.py`, `obp_forward.py` | +| Estado tabla | `BRIDGES` global | `application/subscription/` (`store`, `router`, ops) | +| Export monitor | `send_routing_table` / pickle | `routing_table_legacy_view.py`, informe v2 `routing_table` | + +Ver también: [Bridges y talkgroups](../user-guide/bridges-and-talkgroups.md), [Arquitectura](architecture.md), [Rendimiento (2.x)](performance.md), [Protocolo de informes v2](../protocols/report-v2.md#routing_table). diff --git a/docs/es/server/development/performance.md b/docs/es/server/development/performance.md new file mode 100644 index 0000000..41a2179 --- /dev/null +++ b/docs/es/server/development/performance.md @@ -0,0 +1,99 @@ +# Rendimiento (2.x) + +**adn-server 2.x** y **adn-monitor 2.x** incluyen varios cambios que reducen trabajo de CPU y huella de memoria frente a **adn-dmr-server** y al stack antiguo de monitor/proxy. Esta página resume **qué** mejora y **qué lo provoca**. + +## Resumen + +| Área | Efecto típico | Causa principal | +|------|---------------|-----------------| +| **Downlink de voz (proxy inject)** | Menos CPU con tráfico de grupo intenso | **`PeerDownlinkIndex`** — fan-out solo a peers que encajan `(slot, TG)` en lugar de escanear todos los hotspots por paquete | +| **Origen ACTIVE en bridge** | Lookup más rápido | **Índices del `SubscriptionStore`** (`relay_tables_with_active_source`) — O(1) por `(system, slot, tgid)` frente a recorrer filas | +| **CPU de fondo** | Menos despertares | **OPTIONS / TG estática por eventos** — eliminado el bucle legacy cada **26 s** `options_config_loop` ([Comportamiento y temporizadores](behaviour-and-timers.md)) | +| **Ráfaga de logins** | Menos CONFIG redundante | **`ConfigPushThrottle`** — debounce adaptativo al empujar CONFIG al monitor | +| **Informes vs voz** | La voz se bloquea menos por informes | **`BoundedReportQueue`** — snapshots coalescidos, drenado acotado por tick | +| **Cable servidor → monitor** | Menos serializar/enviar | **Informe v2** JSON (`routing_table`, `topology`, `voice_event`) en lugar de pickle periódico de `CONFIG`/`BRIDGES` ([Protocolo de informes v2](../protocols/report-v2.md)) | +| **Procesos (RAM)** | Un proceso Python en lugar de dos | **`PROXY` integrado** en `adn-server.py` — sin proceso **adn-proxy** aparte ([Proxy hotspot](../user-guide/hotspot-proxy.md)) | +| **RAM / WS del monitor** | Estado de panel más compacto | **Wire slim `dashboard_state`**, `clean_sys_dict`, fingerprints WS más ligeros ([Arquitectura del monitor](../../monitor/architecture.md)) | + +## Servidor: índice de downlink inject-only + +La mayor ganancia de **CPU** en muchas redes ADN está en el camino **MASTER inject-only** (`PROXY` en modo inject-only). + +**Legacy:** `send_peers` recorre **todos los peers registrados** por cada paquete de downlink → coste **O(peers × paquetes/s)**. + +**2.x:** `PeerDownlinkIndex` precalcula candidatos desde **OPTIONS** (TG estáticas) y estado **UA** de cada peer. Por cada frame de voz de grupo solo se consideran peers que **podrían** querer ese `(slot, TG)`; cada candidato sigue pasando `peer_should_receive_group_voice`. + +```text +Legacy: cada DMRD → probar los N peers +2.x: cada DMRD → lookup en índice → probar k peers (k ≪ N en proxies cargados) +``` + +El parse de OPTIONS se **guarda en caché por peer** (`_CACHED_OPTIONS_STATIC`): si el blob OPTIONS no cambió, se reutilizan las TG estáticas ya parseadas en lugar de volver a interpretarlo en cada paquete. + +| Código | Rol | +|--------|-----| +| `application/routing/peer_downlink_index.py` | Construcción del índice y `(slot, tgid) → candidatos` | +| `infrastructure/twisted_adapters/udp_hbp.py` | `_iter_downlink_peers`, `send_peers` | +| `tests/infrastructure/test_peer_downlink_fanout.py` | Tests de fan-out inject-only | + +**Cuándo se nota:** proxy con **decenas o cientos** de hotspots y voz de grupo continua. En una conferencia pequeña con pocos peers, la diferencia es pequeña. + +## Servidor: índices de enrutado + +En cada frame de voz de grupo el servidor debe encontrar tablas donde **este system es origen ACTIVE**. + +**Legacy:** recorrer filas dentro de `BRIDGES[clave]`. + +**2.x:** `InMemorySubscriptionStore.relay_tables_with_active_source()` usa el índice **`_source_tables`** — lookup por `(system, slot, dst_tgid)` sin recorrer todas las patas. + +Está en la implementación del store; es un **índice algorítmico**, no una opción de configuración aparte. + +| Código | Rol | +|--------|-----| +| `infrastructure/subscription_store.py` | `_source_tables`, `_by_table`, `_active_target_counts` | +| `application/subscription/router.py` | `SubscriptionRouter.resolve()` | + +## Servidor: menos trabajo periódico y en tormenta de logins + +| Cambio | Qué evita | +|--------|-----------| +| **Sin bucle OPTIONS 26 s** | Timer cada 26 s en todos los systems cuando RPTO/arranque/reload ya refrescan bridges estáticos | +| **`ConfigPushThrottle`** | Inundar al monitor con snapshots CONFIG cuando muchos peers conectan en pocos segundos (debounce ~0,3 s → ~2 s en ráfaga) | +| **`BoundedReportQueue`** | Encode pickle/JSON y envío TCP en el hot path de voz; coalesce de snapshots config/bridge duplicados | + +## Servidor: informes y despliegue + +- **Informe v2** — JSON estructurado sustituye snapshots pickle opacos de bridge/config en el cable hacia **monitor 2.x**. Ver [Monitor e informes](../user-guide/monitoring.md) y [Protocolo de informes v2](../protocols/report-v2.md). +- **Proxy integrado** — `PROXY` **in-process**; quitar **adn-proxy** standalone ahorra **RAM** base (un intérprete, config compartida) y simplifica operación. + +## Monitor (adn-monitor 2.x) + +Empareja **adn-server 2.x** con **adn-monitor 2.x** para las mejoras del lado informes: + +| Cambio | Efecto | +|--------|--------| +| **Wire slim / `dashboard_state`** | El monitor ingiere JSON compacto en lugar de duplicar árboles pickle v1 | +| **`clean_sys_dict`** | Expulsión periódica de entradas obsoletas en memoria (tope de crecimiento en paneles largos) | +| **Caché lastheard, fingerprints WS ligeros** | Menos trabajo por refresco del dashboard | +| **Stack FastAPI unificado** | Eliminados API PHP y proceso **proxy** standalone del monitor | + +Detalle: [Arquitectura del monitor](../../monitor/architecture.md). + +## Cuándo se nota la diferencia + +| Despliegue | CPU | RAM | +|------------|-----|-----| +| Pocos masters, sin proxy inject, tráfico bajo | Poca | Poca | +| **Proxy inject-only, muchos hotspots, TG activa** | **Clara** (índice downlink) | Moderada (un proceso servidor vs servidor+proxy) | +| Monitor largo + informe v2 | Moderada (menos serializar en cable) | **Más clara** en monitor (estado slim, `clean_sys_dict`) | + +Crypto, AMBE y MAC OpenBridge siguen dominando en tramos OBP cargados — optimizar la tabla de bridge no elimina ese coste. + +## Lecturas relacionadas + +- [Arquitectura](architecture.md) — capas y entrypoint +- [BRIDGES vs Subscriptions](bridges-vs-subscriptions.md) — modelo de enrutado (no es feature de rendimiento) +- [Comportamiento y temporizadores](behaviour-and-timers.md) — OPTIONS por eventos vs bucle 26 s legacy +- [Proxy hotspot](../user-guide/hotspot-proxy.md) — `PROXY` integrado / inject-only +- [Protocolo de informes v2](../protocols/report-v2.md) — cable JSON al monitor +- Notas de versión: `CHANGELOG.md` en la raíz del repositorio (`Performance` en **2.0.0-rc.1**). diff --git a/docs/es/server/protocols/report-v2.md b/docs/es/server/protocols/report-v2.md index 404d614..46c40b1 100644 --- a/docs/es/server/protocols/report-v2.md +++ b/docs/es/server/protocols/report-v2.md @@ -28,6 +28,22 @@ Sustituir instantáneas al monitor que hoy usan **pickle** (`CONFIG_SND`, `BRIDG Los opcodes `0x10`–`0x13` están reservados en esta fase; pueden ajustarse antes de P1-002. +```mermaid +flowchart TB + subgraph v1 [Informe v1 — adn-server 1.0.x + monitor 1.0.x] + H1[HELLO protocol 1] --> C1[CONFIG_SND pickle] + C1 --> B1[BRIDGE_SND pickle] + B1 --> V1[BRDG_EVENT CSV] + end + + subgraph v2 [Informe v2 — adn-server 2.x + monitor 2.x] + H2[HELLO report_protocol 2] --> T2[TOPOLOGY_SND JSON] + T2 --> R2[ROUTING_TABLE_SND JSON] + R2 --> V2[VOICE_EVENT_SND JSON] + R2 -.-> D2[DELTA_SND opcional] + end +``` + ## Handshake (`hello`) Al conectar, el servidor envía **`HELLO` (`0xFF`)** primero. Clientes v2 miran `report_protocol`: diff --git a/docs/es/server/user-guide/bridges-and-talkgroups.md b/docs/es/server/user-guide/bridges-and-talkgroups.md index afda545..6c8fb65 100644 --- a/docs/es/server/user-guide/bridges-and-talkgroups.md +++ b/docs/es/server/user-guide/bridges-and-talkgroups.md @@ -12,11 +12,37 @@ La tabla de bridges asocia **claves de talkgroup** (cadenas, p. ej. `"26811"`, ` El router recorre `BRIDGES` buscando una fila **ACTIVE** que coincida con el **sistema de origen actual**, **slot** y **TG de destino** antes de reenviar (`dmrd_received` → `to_target`). +En **adn-server 2.x** las mismas reglas están en **`SubscriptionStore`** / **`SubscriptionRouter`**; `BRIDGES` es solo forma de export para el monitor. Ver [BRIDGES vs Subscriptions](../development/bridges-vs-subscriptions.md). + ## Dinámico frente a estático - Los bridges **activados por usuario** se crean cuando alguien pulsa una TG sin fila previa (sujeto a `DEFAULT_UA_TIMER` y opciones). - Las TG **estáticas** y bridges **STAT** se crean desde flujos **OPTIONS** / `make_static_tg` / `GEN_STAT_BRIDGES`. +## Persistencia TG dinámicos (MariaDB) {#persistencia-tg-dinamicos-mariadb} + +Desde **2.0.0-rc.3**, los TG dinámicos activados por usuario de cada hotspot pueden **persistirse en MariaDB** (`peer_dynamic_tgs`) para sobrevivir a **desconexión/reconexión** sin volver a pulsar el TG. + +| Evento | Comportamiento del servidor | +|--------|----------------------------| +| **Cabecera de voz de grupo** (nuevo TG dinámico en un slot) | Registra sesión UA en memoria y **upsert asíncrono** en `peer_dynamic_tgs`. | +| **RPTC** (login OK del hotspot) | **Restaura** filas de ese peer/system en memoria y re-sincroniza bridges (`ensure_dynamic_relay`). | +| **TG 4000** | Borra **todos** los slots dinámicos de ese peer (memoria + BD). Ver [Números especiales — TG 4000](special-numbers.md#tg--id-4000--desactivar-bridges-dinamicos). | +| **Desconexión del hotspot** | Solo limpia el **espejo** por peer; las filas persistidas y mapas globales `_PEER_UA_*` se mantienen hasta expiración o TG 4000. | +| **Purga periódica** | Cada **60 s**, filas **SINGLE=1** expiradas se eliminan de BD y memoria. | + +Peers **SINGLE=0** acumulan varios TG dinámicos por slot (`_PEER_UA_MULTI_TGS`). **SINGLE=1** guarda un TG exclusivo por slot con temporizador. + +**TG 4000** nunca se almacena como sesión dinámica (solo comando de reset). + +Requiere **`DATABASE`** en `adn-server.yaml` — ver [Configuración](configuration.md#database-mariadb). + +## Downlink cross-slot de TG estáticas (inject-only) + +En MASTER **inject-only** ( **`PROXY`** integrado), el downlink de voz de grupo respeta **TG estáticas listadas en OPTIONS de TS1 o TS2**, aunque el **slot en cable** sea otro. Equivale al comportamiento legacy REPEAT para hotspots que listan un TG en un slot y transmiten en otro. + +El servidor **no** reescribe el slot del DMRD entrante; filtra **a qué peers reenvía** el paquete repetido con `peer_should_receive_group_voice` y el índice de downlink. + ## Guardia de fila de origen e iteración segura El reenvío solo se permite cuando el sistema actual tiene una **fila de origen ACTIVE** que coincide con ese contexto TG/slot. Esto evita reenviar desde filas presentes pero no elegibles como patas de origen. diff --git a/docs/es/server/user-guide/configuration.md b/docs/es/server/user-guide/configuration.md index 7262cce..c1041df 100644 --- a/docs/es/server/user-guide/configuration.md +++ b/docs/es/server/user-guide/configuration.md @@ -29,9 +29,9 @@ kill -HUP $(pidof adn-server.py) # o: systemctl reload adn-server Unidad de ejemplo: **`examples/systemd/adn-server.service`** (copiar a `/etc/systemd/system/`; incluye `ExecReload` para `systemctl reload`). -**Se recarga:** `GLOBAL`, `REPORTS`, `ALIASES`, **`LOGGER.LOG_LEVEL`** (sin reiniciar el proceso), **`PROXY`** (timeouts, debug, listas de bloqueo — no bind ni destino), **`SELF_SERVICE`** (fusionado; activar/desactivar bucles BD requiere reinicio), parámetros por system, **systems nuevos/eliminados** (incluida expansión/colapso `GENERATOR` y OBP nuevos), y cambios de IP/puerto (solo reinicia el listener de ese system). +**Se recarga:** `GLOBAL`, `REPORTS`, `ALIASES`, **`LOGGER.LOG_LEVEL`** (sin reiniciar el proceso), **`PROXY`** (timeouts, debug, listas de bloqueo — no bind ni destino), **`SELF_SERVICE`** (flags PBKDF2 fusionados; activar/desactivar bucles BD requiere reinicio), parámetros por system, **systems nuevos/eliminados** (incluida expansión/colapso `GENERATOR` y OBP nuevos), y cambios de IP/puerto (solo reinicia el listener de ese system). -**No se recarga:** `adn-voice.yaml` (loop aparte cada 15 s), código Python, ficheros de alias (recarga periódica). La tabla **BRIDGES** no se reconstruye — reinicia si cambiaste reglas de bridge que exijan reset completo. **`PROXY.LISTEN_PORT`**, **`LISTEN_IP`** y **`TARGET_SYSTEM`** requieren **reinicio completo** para aplicarse. +**No se recarga:** **`DATABASE`** (pool MariaDB y bootstrap `peer_dynamic_tgs`), `adn-voice.yaml` (loop aparte cada 15 s), código Python, ficheros de alias (recarga periódica). La tabla **BRIDGES** no se reconstruye — reinicia si cambiaste reglas de bridge que exijan reset completo. **`PROXY.LISTEN_PORT`**, **`LISTEN_IP`** y **`TARGET_SYSTEM`** requieren **reinicio completo** para aplicarse. **Secretos:** no versionar passphrases reales, URLs de seguridad ni `user_passwords.json` / `encryption_key.secret`. Usa placeholders en plantillas y mantén producción en local. @@ -169,7 +169,7 @@ Para el modelo conceptual (ACTIVE, TS, TGID, timeouts): [Bridges y talkgroups](b ## `REPORTS` -Canal TCP de informes para **adn-monitor** (o paneles compatibles). +Canal TCP de informes para **adn-monitor** (o paneles compatibles). **adn-server 2.x** habla **solo informe v2**; los paneles legacy que esperan **v1** necesitan el opcional [report-proxy](report-proxy.md) (paquete aparte). | Clave | Significado | |-------|-------------| @@ -178,7 +178,7 @@ Canal TCP de informes para **adn-monitor** (o paneles compatibles). | **REPORT_PORT** | Puerto local en el que el **servidor escucha** clientes de informes. | | **REPORT_CLIENTS** | Lista separada por comas o lista de IPs de clientes permitidos (ver ejemplo). | -Detalle: [Monitor e informes](monitoring.md). +Detalle: [Monitor e informes](monitoring.md). Monitores legacy v1: [Proxy de informes](report-proxy.md). --- @@ -198,9 +198,38 @@ Se arranca siempre que exista un bloque **`PROXY`** (ver `adn-server.example.yam --- +## `DATABASE` (MariaDB) + +**Obligatorio** en configs típicas de servidor de conferencia: cualquier despliegue con **`PROXY`**, o al menos un system **`MASTER`** / **`OPENBRIDGE`**. **No** es obligatorio en flotas **solo echo** (`adn-server.py --echo`). + +| Clave | Significado | +|-------|-------------| +| **DB_SERVER** | Host MariaDB/MySQL. | +| **DB_USERNAME** / **DB_PASSWORD** | Credenciales. | +| **DB_NAME** | Nombre de la base (a menudo la misma que **adn-monitor**, p. ej. `hbmon`). | +| **DB_PORT** | Puerto TCP (por defecto **3306**). | + +**Un solo pool** compartido para: + +- **Persistencia de TG dinámicos** — tabla **`peer_dynamic_tgs`** (TG activados por usuario por hotspot entre reconexiones). El servidor **crea la tabla al arranque** si falta (migración **`004_peer_dynamic_tgs`**, mismo esquema que adn-monitor). +- **Self-service integrado** — tabla **`Clients`** con **`SELF_SERVICE.USE_SELFSERVICE: true`**. + +El arranque aborta con log claro si MariaDB no responde o **`DATABASE`** está incompleto. Instala **`mysqlclient`** (`pip install -e ".[selfservice]"` lo incluye). + +**Recarga en caliente:** cambiar **`DATABASE`** exige **reinicio completo** del proceso. + +Detalle: [Bridges y talkgroups — persistencia TG dinámicos](bridges-and-talkgroups.md#persistencia-tg-dinamicos-mariadb). + +--- + ## `SELF_SERVICE` (MySQL / opciones del panel) -Opcional; requiere `pip install -e ".[selfservice]"` con **`USE_SELFSERVICE: true`**. Usa la misma tabla **`Clients`** y parámetros PBKDF2 que **adn-monitor**. Las claves coinciden con la documentación del monitor — ver [Self-service](../../monitor/self-service.md) y [Proxy hotspot](hotspot-proxy.md#claves-self_service). +Opcional; requiere `pip install -e ".[selfservice]"` con **`USE_SELFSERVICE: true`**. Usa el bloque **`DATABASE`** anterior (sin claves DB separadas en **`SELF_SERVICE`**). Los parámetros PBKDF2 deben **coincidir** con **adn-monitor**. Ver [Self-service](../../monitor/self-service.md) y [Proxy hotspot](hotspot-proxy.md#claves-self_service). + +| Clave | Significado | +|-------|-------------| +| **USE_SELFSERVICE** | Activa sincronización de opciones desde el panel (`true` / `false`). | +| **PBKDF2_SALT** / **PBKDF2_ITERATIONS** | Deben coincidir con **`adn-monitor.yaml`** / herramienta de contraseñas. | --- diff --git a/docs/es/server/user-guide/hotspot-proxy.md b/docs/es/server/user-guide/hotspot-proxy.md index 6614ca0..457ebde 100644 --- a/docs/es/server/user-guide/hotspot-proxy.md +++ b/docs/es/server/user-guide/hotspot-proxy.md @@ -14,6 +14,13 @@ La configuración está en **`adn-server.yaml`**, bloques **`PROXY`** y opcional El proxy integrado usa **fan-in**: los hotspots solo necesitan **`PROXY.LISTEN_PORT`** (p. ej. **62031**). El **MASTER** destino es **solo inyección** — **no** abre su propio puerto UDP para ese system (sin rango de puertos por hotspot en el host del servidor). +```mermaid +flowchart LR + HS1[Hotspot A] -->|UDP HBP| LP[PROXY LISTEN_PORT] + HS2[Hotspot B] -->|UDP HBP| LP + LP -->|inyecta| MASTER[TARGET_SYSTEM MASTER] +``` + --- ## Dependencia opcional (self-service) @@ -72,9 +79,10 @@ Misma semántica que **`adn-monitor.yaml`** — tabla **`Clients`** compartida, | Clave | Rol | |-------|-----| | **USE_SELFSERVICE** | Activa sincronización de opciones con MySQL (`true` / `false`). | -| **DB_SERVER**, **DB_USERNAME**, **DB_PASSWORD**, **DB_NAME**, **DB_PORT** | Conexión MySQL. | | **PBKDF2_SALT**, **PBKDF2_ITERATIONS** | Deben **coincidir** con **`adn-monitor.yaml`** para el hash de contraseñas. | +La conexión MariaDB está en el bloque **`DATABASE`** (compartido con persistencia de TG dinámicos) — ver [Configuración](configuration.md#database-mariadb). + Al arrancar el servidor registra **`(SELF_SERVICE) Database connection test: OK`** y **`(SELF_SERVICE) Enabled`** si el pool conecta. El self-service es **asíncrono**; el reenvío de voz no se bloquea por latencia de BD. Detalle del flujo en el panel: [Self-service](../../monitor/self-service.md). diff --git a/docs/es/server/user-guide/introduction.md b/docs/es/server/user-guide/introduction.md index 645a660..a9a5758 100644 --- a/docs/es/server/user-guide/introduction.md +++ b/docs/es/server/user-guide/introduction.md @@ -17,8 +17,8 @@ Enrutado, temporizadores, control de bucle OpenBridge y manejo de protocolo est | Subsistema | Rol | |------------|-----| -| **Bridge router** | Tabla `BRIDGES`: qué sistemas reenvían qué TG en qué slot; bridges dinámicos; bridges estáticos/stat. | -| **Protocolo HBP** | Autenticación, ingreso/salida DMRD, repetición a peers, filtros TG. | +| **Bridge router** | Tabla `BRIDGES`: qué sistemas reenvían qué TG en qué slot; bridges dinámicos; bridges estáticos/stat; **restauración MariaDB de TG dinámicos** al reconectar. | +| **Protocolo HBP** | Autenticación, ingreso/salida DMRD, repetición a peers, filtros TG, seguimiento de **sesión UA por peer**. | | **OpenBridge** | Ingreso DMRE, límite de saltos, control de bucle (`min(1ST)`), BCSQ/BCKA si están habilitados. | | **Voz** | Ficheros AMBE, anuncios programados, tubería TTS, reproducción bajo demanda (TG 9991–9999). | | **Informes** | Canal TCP netstring hacia **adn-monitor** (y paneles compatibles): config, estado de bridges, eventos de llamada (informe v2 JSON). | @@ -28,12 +28,14 @@ Enrutado, temporizadores, control de bucle OpenBridge y manejo de protocolo est - **Echo / playback** — `adn-server.py --echo` con `adn-echo.yaml` mínimo; ver [Echo](echo.md). - **Proxy hotspot integrado** — `PROXY` en **`adn-server.yaml`**; ver [Proxy hotspot](hotspot-proxy.md). +- **Proxy de informes (paneles legacy)** — **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** opcional para que **adn-server 2.x** alimente monitores antiguos estilo HBMonitor / FDMR (wire v1); ver [Proxy de informes](report-proxy.md). No se usa con **adn-monitor 2.x**. ## Siguientes pasos -- [Configuración](configuration.md) — ficheros, `GLOBAL`, **MASTER** / **PEER** / **OPENBRIDGE**, ACL, informes, **`PROXY`**, **`SELF_SERVICE`**, alias, fusión de voz. +- [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`. - [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). +- [Rendimiento (2.x)](../development/performance.md) — mejoras de CPU/RAM en esta versión y qué las provoca. - [Créditos y licencia](attribution.md) — ADN → FreeDMR → hblink3, licencia. diff --git a/docs/es/server/user-guide/monitoring.md b/docs/es/server/user-guide/monitoring.md index 0eca217..3d072c9 100644 --- a/docs/es/server/user-guide/monitoring.md +++ b/docs/es/server/user-guide/monitoring.md @@ -12,12 +12,26 @@ Cuando **`REPORTS`** está habilitado en la config del servidor, el **ADN DMR Pe **Acoplamiento de versiones:** **servidor 1.0.x + monitor 1.0.x** = report v1 (tags). **servidor 2.x** emite **solo report v2** — requiere **monitor 2.x**. Sin wire `dual`; monitor 1.0.x no decodifica este servidor. +### Paneles legacy (report-proxy) + +Si mantienes un **panel antiguo** cuyo backend monitor solo habla **informe v1** (pickle/CSV, sin HELLO v2), **no puede** conectarse a **adn-server 2.x** en `REPORTS.REPORT_PORT`. Usa el opcional **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** para traducir **v2 → v1**: el proxy se conecta al servidor; el monitor legacy se conecta al proxy. **adn-monitor 2.x** **no** necesita este proxy — conéctalo directamente al servidor. + +Ver [Proxy de informes (paneles legacy)](report-proxy.md) para topología, `REPORT_CLIENTS`, puertos y orden de arranque. + Las pilas antiguas (**legado** estilo `adn-dmr-server`) pueden **omitir** HELLO. **adn-monitor** espera hasta **`ADN_CONNECTION.HELLO_TIMEOUT_MS`** (ver [Configuración del monitor](../../monitor/configuration.md#adn_connection)); si no llega HELLO, asume informes **legacy**. El **monitor** decodifica estos mensajes, actualiza **CTABLE** / **BTABLE** y (con MySQL configurado) persiste Last Heard / estadísticas. **Pila completa:** [Descripción general del ADN Monitor](../../monitor/index.md) (monitor FastAPI, WebSocket, self-service). +```mermaid +flowchart LR + SRV[adn-server\nREPORTS.REPORT_PORT] -->|TCP netstring| ING[ingest adn-monitor] + ING --> STATE[CTABLE / BTABLE] + STATE --> WS[WebSocket /ws] + WS --> UI[panel React] +``` + ### Líneas de log del canal de informes (logger `adn-monitor`) Python usa el nombre de logger **`adn-monitor`** (ver **`LOGGER.LOG_FILE`** en `adn-monitor.yaml`). **INFO** típicos del cliente TCP de informes: @@ -40,6 +54,19 @@ En **WARNING**: JSON HELLO inválido (`(REPORT) HELLO payload not valid JSON`), El panel muestra el estado **operativo** desde **START** (canónico); el **log del Monitor** muestra **INGRESS** más **START** para depurar duplicados en malla. +### Chips UA dinámicos (OPTIONS del hotspot) + +El monitor rastrea TG **activados por usuario** por hotspot para los chips índigo del panel: + +| OPTIONS del peer | Fuente en el monitor | +|----------------|----------------------| +| **SINGLE=1** | **`UA_SESSIONS`** en **CONFIG_SND** / `dashboard_state` (el servidor es fuente de verdad). | +| **SINGLE=0** | Eventos de voz (`BRDG_EVENT` / `voice_event`) — varios dinámicos por slot hasta limpiar. | + +**TG 4000** limpia el estado UA con **`GROUP VOICE,INGRESS,RX`** y destino **4000** (el servidor lo envía porque la ruta de voz corta antes y no emite un **START** normal). El monitor **no** debe registrar **4000** como TG dinámico. + +**Emparejamiento de versiones:** **adn-server 2.0.0-rc.3** + **adn-monitor 2.0.0-rc.4** para persistencia de TG dinámicos y sincronización TG 4000 en el monitor. + ## Rotación de logs (logrotate) Tras que **logrotate** renombre o mueva el fichero de log (patrón habitual: **`create`** — el fichero antiguo rota y aparece uno **nuevo vacío** en la ruta configurada), el proceso puede seguir con el descriptor abierto sobre el **inodo anterior**. Los logs parecen “no escribirse” en la ruta actual hasta que el proceso **reabra** los `FileHandler`. diff --git a/docs/es/server/user-guide/report-proxy.md b/docs/es/server/user-guide/report-proxy.md new file mode 100644 index 0000000..76bdc81 --- /dev/null +++ b/docs/es/server/user-guide/report-proxy.md @@ -0,0 +1,100 @@ +# Proxy de informes (paneles legacy) + +**ADN DMR Peer Server 2.x** emite **informe wire v2** (JSON por TCP). **adn-monitor 2.x** entiende ese protocolo y se conecta **directamente** al servidor — no hace falta ningún componente extra. + +Algunos **stacks de panel legacy** siguen trayendo su propio backend `dashboard.py` / `monitor.py` y solo hablan **informe wire v1** (pickle `CONFIG_SND` / `BRIDGE_SND`, CSV `BRDG_EVENT`). Esos monitores **no pueden** conectarse a **adn-server 2.x** en el puerto de informes. + +El paquete opcional **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** queda en medio: se conecta **upstream** al servidor real (v2), escucha **downstream** donde el monitor legacy espera el servidor (v1) y traduce **v2 → v1**. + +| Stack | Servidor upstream | ¿Funciona sin proxy? | +|-------|-------------------|----------------------| +| **adn-monitor 2.x** (React) | **adn-server 2.x** | Sí — conectar a `REPORTS.REPORT_PORT` | +| Panel legacy + monitor incluido (v1) | **adn-dmr-server** (v1) | Sí — directo al puerto de informes del servidor | +| Panel legacy + monitor incluido (v1) | **adn-server 2.x** (v2) | **No** — usar **report-proxy** | + +Objetivos legacy típicos: forks antiguos de **ADN-Dashboard**, despliegues **HBMonitor** / **FDMR Monitor** que aún ejecutan un proceso monitor Python contra `dashboard.cfg` / `monitor.cfg`. + +## Topología + +```text +┌───────────────────┐ +│ adn-server │ +│ ESCUCHA :4321 │ +└─────────▲─────────┘ + │ + │ TCP v2 JSON + │ (report-proxy es CLIENTE) + │ +┌─────────┴─────────┐ +│ report-proxy │ +│ ESCUCHA :4322 │ +└─────────▲─────────┘ + │ + │ TCP v1 pickle + │ (panel legacy es CLIENTE) + │ +┌─────────┴─────────┐ +│ panel legacy │ +│ monitor.py │ +└───────────────────┘ +``` + +| Componente | Rol | Puerto por defecto | Config | Clave | +|------------|-----|-------------------|--------|-------| +| **adn-server** | Escucha clientes de informes | **4321** | `adn-server.yaml` | `REPORTS.REPORT_PORT` | +| **report-proxy** | Se conecta al servidor | 4321 | `report-proxy.yaml` | `UPSTREAM.PORT` | +| **report-proxy** | Escucha al monitor legacy | **4322** | `report-proxy.yaml` | `LISTEN.PORT` | +| **Panel legacy** | Se conecta al proxy | **4322** | `dashboard.cfg` | `SERVER_PORT` | + +**No** apuntes el panel legacy al **4321** — es el puerto v2 del servidor. + +**No** pongas `UPSTREAM.PORT` en **4322** — es el puerto de escucha del propio proxy. + +## Lado servidor (`adn-server.yaml`) + +Los informes deben estar activos y la **IP del host del proxy** debe estar en la lista permitida: + +```yaml +REPORTS: + REPORT: true + REPORT_INTERVAL: 60 + REPORT_PORT: 4321 + REPORT_CLIENTS: "127.0.0.1" # IP de la máquina donde corre report-proxy +``` + +Si el proxy corre en otro host, usa la **IP de ese host** en `REPORT_CLIENTS`, no solo `127.0.0.1`. Ver [Configuración](configuration.md#reports) para todas las claves de `REPORTS`. + +## Proxy y panel legacy + +Instala y ejecuta el proxy desde el repositorio **[ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy)** (`report-proxy.yaml`, `python3 report-proxy.py -c report-proxy.yaml`). Apunta `UPSTREAM` al `REPORT_PORT` del servidor y `LISTEN` al puerto que usa el monitor legacy (a menudo **4322**). + +En `dashboard.cfg` / `monitor.cfg` legacy: + +```ini +[SERVER CONNECTION] +SERVER_IP = 127.0.0.1 +SERVER_PORT = 4322 +``` + +`SERVER_IP` es el host donde **report-proxy** escucha, no necesariamente el host de adn-server. + +**Orden de arranque:** adn-server → report-proxy → backend monitor legacy. + +Pasos completos, ejemplos multi-host, comprobaciones y errores frecuentes: **[README de ADN-report-proxy](https://github.com/ce5rpy/ADN-report-proxy#configuration-legacy-dashboard--adn-server-2x)**. + +## Traducción de wire (resumen) + +| Upstream (v2 desde adn-server) | Downstream (v1 al monitor legacy) | +|--------------------------------|-----------------------------------| +| `HELLO` (`report_protocol: 2`) | `HELLO` (`protocol: 1`) | +| `STATE_SND` / `dashboard_state` | `CONFIG_SND` (pickle) | +| `ROUTING_TABLE_SND` | `BRIDGE_SND` (pickle) | +| `TOPOLOGY_SND` | `CONFIG_SND` (pickle) | +| `VOICE_EVENT_SND` | `BRDG_EVENT` (CSV) | + +Detalle del esquema v2: [Protocolo de informes v2 (JSON)](../protocols/report-v2.md). + +## Ver también + +- [Monitor e informes](monitoring.md) — canal de informes, emparejamiento con **adn-monitor**, líneas de log. +- [Descripción general de ADN Monitor](../../monitor/index.md) — panel recomendado para **adn-server 2.x** (sin proxy). diff --git a/docs/es/server/user-guide/special-numbers.md b/docs/es/server/user-guide/special-numbers.md index b74bec8..ccabdf4 100644 --- a/docs/es/server/user-guide/special-numbers.md +++ b/docs/es/server/user-guide/special-numbers.md @@ -56,17 +56,22 @@ La activación/desactivación in-band de bridges se aplica sobre **voice termina - En bridges reflector (`#...`), el manejo in-band solo se evalúa cuando el destino es **TG 9**. - Por eso los mensajes de reflector y el cableado de marcado usan TG 9, mientras que llamadas privadas no disparan esa lógica de temporizadores de bridge. -## TG / ID 4000 — desactivar bridges dinámicos +## TG / ID 4000 — desactivar bridges dinámicos {#tg--id-4000--desactivar-bridges-dinamicos} -**Propósito:** borrar **bridges dinámicos activados por usuario** para el sistema que recibe la llamada. +**Propósito:** Borrar el estado **activado por usuario (dinámico)** del hotspot que pulsa **4000**. **TG 4000 no es** un talkgroup que deba monitorizarse ni persistirse — es un **comando de reset**. -**Comportamiento:** +**Comportamiento (cabecera de voz de grupo):** + +- Limpia sesiones UA del peer en memoria (**todos los slots** de ese peer). +- Borra filas correspondientes en **`peer_dynamic_tgs`** (MariaDB). +- Limpia campos RX obsoletos en **STATUS** para que un **RPTO** posterior no re-sembré el TG antiguo. +- Ejecuta **desactivación in-band** de bridges en el slot (como legacy). +- Envía **`GROUP VOICE,INGRESS,RX,…,4000`** al monitor (no **START**) para que los chips **SINGLE=0** se limpien **sin** encender TX en vivo. +- En MASTER **inject-only**, empuja **CONFIG_SND** actualizado al monitor. -- Implementado para tráfico de **grupo** con destino **4000** (y comprobaciones relacionadas en el router). -- Se ejecuta **antes** que la ACL normal de TG en la ruta OpenBridge para que el comando no quede bloqueado por listas permitidas. -- Invoca **`deactivate_all_dynamic_bridges`**: desactiva filas dinámicas que no sean stat ni reflector. +**Inject-only frente a global:** Con **`PROXY`** integrado, el reset es **por peer** (solo los dinámicos de ese hotspot). Sin filtro inject-only, sigue aplicándose legacy **`deactivate_all_dynamic_bridges`** a todo el system. -Úsalo cuando los operadores necesiten **reiniciar** el enrutado dinámico sin reiniciar el servidor. +**TG 4000 no debe aparecer** como chip UA dinámico en el monitor ni en `peer_dynamic_tgs`. ### Impacto de `SINGLE_MODE` en la lógica de desactivación @@ -93,6 +98,8 @@ 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). + **Nota:** Un **echo independiente** también está disponible como proceso aparte — [Echo](echo.md). ## Llamada privada al ID 4000 diff --git a/mkdocs.es.yml b/mkdocs.es.yml index 81c53d4..f5e97fa 100644 --- a/mkdocs.es.yml +++ b/mkdocs.es.yml @@ -32,10 +32,15 @@ theme: plugins: - search + - mermaid2 markdown_extensions: - attr_list - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:mermaid2.fence_mermaid_custom - pymdownx.tabbed: alternate_style: true - admonition @@ -56,6 +61,7 @@ nav: - Llamadas privadas: server/user-guide/private-calls.md - Voz, anuncios y TTS: server/user-guide/voice-and-tts.md - Monitor e informes: server/user-guide/monitoring.md + - Proxy de informes (paneles legacy): server/user-guide/report-proxy.md - Proxy hotspot (integrado): server/user-guide/hotspot-proxy.md - Echo (reproducción): server/user-guide/echo.md - Créditos y licencia: server/user-guide/attribution.md @@ -66,6 +72,8 @@ nav: - Protocolo de informes v2 (JSON): server/protocols/report-v2.md - Desarrollo: - Arquitectura: server/development/architecture.md + - 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 - Contribuir: - Traducciones: server/contributing/translations.md diff --git a/mkdocs.yml b/mkdocs.yml index eecd9fc..f817ca8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,10 +32,15 @@ theme: plugins: - search + - mermaid2 markdown_extensions: - attr_list - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:mermaid2.fence_mermaid_custom - pymdownx.tabbed: alternate_style: true - admonition @@ -56,6 +61,7 @@ nav: - Private calls: server/user-guide/private-calls.md - Voice, announcements, and TTS: server/user-guide/voice-and-tts.md - Monitoring and reports: server/user-guide/monitoring.md + - Report proxy (legacy dashboards): server/user-guide/report-proxy.md - Hotspot proxy (integrated): server/user-guide/hotspot-proxy.md - Echo (playback): server/user-guide/echo.md - Credits & license: server/user-guide/attribution.md @@ -66,6 +72,8 @@ nav: - Report protocol v2 (JSON): server/protocols/report-v2.md - Development: - Architecture: server/development/architecture.md + - 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 - Testing: server/development/testing.md - Contributing: diff --git a/pyproject.toml b/pyproject.toml index b3b2770..ce686db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "adn-server" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" description = "ADN DMR Peer Server" readme = "README.md" license = { text = "GPL-3.0-or-later" } @@ -24,7 +24,7 @@ dependencies = [ mqtt = ["paho-mqtt>=2.0"] selfservice = ["mysqlclient>=2.0"] dev = ["pytest>=7.0", "jsonschema>=4.0", "paho-mqtt>=2.0"] -docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "pymdown-extensions>=10.3"] +docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "pymdown-extensions>=10.3", "mkdocs-mermaid2-plugin>=1.1"] [tool.setuptools.packages.find] where = ["src"] diff --git a/src/adn_server/__init__.py b/src/adn_server/__init__.py index 4411afb..ca855fa 100644 --- a/src/adn_server/__init__.py +++ b/src/adn_server/__init__.py @@ -37,4 +37,4 @@ """ADN DMR Peer Server — conference bridge (rewrite of bridge_master).""" -__version__ = "2.0.0-rc.3" +__version__ = "2.0.0-rc.4" diff --git a/src/adn_server/application/dynamic_tg_use_cases.py b/src/adn_server/application/dynamic_tg_use_cases.py index 324aac4..62213f4 100644 --- a/src/adn_server/application/dynamic_tg_use_cases.py +++ b/src/adn_server/application/dynamic_tg_use_cases.py @@ -113,7 +113,7 @@ class DynamicTgUseCases: ] tgids = restore_peer_ua_entries_to_memory(sys_cfg, peer_id, active, now=now) if self._on_restored is not None: - self._on_restored(peer_id, system_name, sys_cfg, active, now) + self._on_restored(peer_id, system_name, sys_cfg, active, now=now) if tgids: logger.info( "(DYNAMIC_TG) Restored %s TG(s) for peer %s on %s: %s", diff --git a/src/adn_server/application/report/monitor_topology.py b/src/adn_server/application/report/monitor_topology.py index 35a9beb..7f8e437 100644 --- a/src/adn_server/application/report/monitor_topology.py +++ b/src/adn_server/application/report/monitor_topology.py @@ -31,7 +31,7 @@ from __future__ import annotations import copy from typing import Any -from adn_server.application.routing.helpers import peer_should_receive_group_voice +from adn_server.application.routing.helpers import is_special_tg, peer_should_receive_group_voice from adn_server.application.proxy.deployment import is_proxy_inject_only, proxy_target_system from adn_server.domain.value_objects import bytes_4, int_id @@ -246,7 +246,12 @@ def _peers_receiving_tgid( def _echo_tx_target_peer(parts: list[str], peers: dict[Any, Any]) -> bytes | None: - """Echo/static downlink: field 5 is 9990 and field 6 resolves one hotspot.""" + """Echo/service downlink TX: field 5 may be 9990 or hotspot id; field 8 is 9990–9999.""" + tgid_slot = _voice_event_tgid_slot(parts) + if tgid_slot is not None: + tgid, _ = tgid_slot + if is_special_tg(str(tgid)): + return _peer_key_from_voice_csv(parts, peers) if len(parts) <= 5: return None try: @@ -320,13 +325,19 @@ def remap_inject_proxy_voice_events( trx = parts[2].strip() if len(parts) > 2 else "" if trx == "TX": + tgid_slot = _voice_event_tgid_slot(parts) echo_peer = _echo_tx_target_peer(parts, peers) if echo_peer is not None: slot = slot_map.get(echo_peer) if slot is not None: + tx_parts = list(parts) + if tgid_slot is not None: + tgid, _ = tgid_slot + if is_special_tg(str(tgid)): + tx_parts[5] = str(tgid) return [ _remap_voice_event_to_slot( - parts, target=target, slot=slot, peer_key=echo_peer + tx_parts, target=target, slot=slot, peer_key=echo_peer ) ] try: diff --git a/src/adn_server/application/routing/helpers.py b/src/adn_server/application/routing/helpers.py index 0f87294..ec24fd0 100644 --- a/src/adn_server/application/routing/helpers.py +++ b/src/adn_server/application/routing/helpers.py @@ -104,8 +104,14 @@ def tg4000_reset_on_vhead(int_dst_id: int, frame_type: int, dtype_vseq: int) -> def is_ua_session_tgid(tgid: int) -> bool: - """True when a keyed TG may be stored as a user-activated dynamic session.""" - return int(tgid) > 0 and int(tgid) != 4000 + """True when a keyed TG may be stored as a user-activated dynamic session. + + Excludes TG 4000 (reset command) and service/echo 9990–9999 (no SINGLE lock). + """ + t = int(tgid) + if t <= 0 or t == 4000: + return False + return not is_special_tg(str(t)) def obp_target_bcsq_quenches_stream( diff --git a/src/adn_server/application/routing_use_cases.py b/src/adn_server/application/routing_use_cases.py index 906bc4c..14455f6 100644 --- a/src/adn_server/application/routing_use_cases.py +++ b/src/adn_server/application/routing_use_cases.py @@ -417,6 +417,11 @@ class RoutingUseCases( bridge_match_slot=bridge_match_slot, dst_int=dst_int, ) + _tx_report_peer = int_id(peer_id) + if not source_is_obp: + _tx_report_peer = int_id( + resolve_voice_peer_id(peer_id, rf_src, system_name, systems_cfg) + ) forwarded = [] _leg_iter: list[tuple[str, dict[str, Any]]] = [ ( @@ -496,7 +501,7 @@ class RoutingUseCases( ) self._send_routing_event( "GROUP VOICE,START,TX,{},{},{},{},{},{}".format( - entry["SYSTEM"], int_id(stream_id), int_id(peer_id), int_id(rf_src), entry.get("TS", 1), int_id(target_tgid) + entry["SYSTEM"], int_id(stream_id), _tx_report_peer, int_id(rf_src), entry.get("TS", 1), int_id(target_tgid) ) ) if "EMB_LC" not in _target_status[stream_id]: @@ -533,7 +538,7 @@ class RoutingUseCases( call_duration = pkt_time - _target_status[stream_id].get("START", pkt_time) self._send_routing_event( "GROUP VOICE,END,TX,{},{},{},{},{},{},{:.2f}".format( - entry["SYSTEM"], int_id(stream_id), int_id(peer_id), int_id(rf_src), entry.get("TS", 1), int_id(target_tgid), call_duration + entry["SYSTEM"], int_id(stream_id), _tx_report_peer, int_id(rf_src), entry.get("TS", 1), int_id(target_tgid), call_duration ) ) elif dtype_vseq in (1, 2, 3, 4): @@ -624,7 +629,7 @@ class RoutingUseCases( "GROUP VOICE,START,TX,{},{},{},{},{},{}".format( entry["SYSTEM"], int_id(stream_id), - int_id(peer_id), + _tx_report_peer, int_id(rf_src), entry_ts, int_id(entry_tgid_b), @@ -648,11 +653,18 @@ class RoutingUseCases( dmrbits = _ts_st["TX_T_LC"][0:98] + dmrbits[98:166] + _ts_st["TX_T_LC"][98:197] call_duration = pkt_time - _ts_st.get("TX_START", pkt_time) _end_peer = _ts_st.get("TX_PEER", peer_id) + _end_report_peer = int_id(_end_peer) + if not source_is_obp: + _end_report_peer = int_id( + resolve_voice_peer_id( + _end_peer, rf_src, system_name, systems_cfg + ) + ) self._send_routing_event( "GROUP VOICE,END,TX,{},{},{},{},{},{},{:.2f}".format( entry["SYSTEM"], int_id(stream_id), - int_id(_end_peer), + _end_report_peer, int_id(rf_src), entry_ts, int_id(entry_tgid_b), diff --git a/tests/application/test_dynamic_tg_persist.py b/tests/application/test_dynamic_tg_persist.py index 09e191e..989dfce 100644 --- a/tests/application/test_dynamic_tg_persist.py +++ b/tests/application/test_dynamic_tg_persist.py @@ -26,6 +26,7 @@ from adn_server.application.dynamic_tg_use_cases import DynamicTgUseCases from adn_server.application.routing.helpers import register_peer_ua_session from adn_server.domain.dynamic_tg import DynamicTgEntry from tests.application.test_peer_single_downlink import _peer_id, _sys_cfg +from twisted.internet import defer class _CaptureStore: @@ -66,3 +67,48 @@ def test_persist_after_register_stores_absolute_expires_from_memory() -> None: entry = store.replaced[0] assert entry.expires_at == now + 60.0 * 60.0 assert entry.single_mode is True + + +def test_restore_peer_passes_now_as_keyword_to_on_restored() -> None: + """sync_restored_dynamic_tgs declares ``now`` keyword-only; restore must match.""" + import time + + peer_id = _peer_id() + sys_cfg = _sys_cfg() + now = time.time() + entry = DynamicTgEntry( + int_id=730039101, + system_name="MASTER-A", + slot=2, + tgid=7304, + single_mode=True, + expires_at=now + 3600.0, + updated_at=now, + ) + seen: dict[str, float] = {} + + def on_restored( + _peer_id: bytes, + _system_name: str, + _sys_cfg: dict, + entries: list[DynamicTgEntry], + *, + now: float, + ) -> None: + seen["now"] = now + seen["count"] = float(len(entries)) + + class _Store: + def load_peer(self, int_id: int, system_name: str): + return defer.succeed([entry]) + + def purge_expired(self, now: float) -> None: + pass + + uc = DynamicTgUseCases(_Store(), on_restored=on_restored) + out: list[list[int]] = [] + d = uc.restore_peer(peer_id, "MASTER-A", sys_cfg) + d.addCallback(lambda tgids: out.append(tgids)) + assert out == [[7304]] + assert seen["count"] == 1.0 + assert isinstance(seen.get("now"), float) diff --git a/tests/application/test_monitor_topology.py b/tests/application/test_monitor_topology.py index 3770174..74061b1 100644 --- a/tests/application/test_monitor_topology.py +++ b/tests/application/test_monitor_topology.py @@ -142,6 +142,13 @@ def test_expand_inject_proxy_emits_all_virtual_masters() -> None: {"parts": {3: "SYSTEM-4", 5: "9990"}}, id="tx_echo_keeps_9990_for_hotspot_rx", ), + pytest.param( + "GROUP VOICE,START,TX,SYSTEM,4100887026,730039101,730039101,2,9990", + [(730039101,)], + {730039101: 4}, + {"parts": {3: "SYSTEM-4", 5: "9990"}}, + id="tx_echo_peer_id_in_field5_dst_9990", + ), pytest.param( "GROUP VOICE,START,RX,SYSTEM,4100887026,73003,7300392,2,9990", [(730039101,)], diff --git a/tests/application/test_peer_single_downlink.py b/tests/application/test_peer_single_downlink.py index 7de9885..6d24376 100644 --- a/tests/application/test_peer_single_downlink.py +++ b/tests/application/test_peer_single_downlink.py @@ -388,6 +388,27 @@ def test_register_peer_ua_session_ignores_4000() -> None: assert peer_single_exclusive_tgid(peer_single, 2, sys_cfg, peer_id=peer_id, now=now) is None +def test_register_peer_ua_session_ignores_echo_9990() -> None: + """Echo TG 9990 must not create SINGLE=1 exclusive listen lock.""" + peer_id = _peer_id() + sys_cfg = _sys_cfg() + now = 1_000_000.0 + peer = {"OPTIONS": b"TS2=730444;SINGLE=1;TIMER=5;"} + register_peer_ua_session(peer, peer_id, 2, 730444, sys_cfg, now=now) + register_peer_ua_session(peer, peer_id, 2, 9990, sys_cfg, now=now) + assert peer_single_exclusive_tgid(peer, 2, sys_cfg, peer_id=peer_id, now=now) == 730444 + + +def test_register_peer_ua_session_ignores_service_999x() -> None: + """On-demand / service 9991–9999 are not UA sessions (same class as echo 9990).""" + peer_id = _peer_id() + sys_cfg = _sys_cfg() + now = 1_000_000.0 + peer = {"OPTIONS": b"TS2=730444;SINGLE=1;TIMER=5;"} + register_peer_ua_session(peer, peer_id, 2, 9999, sys_cfg, now=now) + assert peer_single_exclusive_tgid(peer, 2, sys_cfg, peer_id=peer_id, now=now) is None + + def test_new_tx_replaces_single_session_tg() -> None: peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"} sys_cfg = _sys_cfg()