docs: add server and monitor documentation

pull/4/head
Rodrigo Pérez 6 months ago
parent d94bc0965e
commit 4d7a8c136c

8
.gitignore vendored

@ -32,8 +32,12 @@ data/*.bak
!data/.gitkeep !data/.gitkeep
json/* json/*
!json/.gitkeep !json/.gitkeep
docs/*
!docs/PARROT.md # Internal
docs-priv/
# MkDocs build output (site/en, site/es)
site/
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Python # Python

@ -1,6 +1,6 @@
# ADN DMR Peer Server # ADN DMR Peer Server
Clean Architecture rewrite of the ADN DMR conference bridge. Same behaviour as the original server; configuration is YAML. ADN DMR conference bridge server. Configuration is YAML; the codebase follows clean architecture (domain, application, infrastructure).
## License ## License
@ -25,7 +25,21 @@ Voice features (announcements, TTS, recording) use a separate config file. Copy
- **Each item** (ANNOUNCEMENTS, TTS_ANNOUNCEMENTS) has its own `LANGUAGE` and `ENABLED: true` to activate. - **Each item** (ANNOUNCEMENTS, TTS_ANNOUNCEMENTS) has its own `LANGUAGE` and `ENABLED: true` to activate.
- **ANNOUNCEMENT_LANGUAGES** is optional (for voice ident only); announcements/TTS work without it. - **ANNOUNCEMENT_LANGUAGES** is optional (for voice ident only); announcements/TTS work without it.
- **TTS** requires ffmpeg + vocoder (TTS_VOCODER_CMD or TTS_AMBESERVER_HOST). Pipeline: `.txt` → gTTS → `.mp3` → ffmpeg → `.wav` → vocoder → `.ambe`. First time: create `Audio/<LANG>/ondemand/<FILE>.txt` with the text. See [docs/TTS_SETUP.md](docs/TTS_SETUP.md). - **TTS** requires ffmpeg + vocoder (TTS_VOCODER_CMD or TTS_AMBESERVER_HOST). Pipeline: `.txt` → gTTS → `.mp3` → ffmpeg → `.wav` → vocoder → `.ambe`. First time: create `Audio/<LANG>/ondemand/<FILE>.txt` with the text. See [Voice, announcements, and TTS](docs/en/server/user-guide/voice-and-tts.md).
## Documentation (MkDocs)
Documentation (MkDocs): English under **`docs/en/`**, Spanish under **`docs/es/`** (`server/` and `monitor/` in each). **Install the doc stack first** (includes **Material**); otherwise you may see `Unrecognised theme name: 'material'` if `mkdocs` on your `PATH` is not the same environment.
```bash
python3 -m pip install -r requirements-docs.txt
python3 -m mkdocs build -f mkdocs.yml # → site/en/
python3 -m mkdocs build -f mkdocs.es.yml # → site/es/
```
Use the same `python3` you use for the project (e.g. pyenv’s `3.11.8`). Preview the combined tree: `cd site && python3 -m http.server` then open **`/en/`** and **`/es/`**, or run `python3 -m mkdocs serve -f mkdocs.yml` for English only.
Output: **`site/en/`** and **`site/es/`** under gitignored **`site/`**.
## Run ## Run
@ -50,4 +64,4 @@ cp adn-parrot.example.yaml adn-parrot.yaml
python adn-parrot.py python adn-parrot.py
``` ```
See [docs/PARROT.md](docs/PARROT.md) for architecture, configuration and systemd setup. See [Parrot (playback)](docs/en/server/user-guide/parrot.md) in the docs site for an overview; extended notes may exist in private `docs-priv/` checkouts.

@ -131,6 +131,7 @@ SYSTEMS:
PROXY_CONTROL: false PROXY_CONTROL: false
OVERRIDE_IDENT_TG: "" OVERRIDE_IDENT_TG: ""
# OpenBridge: use DMRE embedded version 5 ("OpenBridge v5") + ENHANCED_OBP on all ADN Systems peers.
OBP-TEST: OBP-TEST:
MODE: OPENBRIDGE MODE: OPENBRIDGE
ENABLED: false ENABLED: false
@ -144,5 +145,5 @@ SYSTEMS:
SUB_ACL: DENY:1 SUB_ACL: DENY:1
TGID_ACL: DENY:0-82,92-199,800-899,9990-9999,730999 TGID_ACL: DENY:0-82,92-199,800-899,9990-9999,730999
RELAX_CHECKS: true RELAX_CHECKS: true
ENHANCED_OBP: true ENHANCED_OBP: true # BCSQ/BCKA + loop control — recommended true for ADN mesh
PROTO_VER: 5 PROTO_VER: 5 # DMRE v5 (89-byte BLAKE2b); match remote peer

@ -56,7 +56,7 @@ VOICE:
# The system encodes it to .ambe (via AMBEServer or vocoder) and plays it. # The system encodes it to .ambe (via AMBEServer or vocoder) and plays it.
# Later: if .ambe exists and is newer than .txt, uses cached .ambe directly. # Later: if .ambe exists and is newer than .txt, uses cached .ambe directly.
# ENABLED: true to activate; requires TTS_VOCODER_CMD or TTS_AMBESERVER_HOST # ENABLED: true to activate; requires TTS_VOCODER_CMD or TTS_AMBESERVER_HOST
# See docs/TTS_SETUP.md for full setup and troubleshooting. # See docs/en/server/user-guide/voice-and-tts.md for the TTS pipeline and troubleshooting.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# External vocoder command ({wav} and {ambe} are replaced at runtime). # External vocoder command ({wav} and {ambe} are replaced at runtime).

@ -1,135 +0,0 @@
# ADN Parrot (Playback)
Exact port of the legacy `playback.py` to clean architecture.
Records incoming group voice calls and plays them back to the sender.
## Architecture
```
Hotspot ──► adn-server (MASTER :56400)
│
├── bridge routes TG 9990 to ECHO
│
└── ECHO (PEER :54917) ──► adn-parrot (MASTER :54915)
│
├── records DMRD packets
├── waits 2 s
└── plays back with new stream ID
```
The parrot runs a **MASTER** system. The `ECHO` **PEER** (defined in `adn-server.yaml`)
connects to it. When a user transmits to TG 9990, the bridge forwards the call to
the ECHO peer, which relays it to the parrot master. The parrot records all packets,
then replays them back through the same path.
## Files
| File | Description |
|---|---|
| `adn-parrot.py` | Launcher script (like `adn-server.py`) |
| `adn-parrot.example.yaml` | Example config (copy to `adn-parrot.yaml`) |
| `src/adn_server/parrot_main.py` | Entrypoint: config loading, protocol setup, reactor |
| `src/adn_server/application/playback_use_cases.py` | Recording/playback logic (port of `playback.py`) |
## Configuration
Copy the example and set your passphrase:
```bash
cp adn-parrot.example.yaml adn-parrot.yaml
```
The config defines a single MASTER system (`PARROT`) that listens on `127.0.0.1:54915`.
The passphrase must match the one used by the ECHO peer in `adn-server.yaml`.
Key settings:
```yaml
SYSTEMS:
PARROT:
MODE: MASTER
PORT: 54915 # must match ECHO.MASTER_PORT in adn-server.yaml
PASSPHRASE: passw0rd # must match ECHO.PASSPHRASE in adn-server.yaml
MAX_PEERS: 1
ALLOW_UNREG_ID: true
```
## Running
```bash
# Direct
python adn-parrot.py
python adn-parrot.py -c /path/to/adn-parrot.yaml
python adn-parrot.py --logging DEBUG
# systemd
sudo systemctl start adn-parrot
sudo systemctl enable adn-parrot
```
### systemd service
Create `/etc/systemd/system/adn-parrot.service`:
```ini
[Unit]
Description=ADN DMR Parrot (playback)
After=multi-user.target adn-server.service
[Service]
User=root
Type=simple
Restart=always
RestartSec=3
SyslogIdentifier=adn-parrot
WorkingDirectory=/opt/new-adn-server
ExecStart=/usr/bin/python3 /opt/adn-server/adn-parrot.py -c /opt/adn-server/adn-parrot.yaml
[Install]
WantedBy=multi-user.target
```
Then:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now adn-parrot
```
## Playback flow
1. ECHO peer receives DMRD from master, forwards to parrot
2. Parrot detects new `stream_id` → starts recording
3. Parrot receives voice terminator → stops recording
4. Waits 2 seconds
5. Generates new `stream_id`, replays all packets with `sleep(0.06)` between each
6. ECHO peer receives playback, master repeats to hotspot
## Compatibility
The parrot uses the standard HBP protocol. Any legacy `playback.py` instance running
on another server can connect to our MASTER systems, and our ECHO peer can connect to
any legacy parrot master. The protocol is identical.
## Logs
Default log file: `/var/log/adn-server/parrot.log`
Successful startup looks like:
```
INFO ADN Parrot -- SYSTEM STARTING...
DEBUG MASTER instance created: PARROT, <HBPProtocol ...>
INFO (PARROT) Repeater Logging in with Radio ID: 9990, 127.0.0.1:54917
INFO (PARROT) Peer 9990 has completed the login exchange successfully
INFO (PARROT) Peer b'ECHO ' (9990) has sent repeater configuration
```
Recording/playback:
```
INFO (PARROT) *START RECORDING* STREAM ID: 12345 SUB: 7301001 TGID 9990, TS 2
INFO (PARROT) *END RECORDING* STREAM ID: 12345
INFO (PARROT) *START PLAYBACK* STREAM ID: 67890 Duration: 3.2
INFO (PARROT) *END PLAYBACK* STREAM ID: 67890
```

@ -0,0 +1,10 @@
# Origen de la documentación
El Markdown publicado con **MkDocs** está en **`en/`** (inglés) y **`es/`** (español). La **entrada por idioma** para GitHub y el sitio generado es **`docs/en/README.md`** o **`docs/es/README.md`**.
| Idioma | `docs_dir` | Salida típica | Comando |
|--------|------------|---------------|---------|
| Inglés | `docs/en` | `site/en/` | `mkdocs build` (usa `mkdocs.yml`) |
| Español | `docs/es` | `site/es/` | `mkdocs build -f mkdocs.es.yml` |
- **Estructura:** `en/server/` y `es/server/` — peer server; `en/monitor/` y `es/monitor/` — ADN Monitor. Las rutas relativas bajo cada locale deben coincidir para facilitar traducciones. Más detalle en [Traducciones](en/server/contributing/translations.md) (EN) / [Traducciones](es/server/contributing/translations.md) (ES).

@ -1,76 +0,0 @@
# TTS (Text-to-Speech) Setup
TTS announcements convert text files to AMBE and play them on a talkgroup at intervals or hourly.
## Pipeline
```
.txt → gTTS → .mp3 → ffmpeg → .wav (8kHz mono) → vocoder/AMBEServer → .ambe → broadcast
```
## First-time setup
1. **Create the text file** in `Audio/<LANGUAGE>/ondemand/<FILE>.txt`
Example for `FILE: texto1` and `LANGUAGE: es_ES`:
```
Audio/es_ES/ondemand/texto1.txt
```
Put the text you want spoken in that file (UTF-8).
2. **Configure encoding** — choose one:
- **Physical AMBE (DV3000)**: set `TTS_AMBESERVER_HOST` to the host where [AMBEServer](https://github.com/marrold/AMBEServer) runs (connected to the DV3000).
- **Software vocoder**: set `TTS_VOCODER_CMD` with a command like `/usr/local/bin/md380-vocoder -e {wav} {ambe}`.
3. **Enable the TTS item** in `TTS_ANNOUNCEMENTS`:
```yaml
TTS_ANNOUNCEMENTS:
- ENABLED: true
FILE: texto1
TG: 2
MODE: interval
INTERVAL: 60
LANGUAGE: es_ES
```
## How it works
- **First run**: The system reads `texto1.txt`, generates speech (gTTS), converts to WAV, encodes to AMBE via the configured vocoder/AMBEServer, saves `texto1.ambe`, and broadcasts it.
- **Later runs**: If `texto1.ambe` exists and is newer than `texto1.txt`, it uses the cached AMBE file directly (no conversion).
- **Update text**: Edit `texto1.txt` and save. On the next interval, the system will regenerate `texto1.ambe` because the .txt is newer.
## Physical AMBE (AMBEServer)
When using a DV3000 or similar hardware via AMBEServer:
1. Run AMBEServer on a host with the DV3000 connected (USB/serial).
2. In `adn-voice.yaml`:
```yaml
TTS_AMBESERVER_HOST: "192.168.1.10" # host where AMBEServer runs
TTS_AMBESERVER_PORT: 2460 # default
```
3. Ensure the ADN server can reach that host:port (UDP).
### Troubleshooting physical AMBE
| Symptom | Check |
|--------|-------|
| "Text file not found" | Create `Audio/<LANG>/ondemand/<FILE>.txt` (e.g. `Audio/es_ES/ondemand/texto1.txt`) |
| "AMBEServer failed, trying external vocoder..." | AMBEServer unreachable or timeout. Verify host/port, firewall, AMBEServer running. |
| "Could not encode to AMBE" | Neither AMBEServer nor TTS_VOCODER_CMD worked. Set one of them. |
| "Timeout connecting to AMBEServer" | Network/firewall; AMBEServer not listening; DV3000 not connected. |
| No audio plays | Check logs for "(TTS) Playing TTS file" — if present, broadcast succeeded; check TG and bridge targets. |
### Log messages
- `(TTS) Converting text to AMBE: ...` — first-time conversion in progress
- `(TTS) Using AMBEServer host:port` — using physical AMBE
- `(TTS) Using cached AMBE` — .ambe exists and is newer than .txt
- `(TTS) AMBEServer failed, trying external vocoder...` — AMBEServer failed; fallback to TTS_VOCODER_CMD if set
## Dependencies
- **gTTS**: `pip install gTTS`
- **ffmpeg**: system package (`apt install ffmpeg` etc.)
- **AMBEServer** (physical AMBE): [marrold/AMBEServer](https://github.com/marrold/AMBEServer) on a host with DV3000

@ -0,0 +1,60 @@
# ADN Systems documentation
This site covers **ADN DMR Peer Server** and **ADN Monitor** as one operational stack. Content is organized by **product** (`server/` vs `monitor/`) under the active **locale**.
- **English:** [`docs/en/`](README.md) (this tree) — build with `mkdocs.yml` → `site/en/`.
- **Spanish:** same content under `docs/es/` — `mkdocs build -f mkdocs.es.yml` → `site/es/` (see `docs/es/README.md` in the repository).
## ADN DMR Peer Server
The **ADN DMR Peer Server** is a [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.html) conference bridge for digital mobile radio (DMR). It is structured in **clean architecture** layers (domain, application, infrastructure).
### What the server does
- Terminates **HBP** (HomeBrew Protocol) links to **MASTER** and **PEER** systems (hotspots, repeaters).
- Terminates **OpenBridge** links to other servers over UDP — **DMRE v5** (recommended on ADN) or legacy **DMRD** v1.
- Runs **bridge routing** (`BRIDGES`): forwards group voice, loop control, ACLs, optional **BCSQ** / **BCKA**.
- Supports **private calls** (`SUB_MAP`), **voice**, **TTS**, **recording**, and **TCP reporting** to the monitor.
### Where to start (server)
| I want to… | Start here |
|------------|------------|
| Run and configure | [Introduction](server/user-guide/introduction.md), [Configuration](server/user-guide/configuration.md) |
| 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) |
| 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) |
| Credits, license, lineage | [Credits & license](server/user-guide/attribution.md) |
### Quick start (server)
```bash
pip install -r requirements.txt
cp adn-server.example.yaml adn-server.yaml
python adn-server.py -c adn-server.yaml
```
More: [Introduction](server/user-guide/introduction.md).
---
## ADN Monitor
Dashboard, WebSocket live view, optional **PHP API**, **MySQL** self-service, and **hotspot proxy** — see [Monitor overview](monitor/index.md).
| I want to… | Start here |
|------------|------------|
| `adn-mon.yaml` and layout | [Monitor configuration](monitor/configuration.md) |
| Hotspot proxy (UDP, `PROXY`, peer port range) | [Hotspot proxy](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) |
---
## Locales
- **English** — **`docs/en/`** (this build).
- **Spanish** — **`docs/es/`**.

@ -0,0 +1,66 @@
# Architecture and deployment
## Clean architecture (Python monitor)
Under `monitor/src/adn_monitor/`:
- **Domain** — value objects, errors, opcode types.
- **Application** — `MonitorState`, `process_message` in `monitor_controller.py`, alias service, Last Heard / TG count use cases, time formatting.
- **Infrastructure** — YAML `load_config`, Twisted **TCP client** (`ReportClientFactory`) to the peer server, **WebSocket** factory for the dashboard, MySQL repositories, pickle/json decoders for `CONFIG_SND` / `BRIDGE_SND`.
The monitor **connects outbound** to **`ADN_CONNECTION.ADN_IP:ADN_PORT`** and receives length-prefixed (netstring-style) messages. It updates in-memory **CTABLE** (masters/peers/OpenBridge) and **BTABLE** (bridges), and persists **BRDG_EVENT** outcomes when MySQL is configured.
## Report protocol (from the peer server)
Same opcodes as documented for the server: **CONFIG_SND**, **BRIDGE_SND**, **BRDG_EVENT**, etc. The monitor decodes and applies them in `process_message` — see [Monitoring and reports](../server/user-guide/monitoring.md).
## WebSocket
`monitor.py` runs a Twisted **WebSocket** on **`WEBSOCKET_SERVER.WEBSOCKET_PORT`**, pushing JSON snapshots at **`FREQUENCY`** so the React app updates without polling for core state.
## PHP backend
- **Slim 4** front controller: `backend/public/index.php`.
- Loads **`adn-mon.yaml`** via **`ADN_CONFIG_PATH`** (same as monitor).
- **`/api/config/dashboard`** — title, language, feature flags (`selfService`, `showConsole`, …) from **`DASHBOARD`**.
- **`/api/auth/*`** — session cookie auth when **SELF_SERVICE** DB is available.
- **`/api/self-service/*`** — device options (see [Self-service](self-service.md)).
- **`/api/aliases/*`** — optional proxy to TG/bridge list URLs from **ALIASES**.
## Frontend
- **Vite + React** under `frontend/`; build produces static assets served by nginx/Apache or similar.
- Uses **`API_BASE`** (build-time) to reach the PHP API and **WebSocket URL** for live data.
## Hotspot proxy
- Entry: `proxy/proxy.py`; package `src/adn_proxy/` (domain / application / infrastructure).
- Reads **`PROXY`** and **`SELF_SERVICE`** from the same YAML.
- For each hotspot client, allocates a port in **`DESTPORT_START`…`DEST_PORT_END`** and forwards UDP to **`MASTER`**.
- When **self-service** updates **`Clients.options`** and sets **`modified=1`**, the proxy sends **RPTO** to the **master** on a timer (~10 s). The **peer server** then applies options to the hotspot path (see [Self-service](self-service.md)).
**Why it is not part of the peer server binary:** it shares **`adn-mon.yaml`**, MySQL self-service, and deployment with the monitor stack — see [Why it ships with the monitor](hotspot-proxy.md#why-it-ships-with-the-monitor-not-inside-the-peer-server).
**Details:** [Hotspot proxy](hotspot-proxy.md) (config keys, peer server port range, startup).
## Typical deployment topology
```text
[Hotspots] --UDP--> [Proxy :LISTEN_PORT] --UDP--> [Peer server :DESTPORT range]
|
v
MySQL (Clients)
[Peer server :REPORT_PORT] <--- TCP --- [monitor.py : connects as client]
[Browser] --HTTPS--> [PHP API + static frontend]
[Browser] --WS----> [monitor WebSocket :9000]
```
---
## See also
- [Documentation home](../README.md)
- [Configuration](configuration.md)
- [Self-service](self-service.md)

@ -0,0 +1,120 @@
# Configuration (`adn-mon.yaml`)
All components read the **same** YAML (default path often `monitor/adn-mon.yaml`; override with **`ADN_CONFIG_PATH`**). The example shipped in the **adn-monitor** repo is the authoritative template; keys below match `monitor/adn-mon.yaml` and `monitor/src/adn_monitor/infrastructure/config_loader.py` (internal names may differ).
---
## `GLOBAL`
| Key | Meaning |
|-----|---------|
| **BRIDGES_INC** | Show bridge status in the dashboard when `true`. |
| **HOMEBREW_INC** | Include Homebrew (HBP) peer/master status. |
| **LASTHEARD_INC** | Enable Last Heard features / tables. |
| **LASTHEARD_ROWS** | Row count for Last Heard widgets. |
| **EMPTY_MASTERS** | Whether to show masters with no peers. |
| **TGCOUNT_INC** | Enable TG count page / stats. |
| **TGCOUNT_ROWS** | Rows for TG count display. |
| **TIMEZONE** | IANA timezone name (e.g. `America/Santiago`) for display; empty uses server local time. |
---
## `ADN_CONNECTION`
Must match the **ADN DMR Peer Server** reporting configuration.
| Key | Meaning |
|-----|---------|
| **ADN_IP** | Host/IP where the **peer server’s report TCP listener** is bound (from the monitor’s network view). |
| **ADN_PORT** | TCP port — must equal **`REPORTS.REPORT_PORT`** on the server and be reachable. |
---
## `SELF_SERVICE`
MySQL credentials and PBKDF2 parameters for **login** and **`Clients`** table access. **PBKDF2_SALT** and **PBKDF2_ITERATIONS** must match **`hotspot_proxy_self_service.py`** (or your password-registration tool) so stored password hashes verify in PHP and Python.
| Key | Meaning |
|-----|---------|
| **USE_SELFSERVICE** | Used by the **proxy** config loader to enable DB-backed options / self-service paths (see proxy README). |
| **DB_SERVER**, **DB_USERNAME**, **DB_PASSWORD**, **DB_NAME**, **DB_PORT** | MySQL connection for **`Clients`** (and related) tables. |
If the PHP backend cannot connect, **auth** and **self-service** API routes are not registered (see `backend/public/index.php`).
---
## `PROXY`
Hotspot **UDP proxy** — full guide: [Hotspot proxy](hotspot-proxy.md). Summary: forwards each client to **`MASTER:DESTPORT_START`…`DEST_PORT_END`**; the peer server must **listen** on that IP and port range (see also [Architecture](architecture.md)).
| Key | Meaning |
|-----|---------|
| **MASTER** | Peer server host (IP or DNS; resolved at proxy startup). |
| **LISTEN_PORT** / **LISTEN_IP** | Where the proxy accepts hotspot UDP (empty IP often means all interfaces). |
| **DESTPORT_START** / **DEST_PORT_END** | One port per proxied client toward **`MASTER`**. |
| **TIMEOUT**, **STATS**, **DEBUG**, **CLIENT_INFO** | Behaviour and logging. |
| **BLACK_LIST** / **IP_BLACK_LIST** | Optional block lists. |
---
## `OPB_FILTER`
Comma-separated **network IDs** (as strings). Traffic from those OpenBridge sources can be **hidden** from certain dashboard persistence paths (see monitor controller `OPB_FILTER` handling).
---
## `ALIASES`
Similar idea to the peer server: download **peer / subscriber / TGID** JSON and optional checksums. Keys include **PATH**, **\*_FILE**, **\*_URL**, **STALE_HOURS**, **REVIEW_INTERVAL_MINUTES**, **CHECKSUM_***, **TG_LIST_URL**, **BRIDGE_LIST_URL** (backend proxy for frontend pages).
---
## `LOGGER`
| Key | Meaning |
|-----|---------|
| **LOG_PATH** | Directory for log files. |
| **LOG_FILE** | Monitor log filename (e.g. `adn-mon.log`). |
| **PROXY_LOG_FILE** | Separate log name for the proxy (when run with proxy logging). |
| **LOG_LEVEL** | e.g. `INFO`, `DEBUG`. |
---
## `WEBSOCKET_SERVER`
| Key | Meaning |
|-----|---------|
| **WEBSOCKET_PORT** | Port for Twisted WebSocket pushing JSON state to browsers. |
| **FREQUENCY** | Push interval (seconds). |
| **CLIENT_TIMEOUT** | Drop idle WS clients after N seconds (`0` = disable). |
| **USE_SSL**, **SSL_PATH**, **SSL_CERTIFICATE**, **SSL_PRIVATEKEY** | Optional WSS. |
---
## `DASHBOARD`
| Key | Meaning |
|-----|---------|
| **DASHTITLE** | Header title. |
| **BACKGROUND** | Use `bk.jpg` background if `true`. |
| **LANGUAGE** | Default UI language (`en`, `es`, …). |
| **SELF_SERVICE** | If `true`, the UI can show the **Self-service** nav entry (backend must expose API + DB). |
| **SHOW_CONSOLE** | Show console page (call start/end messages). |
| **MIN_DURATION** | Minimum call duration (seconds) for **dashboard** Last Heard table (Last Heard page may still show shorter). |
| **nav_links**, **footer**, **news** | Optional structured links / marquee items. |
---
## Environment
- **`ADN_CONFIG_PATH`**: Absolute path to `adn-mon.yaml` for monitor, backend, and proxy.
- Backend may use **`API_BASE_PATH`** if the API is mounted under a prefix.
---
## See also
- [Documentation home](../README.md)
- [Architecture](architecture.md)
- [Self-service](self-service.md)
- Peer server **`REPORTS`**: [Monitoring and reports](../server/user-guide/monitoring.md), [Server configuration](../server/user-guide/configuration.md) (section **`REPORTS`**).

@ -0,0 +1,114 @@
# Hotspot proxy
The **hotspot proxy** is part of the **adn-monitor** repository. It is a **UDP relay** between **DMR hotspots** (Homebrew / HBP) and the **ADN DMR Peer Server** MASTER: each connected hotspot is mapped to a **dedicated destination port** on the peer server host so many hotspots can share one public IP without port clashes.
Source layout: `proxy/proxy.py`, package `proxy/src/adn_proxy/` (clean architecture). **GPL v3** (derivative of Simon Adlem, G7RZU’s original proxy).
### Why it ships with the monitor (not inside the peer server)
There is no single mandatory layout for every deployment, but **today the proxy lives in the adn-monitor repo** on purpose:
- **Same config and ops** as the dashboard stack: **`adn-mon.yaml`**, **`ADN_CONFIG_PATH`**, and usually the same host as **PHP** and **MySQL**.
- **Self-service** ( **`Clients`**, RPTO, **`modified`**) is built around that ecosystem; the peer server binary does not own that database or the **`PROXY`** block.
- **Role split:** the **ADN DMR Peer Server** is the **radio core** (HBP/OpenBridge, bridges, voice, TCP reports). The hotspot proxy is an **optional UDP front** toward a MASTER that already listens on a port **range** — useful when many hotspots share one public address.
**Bundling the proxy into the peer server** (one binary, one `adn-server.yaml`) is conceivable for packaging, but it implies **merging configuration**, **rethinking self-service wiring**, and extra maintenance — only worth it if you explicitly want a single deployable “all-in-one” server.
---
## Configuration file (same as monitor)
The proxy does **not** use `adn-server.yaml`. It reads the **monitor** YAML:
| Source | Purpose |
|--------|---------|
| **`ADN_CONFIG_PATH`** | Environment variable: absolute path to **`adn-mon.yaml`** (shared with **monitor**, **PHP backend**, optional **`.env`** in repo root). |
| **`python proxy/proxy.py --config /path/to/adn-mon.yaml`** | Overrides the path for this process only. |
| **Default** (if unset) | `../monitor/adn-mon.yaml` relative to the `proxy/` directory when run from the adn-monitor tree. |
Sections used:
- **`PROXY`** — listen address, master host, destination port **range**, timeouts, debug, block lists.
- **`SELF_SERVICE`** — MySQL and **`USE_SELFSERVICE`** (for **`Clients`** table, RPTO / options).
- **`LOGGER`** — **`LOG_PATH`** and **`PROXY_LOG_FILE`** (proxy log is separate from **`LOG_FILE`** used by `monitor.py`).
Optional **environment** overrides (see `proxy/README.md` in the repo): e.g. **`ADN_PROXY_DEBUG`**, **`ADN_PROXY_LISTENPORT`**.
---
## `PROXY` keys (`adn-mon.yaml`)
| Key | Role |
|-----|------|
| **MASTER** | IP or **hostname** of the **ADN DMR Peer Server** host. Resolved to an IPv4 address at startup (Twisted requires an IP for `write()`). |
| **LISTEN_PORT** | UDP port where **hotspots** connect **to the proxy** (the address users configure on the hotspot). |
| **LISTEN_IP** | Empty often means all interfaces; otherwise bind to this address. |
| **DESTPORT_START** / **DEST_PORT_END** | Inclusive range of UDP ports on **`MASTER`** used **one per proxied hotspot** (sequential allocation inside the proxy). |
| **TIMEOUT** | Idle / session timeout (seconds). |
| **STATS** | Extra statistics logging. |
| **DEBUG** | Verbose packet logging (or use **`ADN_PROXY_DEBUG=1`**). |
| **CLIENT_INFO** | Per-client info in logs. |
| **BLACK_LIST** / **IP_BLACK_LIST** | Block radio IDs or source IPs. |
Internal config keys (after load) use mixed-case names (`Master`, `ListenPort`, …) — see `adn_proxy.infrastructure.config_loader`.
---
## Peer server (`adn-server.yaml`) must cover the port range
The proxy forwards traffic to **`MASTER:DESTPORT`** for each client, where **DESTPORT** is chosen inside **[DESTPORT_START, DEST_PORT_END]**.
The **ADN DMR Peer Server** must therefore **listen on UDP** on **that host** for **every port** in the range that you intend to use (one **MASTER** listener per port, or equivalent).
- A **single** `MODE: MASTER` with **one** `PORT` is **not** enough for multiple proxy clients if they map to different **DESTPORT** values — you need **multiple listeners** on the range.
- Typical approaches: **`GENERATOR`** on a MASTER system (splits into `NAME-0`, `NAME-1`, … with consecutive **PORT** values — see [Server configuration](../server/user-guide/configuration.md)), and/or multiple **`SYSTEMS`** entries, aligned with **`DESTPORT_START`…`DEST_PORT_END`** in **`PROXY`**.
If the server only listens on e.g. **56400** but the proxy sends to **56401**, that client will not register.
---
## How the process starts
1. Resolve config path (`ADN_CONFIG_PATH`, `--config`, or default).
2. **`load_config()`** parses YAML → **`PROXY`**, **`SELF_SERVICE`**, **`LOG`**.
3. Optional **MySQL** pool if self-service / DB features are enabled.
4. Twisted **reactor** runs UDP **ProxyProtocol** on **`LISTEN_IP:LISTEN_PORT`**, forwarding to **`MASTER:assigned_dest_port`**.
Run (from adn-monitor root, with env set):
```bash
export ADN_CONFIG_PATH=/opt/adn-monitor/monitor/adn-mon.yaml
python proxy/proxy.py
# or
python proxy/proxy.py --config /path/to/adn-mon.yaml
```
Use **systemd** or another supervisor to run alongside **`monitor.py`** and the **PHP** stack.
---
## RPTO, options, and self-service
The proxy **never** sends **RPTO** directly to the hotspot for self-service updates. It sends **RPTO to the MASTER** (peer server); the server updates bridge/options and the normal HBP path applies.
| Event | Proxy behaviour |
|-------|------------------|
| ~**10 s** after hotspot login (**RPTC**) | Read **`Clients.options`** from DB → **RPTO** → master `(MASTER, dport)`. |
| Every ~**10 s** | Rows with **`modified = 1`** → **RPTO** → master, then clear **`modified`**. |
| Hotspot sends **RPTO** | Forward to master; DB updates as implemented. |
Details: [Self-service](self-service.md) and the **adn-monitor** `proxy/README.md`.
---
## Monitor visibility
Hotspots appear on the dashboard only if the **peer server** sends **TCP reports** to the same host/port as **`ADN_CONNECTION`** in **`adn-mon.yaml`**. Align **`REPORTS`** on the server with **`ADN_IP` / `ADN_PORT`**. See [Monitoring and reports](../server/user-guide/monitoring.md).
---
## See also
- [Monitor configuration](configuration.md) — full **`adn-mon.yaml`** reference (PROXY section summary).
- [Architecture](architecture.md) — where the proxy sits in the stack.
- [Self-service](self-service.md) — DB, **`modified`**, RPTO timing.

@ -0,0 +1,40 @@
# ADN Monitor (overview)
**ADN Monitor** is a separate project from the **ADN DMR Peer Server**, but the two are normally deployed **together**: the server sends **TCP reports** (config, bridges, call events) to the monitor; the monitor drives the **web dashboard** (React) and **WebSocket** live updates. Optional components include the **PHP API** (Slim), **MySQL** (self-service / device registry), and the **hotspot proxy** (UDP between hotspots and the peer server).
This chapter documents the **adn-monitor** stack at the same level of detail as the server guides. Source code lives in the **adn-monitor** repository, not in the **adn-server** repository (where this documentation is maintained).
## What each part does
| Part | Role |
|------|------|
| **`monitor/monitor.py`** | Python (Twisted): connects to the peer server’s **report TCP** port, decodes netstring payloads (`CONFIG_SND`, `BRIDGE_SND`, `BRDG_EVENT`), maintains **CTABLE** / **BTABLE**, writes **Last Heard** / TG stats to **MySQL** when configured, serves **WebSocket** JSON to the dashboard. |
| **`backend/`** | PHP **Slim** app: `/api/config/dashboard`, auth, optional **self-service** APIs, alias proxies. Reads the **same** `adn-mon.yaml` via **`ADN_CONFIG_PATH`**. |
| **`frontend/`** | React (Vite): dashboard UI; consumes backend API + WebSocket. |
| **`proxy/`** | Python (Twisted): UDP **hotspot proxy**; forwards Homebrew between hotspots and the peer server; reads **`Clients`** in MySQL for **RPTO** options (self-service). |
## Single configuration file
**`adn-mon.yaml`** (path often set with **`ADN_CONFIG_PATH`** in `.env`) is shared by:
- Python monitor (`monitor.py`)
- PHP backend (`backend/public/index.php`)
- Hotspot proxy (`proxy/proxy.py`)
So **one** YAML drives reporting addresses, dashboard strings, WebSocket port, **SELF_SERVICE** DB credentials, and **PROXY** listen/range settings.
## Link to the peer server
| Server (`adn-server.yaml`) | Monitor (`adn-mon.yaml`) |
|----------------------------|---------------------------|
| **`REPORTS.REPORT_CLIENTS`** — list of IPs allowed to connect **to** the report listener, or the monitor host | **`ADN_CONNECTION.ADN_IP`** / **`ADN_PORT`** — where the **monitor connects** (must match the server’s report bind and port). |
| **`REPORTS.REPORT_PORT`** — TCP port the **server listens on** for incoming report connections | Same port as **`ADN_PORT`**. |
See [Monitoring and reports](../server/user-guide/monitoring.md) for report opcodes and [Monitor configuration](configuration.md) for every `adn-mon.yaml` section.
## See also
- [Hotspot proxy](hotspot-proxy.md) — `PROXY`, peer server port range, how the process loads config and runs
- [Architecture and deployment](architecture.md)
- [Configuration (`adn-mon.yaml`)](configuration.md)
- [Self-service](self-service.md)

@ -0,0 +1,77 @@
# Self-service
**Self-service** lets a hotspot owner **log in** to the dashboard, **edit their device options** (static TG lists, default reflector, timer, language, etc.), and have those options **pushed to the ADN DMR Peer Server** without manually editing YAML on the server.
It involves **four** pieces: **MySQL** (`Clients` table), **PHP API** (session + REST), **React** (`/self-service` page), and the **hotspot proxy** (periodic **RPTO** to the master). The **peer server** is the component that ultimately applies **RPTO** / **OPTIONS** to the running bridge and hotspot behaviour.
---
## Prerequisites
1. **`SELF_SERVICE`** block in **`adn-mon.yaml`** with valid **MySQL** credentials and **PBKDF2** parameters matching your password tooling (same salt/iterations as **`hotspot_proxy_self_service.py`** when used).
2. **`DASHBOARD.SELF_SERVICE: true`** so the UI shows the **Self-service** menu entry (and the backend exposes `/api/self-service/*` when DB connects).
3. **`Clients`** table populated with rows: **`callsign`**, **`int_id`** (DMR ID), **`psswd`** (PBKDF2-SHA256 hex), **`options`** (semicolon-separated `KEY=value`), **`logged_in`**, **`host`**, **`modified`**, etc. (see adn-monitor DB schema / migration scripts in the repo).
4. Hotspot traffic should pass through the **proxy** if you rely on **`modified`** and **RPTO** push (see flow below).
---
## Authentication
| Endpoint | Purpose |
|----------|---------|
| **`POST /api/auth/login`** | Body: `callsign`, `password`. Verifies **PBKDF2** hash against **`Clients.psswd`** for rows with **`logged_in = 1`**. On success: PHP session with **`user_id`**, **`int_ids`** (all DMR IDs for that callsign). |
| **`GET /api/auth/login-by-ip`** | Optional: single user match for **`Clients.host`** = client IP (same session shape). |
| **`POST /api/auth/logout`** | Clears session. |
| **`GET /api/auth/me`** | Returns `{ callsign, int_ids, selected_int_id }` for the React app. |
Session lifetime is extended on activity (**SelfServiceController** uses a long inactivity timeout).
---
## Device API (after login)
| Method | Path | Purpose |
|--------|------|---------|
| **GET** | `/api/self-service/device?int_id=` | Load **`Clients`** row for that **`int_id`** (must be in session **`int_ids`**). Returns JSON: **`int_id`**, **`callsign`**, **`mode`**, **`options`** parsed into TS1/TS2 lists, DIAL, VOICE, LANG, SINGLE, TIMER. |
| **POST** | `/api/self-service/device/options` | Body: **`int_id`**, **`options`** string (Homebrew **OPTIONS** line). Must end with **`;`**. Max length **4096**. Updates DB: **`Clients.options`**, sets **`modified = 1`**. |
| **GET** | `/api/self-service/device/modified?int_id=` | Returns **`{ modified: 0|1 }`** from **`Clients.modified`** (UI can poll until the proxy clears it). |
| **POST** | `/api/self-service/device/select` | Body: **`int_id`** — set session **`selected_int_id`** when the user has multiple devices. |
---
## End-to-end flow (why options reach the hotspot)
1. User saves options in the web UI → **PHP** 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).
4. The server pushes the appropriate signalling to the **hotspot** so static TG / reflector / timer settings take effect **without** a full hotspot restart (exact behaviour matches the HBP **OPTIONS** flow in the server).
Important: the proxy sends **RPTO to the master only**, not to the hotspot directly. If the proxy is not in the path, you must ensure another mechanism applies **OPTIONS** or you run the server without this proxy path.
---
## Password hashing
**`AuthenticateUser`** uses:
```text
hash_pbkdf2('sha256', password, PBKDF2_SALT, PBKDF2_ITERATIONS)
```
stored as hex in **`Clients.psswd`**. The same parameters must be used wherever passwords are **registered** (e.g. **hotspot_proxy_self_service.py** in the **adn-dmr-server** / tooling repo).
---
## UI
- Route **`/self-service`** in the React app (`SelfService.tsx`): loads **`/api/auth/me`**, then device details for **`selected_int_id`**, edit options string, save.
- External **SelfCare** link (e.g. `selfcare.adn.systems`) may appear in the nav as a separate product — not the same as this **local** self-service.
---
## See also
- [Documentation home](../README.md)
- [Configuration](configuration.md) — `SELF_SERVICE`, `DASHBOARD`, `PROXY`
- [Architecture](architecture.md) — proxy + report path
- Hotspot proxy details: **adn-monitor** `proxy/README.md` (RPTO timing table)

@ -0,0 +1,38 @@
# Translations
## Current layout
**MkDocs** pages exist in parallel trees:
| Locale | Path | Build |
|--------|------|-------|
| English | **`docs/en/`** | `mkdocs.yml` → **`site/en/`** |
| Spanish | **`docs/es/`** | **`mkdocs.es.yml`** → **`site/es/`** |
- **`docs/en/server/`** — ADN DMR Peer Server (user guide, protocols, development, contributing).
- **`docs/en/monitor/`** — ADN Monitor (dashboard, `adn-mon.yaml`, self-service).
Spanish mirrors the same relative paths under **`docs/es/`**.
`mkdocs.yml` sets **`docs_dir: docs/en`** and **`theme.language: en`**. **`mkdocs.es.yml`** sets **`docs_dir: docs/es`** and **`theme.language: es`**.
Build **both** (`mkdocs build` and `mkdocs build -f mkdocs.es.yml`). The outputs land in **`site/en/`** and **`site/es/`**. Publish the combined **`site/`** directory as your HTTP server layout requires.
For a quick local check: `cd site && python -m http.server` then open **`/en/`** and **`/es/`**. Optional: use **`mkdocs-static-i18n`** later for a single build with page-level language pairs.
## Adding or updating a locale
1. Keep **navigation structure** aligned across locales (same relative paths: `server/user-guide/introduction.md`, etc.).
2. Headings that are cross-linked use **`attr_list`** explicit anchors `{#id}` where slugs must stay stable — see Spanish pages for examples.
3. When adding a third locale, add another MkDocs config and output directory following the same pattern.
## Writing for translators
- Use **short, clear sentences**.
- Avoid idioms and culture-specific jokes.
- Keep **terminology** consistent (OpenBridge, BCSQ, TG, `BRIDGES`).
- Put **code identifiers** and **YAML keys** in backticks.
## Not translated by default
- The **repository root** `README.md` may stay English-only or link to the published docs site.

@ -0,0 +1,26 @@
# Architecture (clean layering)
## Layers
1. **Domain** (`src/adn_server/domain/`) — entities, value objects, errors, `Result`. No I/O, no Twisted.
2. **Application** (`src/adn_server/application/`) — use cases (`BridgeUseCases`, `VoiceUseCases`, …) and **ports** (interfaces).
3. **Infrastructure** (`src/adn_server/infrastructure/`) — YAML config, Twisted UDP/TCP, voice, persistence, security adapters.
**Dependency rule:** infrastructure → application → domain (inward only).
## Entrypoint
`main.py` wires configuration, **LoopingCall** timers, factories for **HBPProtocol**, report client, and injects use cases.
## Where to read code
| Topic | Location |
|-------|----------|
| Bridge routing, `dmrd_received`, OpenBridge loop | `application/bridge_use_cases.py` |
| HBP / OpenBridge UDP | `infrastructure/twisted_adapters/udp_hbp.py` |
| Report TCP | `infrastructure/twisted_adapters/report_server.py` (factory), bridge events from use cases |
| Voice / TTS | `application/voice_use_cases.py`, `infrastructure/voice/` |
## Configuration as shared state
A **mutable `config` dict** is passed through adapters; runtime updates (options, `SUB_MAP`, OpenBridge `_bcsq`) remain visible globally for the lifetime of the process.

@ -0,0 +1,11 @@
# Behaviour and timers
## Stable control loops
The server uses **Twisted** `LoopingCall` tasks for periodic work: bridge rules, stream trimming, OpenBridge options refresh, alias reload, voice config reload, security downloads, reporting, and maintenance pings.
Intervals are part of the **observable behaviour** of the product (operators and integrators may rely on timing for troubleshooting). Avoid adding **extra** refreshes or duplicate work inside **hot paths** (for example per-packet handlers for OpenBridge) when the same concern is already covered by the scheduled loop—this keeps load predictable and avoids double application of rules.
## Configuration visibility
Runtime state lives in a shared **`config`** dict: options from peers, `SUB_MAP`, OpenBridge control-plane fields (`_bcsq`, `_bcka`), and similar. Adapters update this structure; use cases read it. This matches how the running process is inspected in logs and support scenarios.

@ -0,0 +1,33 @@
# DMRE v5 frame layout
This page describes the **OpenBridge DMRE** wire format when the **embedded protocol version** is **5** (often called **“OpenBridge v5”** in operator docs — same as **DMRE v5**; see [OpenBridge](openbridge.md#dmre-and-openbridge-v5)).
Extended **OpenBridge** datagrams use the **`DMRE`** opcode (bytes `D`,`M`,`R`,`E`) and a **version** field. When the embedded version byte is **> 4**, the packet is **89 bytes** with **hops** at byte **72** and **BLAKE2b** from **73** to **89**.
## Short form (85 bytes)
When the embedded version is **≤ 4**, **source repeater** is omitted; **hops** and **MAC** shift (see implementation in `udp_hbp.py`).
## Field summary (89-byte v5)
| Region | Content |
|--------|---------|
| 0:4 | Opcode `DMRE` |
| 4:5 | Sequence |
| 5:8 | RF source |
| 8:11 | Destination ID |
| 11:15 | Server ID |
| 15:16 | Bits (slot, call type, frame type, dtype/vseq) |
| 16:20 | Stream ID |
| 20:53 | Voice payload |
| 53:55 | BER, RSSI |
| 55:56 | Embedded protocol version |
| 56:64 | Timestamp (ns, big-endian) |
| 64:68 | Source server ID |
| 68:72 | Source repeater (v5 extended) |
| 72:73 | Hops |
| 73:89 | BLAKE2b MAC (16 bytes) |
**Integrity:** BLAKE2b-128 with the **passphrase** as key; MAC covers bytes **before** the MAC field.
The byte layout in code (`infrastructure/twisted_adapters/udp_hbp.py`) is authoritative and may gain minor clarifications over time.

@ -0,0 +1,20 @@
# HBP (HomeBrew Protocol) — DMRD
## Role
**HBP** is the UDP framing used between this server and **MASTER** / **PEER** systems. Payloads use the **`DMRD`** opcode (four ASCII bytes) followed by DMR voice/data fields (RF source, destination, stream ID, AMBE block, etc.).
## Authentication
- **MASTER** side: RPTL → salt → RPTK → config; peer options (**RPTO**) refresh bridge options.
- **PEER** side: connects upstream, repeats authentication, maintenance pings.
Implementation: `infrastructure/twisted_adapters/udp_hbp.py` (`HBPProtocol`).
## OpenBridge vs HBP
OpenBridge uses **`DMRD`** v1 (HMAC-SHA1) or **`DMRE`** (extended); see [OpenBridge](openbridge.md). HBP **MASTER/PEER** links use classic **DMRD** rules.
## BER / RSSI
For non-OpenBridge sources, optional **BER/RSSI** bytes may be appended after the 53-byte voice payload in ingress; forwarding to OpenBridge may strip or preserve fields depending on destination and `to_target` rules.

@ -0,0 +1,70 @@
# OpenBridge (FreeBridge / DMRE)
## DMRE and “OpenBridge v5”
On the wire, extended OpenBridge uses the **`DMRE`** opcode. The **embedded protocol version** byte inside the frame (see [DMRE v5 layout](dmre-v5.md)) selects the layout: **version > 4** is the **89-byte v5** format (hops, source repeater field, BLAKE2b MAC). In documentation and operator discussions, **“DMRE v5”** and **“OpenBridge v5”** refer to the same thing: **DMRE frames with embedded version 5** (not the older short DMRD-only path).
**Recommendation (ADN Systems network):** All inter-server links that participate in the **ADN Systems** mesh should use **DMRE v5** (`PROTO_VER: 5` in YAML, which sets the negotiated **VER** / embedded version) and **`ENHANCED_OBP: true`** so **BCSQ**, **BCKA**, and multi-path loop control behave consistently. **Peers** (other servers) should be configured the same way. **DMRD v1** (HMAC-only) remains supported for interoperability with older stacks, but it is **not** the preferred mode for new ADN deployments.
## What OpenBridge is
**OpenBridge** is a UDP protocol between **servers** (and some gateways). It carries DMR voice using:
- **`DMRD`** version 1 — HMAC-SHA1 authenticated payload (legacy interop); or
- **`DMRE`** — extended frame with **BLAKE2b** MAC, embedded version, timestamps, **hops**, source server/repeater IDs, etc. (**DMRE v5** = embedded version 5, recommended above).
This stack implements the **OPENBRIDGE** peer mode in **`udp_hbp.py`** and bridge routing in **`BridgeUseCases`**.
## Ingress (DMRE)
1. Verify **BLAKE2b** over the authenticated prefix.
2. Check **NETWORK_ID**, **TARGET** socket / `RELAX_CHECKS`, **slot** (TS1 for OBP ingress).
3. Increment **hops**; if **> 10**, drop and optionally send **BCSQ**.
4. Rebuild a pseudo-**DMRD** for the bridge and call **`dmrd_received`** with hop metadata.
## Egress (`send_system`)
- Build **DMRE** or **DMRD** v1 depending on negotiated **VER** and embedded version.
- Preserve **hops**, **BER/RSSI**, **source_server** / **source_rptr** as required for interop.
## Loop control (multi-path mesh)
For **group** voice on OpenBridge:
1. **Finished** / **timeout 180 s** — drop stale streams.
2. **Echo HBP** — if a non-OBP system already has this `stream_id` in RX, this OBP path is treated as echo.
3. **Multiple OBP** — among OBP legs with the same `stream_id` and TG, **only the earliest `1ST` time** (`min(perf_counter)` ) **forwards**; others stop and may send **BCSQ** if **`ENHANCED_OBP`** is true.
## BCSQ (Bridge Control — Source Quench)
- **Meaning:** “Do not forward this **`stream_id`** on this **TG** to my leg.”
- **Sent by** the **losing** OBP in loop control to its **peer** (not a global broadcast).
- **Honoured** when **forwarding** to another OBP: if the destination’s `_bcsq` matches, **skip** `send_system`.
## BCKA (keepalive)
- **ENHANCED_OBP** — if peer keepalive is stale, block forward until refreshed.
## Bridge forwarding (`to_target`)
Per destination row: dedupe `(SYSTEM, TS)`, check **BCSQ**, **BCKA**, **ACL**, rewrite **LC** / **TGID**, force **TS1** bit pattern for OBP, call **`send_system`**.
## Ingress filters (group TG)
For **group** (and **vcsbk**) traffic, OpenBridge applies **TG filters** before traffic reaches the bridge router. Dropped streams may trigger **BCSQ** back to the peer. The exact rules differ between **DMRD v1** and **DMRE** paths in `udp_hbp.py`; in general they reject traffic that is treated as **local-only** or **wrong server** for the destination, for example:
- Low TG numbers (e.g. **≤ 79** on DMRE; DMRD v1 also combines **9990–9999**, **92–199**, **900999** in one check).
- **9990–9999** and **900999** (service / local-server ranges on DMRE).
- **92–199** unless the **source server** ID matches your server’s main ID (DMRE).
- **80–89** and **800–899** unless the **MCC** prefix matches (DMRE).
**Private (unit)** calls are not subject to the same group-TG filter block in the same way; configure **ACL** separately.
Operator-oriented summary: [Special numbers — OpenBridge ingress](../user-guide/special-numbers.md#openbridge-ingress--group-tg-filters).
## Monitor events (adn-monitor)
- **INGRESS** — debug-only first sight per leg.
- **START** — canonical **after** loop win (dashboard state).
See also: [DMRE v5 layout](dmre-v5.md), [Monitoring](../user-guide/monitoring.md).

@ -0,0 +1,18 @@
# Credits, license, and project history
## Credits and lineage
**ADN Systems DMR Peer Server** is derived from **FreeDMR**, reorganised with **clean architecture** (domain, application, infrastructure) and ADN-specific features: scheduled announcements, **TTS**, on-demand voice, **`adn-voice.yaml`** merge, TCP reporting to **ADN Monitor**, integration with **`adn-mon.yaml`**, self-service, hotspot proxy, and more.
**[FreeDMR Peer Server](https://gitlab.hacknix.net/hacknix/FreeDMR)** is by **Simon Adlem, G7RZU** ([hacknix](https://gitlab.hacknix.net/hacknix/FreeDMR)).
**[hblink3](https://github.com/HBLink-org/hblink3)** — HBLink for Python3. Copyright (C) 2016-2020 Cortney T. Buffington, N0MJS n0mjs@me.com.
## License
This project is distributed under the **GNU General Public License v3** (or later), consistent with FreeDMR and related stacks. The full legal text is in the repository (`LICENSE`, and `COPYING` where present).
## See also
- [Introduction](introduction.md)
- [Architecture](../development/architecture.md)

@ -0,0 +1,28 @@
# Bridges and talkgroups
## `BRIDGES` model
The bridge table maps **talkgroup keys** (strings, e.g. `"26811"`, `"#reflector"`) to **rows**. Each row describes:
- **`SYSTEM`** — which configured system originates or receives this leg.
- **`TS`** — timeslot (1 or 2). OpenBridge sources are normalised to **TS1** in routing (`bridge_match_slot`).
- **`TGID`** — destination ID bytes for LC rewrite toward that leg.
- **`ACTIVE`** — whether this leg participates.
- **`TIMEOUT`**, **`TO_TYPE`**, **`ON`/`OFF`/`RESET`** — activation semantics (user-activated bridges, reflectors, etc.).
The router scans `BRIDGES` for an **ACTIVE** row matching the **current source system**, **slot**, and **destination TG** before forwarding (`dmrd_received` → `to_target`).
## 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.
## OpenBridge and TG display
For OpenBridge, the **DMR destination** in the packet may differ from the **TGID** in a bridge row (remap). Monitoring may show **RX TG** (as received) vs **TX TG** (as rewritten for a destination); correlate by **`stream_id`**, not TG alone.
## Contention
Group voice uses **hang time**, **`STREAM_TO`**, and slot `TX_*` / `RX_*` state to avoid colliding transmissions on the same resources.
See also: [Special numbers](special-numbers.md), [OpenBridge protocol](../protocols/openbridge.md).

@ -0,0 +1,219 @@
# Configuration
## Files and workflow
| File | Committed | Role |
|------|-----------|------|
| `adn-server.example.yaml` | Yes | Template — copy to `adn-server.yaml` and edit. |
| `adn-server.yaml` | **No** (gitignored) | Main server: systems, globals, logging, aliases, reports. |
| `adn-voice.example.yaml` | Yes | Template for voice — copy to `adn-voice.yaml`. |
| `adn-voice.yaml` | **No** (typical) | Voice/TTS/recording; merged into `config["VOICE"]` at startup and **hot-reloaded** (~every 15 s) if the file changes. |
Run:
```bash
python adn-server.py -c /path/to/adn-server.yaml
```
Optional: `--logging LEVEL` overrides `LOGGER.LOG_LEVEL`.
If `adn-voice.yaml` sits next to `adn-server.yaml`, it is loaded automatically. You can also put a `VOICE:` block inside `adn-server.yaml`; the separate file is the usual way to change announcements without touching the main config.
**Secrets:** Never commit real passphrases, security URLs, or `user_passwords.json` / `encryption_key.secret`. Use placeholders in templates and keep production files local.
---
## Architecture: what is a “system”?
Each entry under **`SYSTEMS`** is a named **logical link** (UDP endpoint) that speaks **HBP** (HomeBrew Protocol) to hotspots/repeaters, or **OpenBridge** to other servers. Names are free-form strings (`SYSTEM`, `ECHO`, `OBP-UK`, …) and are used in logs and in the **bridge table** (`BRIDGES`) to identify where traffic enters or leaves.
Three **modes** exist:
| Mode | Typical use | Listens | Connects upstream |
|------|-------------|---------|-------------------|
| **MASTER** | Conference server for one or more hotspots/repeaters | **Yes** — `IP` / `PORT`, peers register with passphrase | No (peers connect to you) |
| **PEER** | Hotspot/repeater or service (e.g. parrot) behaving as a **client** of a MASTER | **Yes** — local `IP` / `PORT` | **Yes** — `MASTER_IP` / `MASTER_PORT` must point at a MASTER |
| **OPENBRIDGE** | Link to another **server** over OpenBridge (DMRD v1 / DMRE) | **Yes** — `IP` / `PORT` | **Yes** — `TARGET_IP` / `TARGET_PORT` (peer server) |
**MASTER** holds the **`PEERS`** table at runtime (hotspots that authenticated). **PEER** maintains **STATS** (connection, pings). **OPENBRIDGE** uses **NETWORK_ID**, **PASSPHRASE**, **TARGET_***, **PROTO_VER** / **VER**, and optional **ENHANCED_OBP**, **RELAX_CHECKS**, **TGID_ACL**.
A single process can run **several** systems at once (e.g. one MASTER for users, one ECHO for parrot, one OBP to a partner network).
---
## `GLOBAL`
Server-wide defaults. Many keys can be overridden per system if `USE_ACL` (or similar) is set on that system.
| Key | Meaning |
|-----|---------|
| **PING_TIME** | Interval (seconds) for PEER keepalive / ping logic toward MASTER. |
| **MAX_MISSED** | How many missed pings before treating the PEER link as unhealthy (depends on implementation with STATS). |
| **USE_ACL** | If true, **REG_ACL**, **SUB_ACL**, **TGID_TS1_ACL**, **TGID_TS2_ACL** are applied (after processing into internal tuples). |
| **REG_ACL** | Access control for **registration** / peer IDs (`PERMIT:…` / `DENY:…`; see [ACL strings](#acl-strings)). |
| **SUB_ACL** | ACL for **subscriber** (radio) IDs on received traffic. |
| **TGID_TS1_ACL** | ACL for **talkgroup** on **timeslot 1**. |
| **TGID_TS2_ACL** | ACL for **talkgroup** on **timeslot 2**. |
| **GEN_STAT_BRIDGES** | If true, OpenBridge can trigger creation of **static** bridge rows for certain TGs (see [Bridges and talkgroups](bridges-and-talkgroups.md)). |
| **SERVER_ID** | Numeric server ID; stored as 4-byte value for OpenBridge / voice metadata. |
| **VALIDATE_SERVER_IDS** | If true (DMRE path), **source server** IDs may be checked against a downloaded list (`ALIASES` **SERVER_ID_**\*). |
| **URL_SECURITY** / **PORT_SECURITY** / **PASS_SECURITY** | If set, enables download of keys/password material from the security endpoint (see example comments). Empty = disabled. |
| **USERS_PASS** | Filename for per-radio password JSON (optional). |
| **HASH_ENCRYPT** | Path to encryption key for password file handling. |
---
## `SYSTEMS` — common fields
These appear mainly on **MASTER** (and often on **PEER**). OpenBridge uses a different subset.
| Key | Meaning |
|-----|---------|
| **MODE** | `MASTER`, `PEER`, or `OPENBRIDGE`. |
| **ENABLED** | If `false`, the system is skipped. |
| **IP** / **PORT** | UDP bind address for this system’s HBP or OpenBridge listener. |
| **PASSPHRASE** | Shared secret for HBP authentication (MASTER ↔ PEER). Must match between a PEER and its MASTER. |
| **USE_ACL** | Per-system ACL override when true (uses system-level `REG_ACL` / `SUB_ACL` / `TGID_TS*_ACL`). |
| **GROUP_HANGTIME** | Hang time (seconds) for group voice state. |
| **DEFAULT_UA_TIMER** | Default timeout (minutes in many places) for **user-activated** bridges. |
| **ANNOUNCEMENT_LANGUAGE** | Default language folder under `Audio/<lang>/` for prompts on this system. |
| **ALLOW_UNREG_ID** | Whether unregistered subscriber IDs are allowed (MASTER). |
---
## `SYSTEMS` — MASTER
| Key | Meaning |
|-----|---------|
| **REPEAT** | If true, received traffic can be **repeated** to other connected peers on the MASTER (typical conference behaviour). |
| **MAX_PEERS** | Maximum connected hotspots. |
| **EXPORT_AMBE** | Feature flag for AMBE export (if enabled in build). |
| **SINGLE_MODE** | Affects OPTIONS / generator expansion (single-user style). |
| **VOICE_IDENT** | Enables periodic **voice ident** when conditions are met (see `IdentUseCases`). |
| **TS1_STATIC** / **TS2_STATIC** | Comma-separated static TG lists pushed via OPTIONS handling (see `options_config`). |
| **DEFAULT_REFLECTOR** | Default **reflector** number for `#` dial bridges (0 = none). |
| **OVERRIDE_IDENT_TG** | Optional TG for voice ident instead of all-call. |
| **GENERATOR** | If **> 1**, this MASTER is expanded into **`NAME-0`**, **`NAME-1`**, … with consecutive ports (see `expand_generator` in code). |
**MASTER** listens for PEER connections; each authenticated peer is stored under **`PEERS`** at runtime.
---
## `SYSTEMS` — PEER
A **PEER** connects **outbound** to a **MASTER** and listens locally for the radio or app.
| Key | Meaning |
|-----|---------|
| **MASTER_IP** / **MASTER_PORT** | Address of the **MASTER** to register with (must match that MASTER’s `IP`/`PORT`). |
| **RADIO_ID** | This peer’s ID in HBP (4-byte). |
| **CALLSIGN**, **RX_FREQ**, **TX_FREQ**, **COLORCODE**, **LATITUDE**, … | RPT payload fields sent to the MASTER during registration (fixed widths in protocol). |
| **OPTIONS** | Byte string / options line (e.g. `TS2=9990;`) for static TG / behaviour. |
| **LOOSE** | Relaxed handling flag where applicable. |
The **parrot** example (`adn-parrot.example.yaml`) is a PEER that attaches to the **ECHO** MASTER: same **PASSPHRASE**, **MASTER_PORT** = ECHO’s **PORT**. See [Parrot](parrot.md).
---
## `SYSTEMS` — OPENBRIDGE
| Key | Meaning |
|-----|---------|
| **NETWORK_ID** | Must match the peer’s **NETWORK_ID** in OpenBridge packets. |
| **TARGET_IP** / **TARGET_PORT** | Remote OpenBridge peer (UDP). |
| **TGID_ACL** (or **TG1_ACL**) | Talkgroup ACL for OpenBridge (often `DENY:0-82,…` style ranges). |
| **RELAX_CHECKS** | Allow packets when peer socket does not match `TARGET` strictly (use with care). |
| **ENHANCED_OBP** | Enables **BCSQ** / **BCKA** and multi-path loop control — **should be `true`** for ADN Systems inter-server links (see below). |
| **PROTO_VER** | Embedded **DMRE** protocol version; **`5`** selects **DMRE / OpenBridge v5** (89-byte frame, BLAKE2b). Default in code is **5**; use **5** for new ADN deployments. |
**ADN Systems recommendation:** For every OpenBridge peer in the ADN mesh, set **`PROTO_VER: 5`** (DMRE v5) and **`ENHANCED_OBP: true`**. Align the same settings on **both** ends. Older **DMRD v1**-only peers are possible for backward compatibility but are not the recommended mode for the network.
Ingress filters and loop control: [OpenBridge protocol](../protocols/openbridge.md) (including [DMRE vs OpenBridge v5](../protocols/openbridge.md#dmre-and-openbridge-v5)) and [Special numbers — OpenBridge ingress](special-numbers.md#openbridge-ingress--group-tg-filters).
---
## `BRIDGES` (runtime)
The **bridge table** maps TG keys to routing rows. It is **in memory** for the running process.
The YAML loader does **not** load a top-level `BRIDGES:` block from `adn-server.yaml` into the router today — initial rows are created in code (e.g. **9990 / ECHO** bootstrap when an **ECHO** system exists), then **OPTIONS**, **user-activated** bridges, **static** bridges, and **OpenBridge** logic add rows over time.
For the conceptual model (ACTIVE, TS, TGID, timeouts): [Bridges and talkgroups](bridges-and-talkgroups.md).
---
## `REPORTS`
TCP report channel for **adn-monitor** (or compatible dashboards).
| Key | Meaning |
|-----|---------|
| **REPORT** | Enable/disable sending. |
| **REPORT_INTERVAL** | Periodic push interval (seconds). |
| **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).
---
## `LOGGER`
Implemented in `infrastructure/logging_config.py` (`setup_logging`). Values are read from the **`LOGGER`** block (or overridden by `--logging` for **LOG_LEVEL** only).
| Key | Meaning |
|-----|---------|
| **LOG_HANDLERS** | Comma-separated list of handler **tokens** (whitespace around commas is fine). Each token selects outputs; you can combine several. Recognised values: **`console-timed`** or **`console`** — log to **stderr** with format `LEVEL asctime message`; **`file-timed`** or **`file`** — log to **LOG_FILE** with the same format (UTF-8). **Default** if omitted: `console-timed`. Examples: `console-timed` only; `file-timed` only; `console-timed,file-timed` for both console and file. |
| **LOG_FILE** | Path used when **`file-timed`** or **`file`** is in **LOG_HANDLERS**. If missing, the code defaults to `/dev/null`. If the path is **`/dev/null`**, file handlers are **not** attached even if listed. If the file cannot be opened (permissions, missing directory), a warning is written to stderr and logging continues without that file handler. |
| **LOG_LEVEL** | Root logger level: **`DEBUG`**, **`INFO`**, **`WARNING`**, **`ERROR`**, **`CRITICAL`** (case-insensitive; default **INFO**). Unknown names fall back to **INFO**. You can override at startup with **`python adn-server.py --logging LEVEL`** (same names). A custom **`TRACE`** level is registered for occasional `logger.trace(...)` calls; use **`DEBUG`** for verbose diagnostics in normal operation. |
| **LOG_NAME** | Name of the logger returned to the application (default **`ADN`**). Does not change the list of handlers; it selects which named logger gets the configured level. |
---
## `ALIASES`
Downloads and local files for **peer IDs**, **subscriber IDs**, **talkgroup labels**, optional **server ID list**, **checksums**, and **keys**. Used for dashboards, validation, and optional security downloads.
| Key | Meaning |
|-----|---------|
| **PATH** | Base directory for JSON/TSV/pickle files. |
| **TRY_DOWNLOAD** | Whether to fetch from URLs when stale. |
| **PEER_FILE** / **SUBSCRIBER_FILE** / **TGID_FILE** | Local filenames. |
| **\*_URL** | Remote sources for downloads. |
| **SUB_MAP_FILE** | Pickle path for **SUB_MAP** (private call routing); default name if empty. |
| **STALE_DAYS** | Refresh threshold for downloads. |
---
## `VOICE` (from `adn-voice.yaml` or inline)
Merged into `config["VOICE"]`. See [Voice, announcements, and TTS](voice-and-tts.md) and `adn-voice.example.yaml`.
---
## ACL strings
Processed by `acl_build`: `PERMIT:` or `DENY:` followed by comma-separated IDs or ranges.
Examples:
- `PERMIT:ALL` — allow all IDs in range.
- `DENY:1` — deny ID 1 only.
- `DENY:0-82,9990-9999` — deny listed ranges.
Global ACLs apply when `USE_ACL` is true; OpenBridge may use **TGID_ACL** on the OBP system.
---
## Python environment
Use the project interpreter (see workspace rules), e.g. `python3.11` from pyenv, for consistent behaviour with production.
---
## See also
- [Introduction](introduction.md) — role of the server.
- [Bridges and talkgroups](bridges-and-talkgroups.md) — `BRIDGES` semantics.
- [Special numbers](special-numbers.md) — reserved TGs and server IDs.
- [Parrot](parrot.md) — PEER example (parrot process).

@ -0,0 +1,36 @@
# Introduction
## Purpose
This service is a **DMR peer and bridge**. It implements:
- **HBP** over UDP to **MASTER** and **PEER** systems (DMRD frames, authentication, pings).
- **OpenBridge** over UDP to other networks — **DMRE v5** (embedded version 5, BLAKE2b, hops) is the **recommended** inter-server mode on ADN; **DMRD** v1 remains for legacy interop (see [OpenBridge](../protocols/openbridge.md#dmre-and-openbridge-v5)).
Configuration is **YAML** (`adn-server.yaml`), merged at runtime with optional voice settings (`adn-voice.yaml`). The shipped template is `adn-server.example.yaml`.
## Design
Routing, timers, OpenBridge loop control, and protocol handling are implemented in **application** and **infrastructure** modules behind stable **ports**; the **domain** layer holds types and rules without I/O. This keeps the system easier to reason about and extend.
## Major subsystems
| 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. |
| **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, `BRDG_EVENT` call events. |
## Related programs
- **Parrot / playback** — separate entrypoint (`adn-parrot.py`) for record-and-playback; see [Parrot](parrot.md).
## Next steps
- [Configuration](configuration.md) — files, `GLOBAL`, **MASTER** / **PEER** / **OPENBRIDGE**, ACLs, reports, aliases, voice merge.
- [Bridges and talkgroups](bridges-and-talkgroups.md) — how `BRIDGES` works.
- [Special numbers](special-numbers.md) — TG 4000, information services, echo.
- [ADN Monitor](../../monitor/index.md) — dashboard, `adn-mon.yaml`, self-service (separate repo, deployed with the server).
- [Credits & license](attribution.md) — ADN → FreeDMR → hblink3, license.

@ -0,0 +1,29 @@
# Monitoring and reports
## TCP report channel
When **`REPORTS`** is enabled in the server config, the **ADN DMR Peer Server** listens on TCP and **report clients** (typically **adn-monitor**) connect and receive:
- **CONFIG_SND** / **BRIDGE_SND** — pickled snapshots of systems and bridges.
- **BRDG_EVENT** — text events for calls (`GROUP VOICE`, `PRIVATE VOICE`, etc.).
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) (Python monitor, WebSocket, PHP API, optional proxy and self-service).
## OpenBridge monitor semantics
- **`GROUP VOICE,INGRESS,RX`** — first sight of a stream on an OpenBridge **leg** (debug; full visibility in logs).
- **`GROUP VOICE,START,RX`** — **canonical** start after **loop control** (feeds dashboard chips / CTABLE).
- **`GROUP VOICE,END,…`** — call end; RX/TX variants depending on direction.
The dashboard shows **operational** state from **START** (canonical); the **Monitor** log shows **INGRESS** plus **START** for troubleshooting mesh duplicates.
## Requirements
- Network reachability from the **monitor host** to the server’s **`REPORTS.REPORT_PORT`** (and the server’s **`REPORT_CLIENTS`** allow list must include the monitor if used).
- **adn-monitor** `ADN_CONNECTION.ADN_IP` / **`ADN_PORT`** must match the server — see [Monitor configuration](../../monitor/configuration.md#adn_connection).
## Self-service and hotspots
Operators editing **device options** from the dashboard use the **self-service** flow (MySQL **`Clients`**, proxy **RPTO**). That is documented under [Self-service](../../monitor/self-service.md); it is **not** part of the peer server binary alone. For **hotspot proxy** configuration (`PROXY` in `adn-mon.yaml`), how it binds to the peer server **UDP port range**, and how the process starts, see [Hotspot proxy](../../monitor/hotspot-proxy.md).

@ -0,0 +1,22 @@
# Parrot (playback)
## What it is
**Parrot** is a **separate entrypoint** (`adn-parrot.py` / `parrot_main`) that records incoming **group** voice and plays it back (echo / parrot), independent of the main bridge process.
## Configuration
- Copy **`adn-parrot.example.yaml`** → **`adn-parrot.yaml`** (not committed).
- Run:
```bash
python adn-parrot.py -c adn-parrot.yaml
```
## Relation to TG 9990 / ECHO
The main server may expose an **ECHO** bridge on **TG 9990** for in-band echo. **Parrot** is a **standalone** service with its own config — use one or the other according to your deployment.
## Documentation
This page is the summary shipped with the repository; extend your deployment notes locally as needed.

@ -0,0 +1,24 @@
# Private calls
## Overview
**Unit (private)** calls use a different path than **group** voice. The router uses **`SUB_MAP`** (subscriber → last known system/slot/time) and collision rules to decide whether and where to forward.
## SUB_MAP
- Populated when stations register traffic; persisted via configured **`SUB_MAP`** pickle path under **`ALIASES`**.
- Used to resolve **destination radio ID** to a **target system** and **slot** for private forwarding.
## OpenBridge vs MASTER
Private handling uses CSBK/data/unit branches, `SUB_MAP` lookup, and busy-slot checks where applicable (see `BridgeUseCases` in source).
## TG / ID 4000 (unit)
As documented in [Special numbers](special-numbers.md), a **private** call to **4000** disconnects dynamics and is **not** treated as a normal private call route.
## Reporting
Private **START/END** events may be emitted to the report TCP client when **`REPORTS.REPORT`** is enabled, analogous to group voice (shape `PRIVATE VOICE,...` where implemented).
For protocol ingress details, see [HBP](../protocols/hbp.md) and the bridge use cases in source (`BridgeUseCases._pvt_call_received`).

@ -0,0 +1,103 @@
# Special numbers (TG / IDs)
Several **destination IDs** are reserved for **control or services**. They are handled in protocol layers and/or the bridge router, not as normal group traffic.
## ID 5000 — server voice source (not “announcement TG”) {#id-5000--server-voice-source-not-announcement-tg}
**Important:** **5000** is the **RF source ID** the server uses when it **transmits** automated voice. Radios and dashboards show **caller ID 5000** for that traffic.
| Traffic | Destination in the DMR packet | Notes |
|---------|----------------------------------|--------|
| **Scheduled AMBE** (`ANNOUNCEMENTS`) | Whatever **`TG`** you set in `adn-voice.yaml` | Source ID **5000**. |
| **TTS** (`TTS_ANNOUNCEMENTS`) | Same — configured **`TG`** | Source ID **5000**. |
| **On-demand** (TG **9991–9999**) | **TG 9** | Short info clips; source **5000** (see [Voice, announcements, and TTS](voice-and-tts.md)). |
| **Disconnected / reflector prompts** | **TG 9** | Source **5000**. |
| **Voice ident** | **All-call** (`16777215`) or **`OVERRIDE_IDENT_TG`** if set | Source **5000**. |
You do **not** “monitor TG 5000” to hear scheduled announcements: you monitor the **configured announcement TG** (e.g. 2, 9, 26811). **5000** appears as the **transmitter ID** on those calls.
### Destination TG 5000 (inbound group)
If a group call arrives with **destination TG 5000** and there is **no** existing `BRIDGES` entry, the server does **not** auto-create a user-activated bridge for that TG (same class as IDs **0–4**, **9**, **4000**). To carry traffic to TG 5000 you need an **explicit** bridge row.
## TG 9 — local service lane (prompts and bridge plumbing) {#tg-9-local-service-lane-prompts-and-bridge-plumbing}
**TG 9** is used in two different ways: what **operators hear** from short server prompts, and how **internal bridge rows** are wired.
### What you hear (outbound server voice)
For **on-demand** playback (after you key **9991–9999**) and for **disconnected / reflector** voice lines, the server transmits **group** packets with:
- **Source ID 5000**
- **Destination TG 9**
- **Timeslot 2** (the code drives the **TS2** slot for that hotspot)
That follows a common **HomeBrew / conference** convention: keep short **local service** audio on **TG 9 / TS2** so it stays separate from normal QSO traffic on your main TG (often on TS1). Hotspots must pass **TS2** and be on a configuration where **TG 9** is not blocked, or those prompts will not be heard.
**Red vs local:** On **OpenBridge**, **inbound group** traffic to **TG 9** is in the **≤ 79** “local / repeater” range and is **not** brought into the bridge from the IP mesh (it is dropped at ingress). Server prompts use the **local HBP path** to the hotspot/repeater, not a wide-area bridged TG. Exception: only if you **explicitly** add `BRIDGES` rows that forward TG 9 could that traffic be sent elsewhere — not the default.
**Scheduled** announcements and **TTS** use the **`TG`** you set in `adn-voice.yaml` — they are **not** forced to TG 9 unless you configure that TG yourself.
### Router behaviour (reserved TG)
- **No automatic user-activated bridge** if someone transmits to **TG 9** and no `BRIDGES` row exists (same class as **0–4**, **4000**, **5000**, etc.).
- With **`GEN_STAT_BRIDGES`**, automatic **STAT** bridge creation from OpenBridge does **not** apply to destination **TG 9** (it is excluded on purpose).
- The **bridge debug** loop removes invalid conference bridges keyed **`9`** (and **`0`–`8`**) so single-digit stray bridges do not accumulate.
### Bridge table (advanced)
Many **reflector / dial** rows store **`TGID` = 9** on **TS2** as the **leg destination** used internally to attach the dynamic path to the real talkgroup — that is wiring inside `BRIDGES`, not something you “call” like a normal national TG.
## TG / ID 4000 — deactivate dynamic bridges
**Purpose:** Clear **user-activated (dynamic) bridges** for the system that receives the call.
**Behaviour:**
- 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.
Use this when operators need to **reset** dynamic routing without restarting the server.
## TG 9991–9999 — information / on-demand audio
**Purpose:** **Play back** pre-generated AMBE (“ondemand”) files (e.g. station info, help).
**Behaviour:**
- Triggers **`playFileOnRequest`**-style handling: maps the last digits to a file name under the configured audio tree.
- Works from **MASTER** and **PEER** paths.
The **audio** is sent with **source ID 5000** and **destination TG 9** in the generated stream. File layout: [Voice, announcements, and TTS](voice-and-tts.md).
## TG 9990 — echo / parrot (in-band)
**Purpose:** Bridge rows for **echo** often use **9990** with the **ECHO** system (see `BRIDGES` and options in your YAML).
**Note:** A **standalone parrot** is also available as a separate process — [Parrot](parrot.md).
## Private call to ID 4000
A **unit** call to **4000** is treated as **disconnect dynamics** only; it is **not** routed as a normal private call.
## OpenBridge ingress — group TG filters {#openbridge-ingress--group-tg-filters}
On **OpenBridge** (**DMRD** v1 and **DMRE**), **non-unit** group traffic to certain TGs may be **dropped** before bridging (with **BCSQ** where applicable). Rules differ slightly between DMRD and DMRE; they include low-number TGs (e.g. **≤ 79**), **9990–9999**, **900999**, ranges **92–199** (vs source server), and MCC-related ranges (**80–89**, **800–899**). Details: [OpenBridge protocol](../protocols/openbridge.md#ingress-filters-group-tg).
## Prohibited / reserved TGs (bridge creation)
Many small IDs (0–5, 9, etc.) and the **999x service** range are excluded from certain **automatic** bridge-creation paths; **5000** and **4000** are in the “no auto UA bridge” set when no row exists. Exact sets are defined in the bridge router and options handling in source.
## Summary table
| ID / range | Role |
|------------|------|
| **5000** (source) | Server-generated voice (announcements, TTS, prompts, ident) — **caller ID** on receivers |
| **5000** (destination) | No auto user-activated bridge if missing from `BRIDGES` |
| **4000** (group) | Deactivate dynamic bridges |
| **4000** (unit) | Disconnect dynamics; not routed as PC |
| **9991–9999** | On-demand / information audio (trigger TG); playback uses src **5000** → TG **9** (TS2) |
| **9** | Service/prompt lane (short server audio); reserved for auto-bridges; internal **TGID** on TS2 legs |
| **9990** | Echo bridge TG (with ECHO system) |
| **16777215** | All-call (default voice-ident destination unless overridden) |

@ -0,0 +1,56 @@
# Voice, announcements, and TTS
## Configuration files
- **`adn-voice.yaml`** (optional, not committed) — merged into `config["VOICE"]`.
- Hot reload on file **mtime** (~15 s loop).
Template: `adn-voice.example.yaml`.
## Server voice identity (ID 5000)
All **server-originated** voice uses **RF source ID 5000** in the DMR stream so clients can tell infrastructure traffic from user radios:
- Scheduled **ANNOUNCEMENTS** and **TTS_ANNOUNCEMENTS** (destination = the **`TG`** field in each item).
- **On-demand** clips triggered by dialling **9991–9999** (playback uses destination **TG 9** on **TS2**; you still key **999x** to request the file).
- **Disconnected / reflector** voice lines (same: **TG 9** / **TS2**).
See [TG 9 — local service lane](special-numbers.md#tg-9-local-service-lane-prompts-and-bridge-plumbing) for why TG 9 is used and what must be enabled on the hotspot.
- **Voice ident** (destination **all-call** or **`OVERRIDE_IDENT_TG`**).
See [Special numbers — ID 5000](special-numbers.md#id-5000--server-voice-source-not-announcement-tg) for the full table.
## Features
| Feature | Description |
|---------|-------------|
| **Scheduled announcements** | AMBE phrases from `Audio/<lang>/ondemand/` on a schedule (hourly / interval). |
| **TTS announcements** | Text → gTTS → MP3 → ffmpeg → WAV → vocoder → AMBE (see pipeline below). |
| **Voice ident** | Periodic identification (optional); uses `VOICE_IDENT` in main server config and `OVERRIDE_IDENT_TG` / all-call. |
| **Recording** | When enabled, records AMBE to disk for traffic on **`RECORDING_TG`** / **`RECORDING_TIMESLOT`** (see template `adn-voice.example.yaml`). |
| **On-demand (9991–9999)** | See [Special numbers](special-numbers.md). |
## TTS pipeline (summary)
1. Source text in `.txt` under `Audio/<language>/ondemand/`.
2. **gTTS** produces **MP3**.
3. **ffmpeg** converts to **WAV**.
4. Vocoder command or **AMBEServer** produces **AMBE** for transmission.
**ffmpeg** must be installed on the host OS.
Configure **`TTS_VOCODER_CMD`** or **`TTS_AMBESERVER_HOST`** / **`TTS_AMBESERVER_PORT`** in `adn-voice.yaml` for encoding; see comments in `adn-voice.example.yaml`.
## Anti-collision (QSO) with announcements
When scheduling announcements, the server may **wait** if target slots are busy, **drop** targets if a live QSO appears mid-stream, and only mark **hourly** announcement state after a successful target list — this avoids clobbering live traffic.
## Broadcast queue
Parallel **broadcasts** on **different** TGs may run concurrently; **same-TG** broadcasts are **serialised** so one announcement completes before another on that TG.
## See also
- [Configuration](configuration.md) — voice file paths.
- [Parrot](parrot.md) — separate playback service.
- [Special numbers](special-numbers.md) — 5000, 999x, recording-related behaviour.

@ -0,0 +1,61 @@
# Documentación ADN Systems
Este sitio describe el **ADN DMR Peer Server** y el **ADN Monitor** como una pila operativa unida. El contenido se organiza por **producto** (`server/` frente a `monitor/`) en este **locale** español.
- **English:** mismo contenido bajo `docs/en/` — `mkdocs build -f mkdocs.yml` → `site/en/` (ver `docs/en/README.md` en el repositorio).
## ADN DMR Peer Server
El **ADN DMR Peer Server** es un puente de conferencia [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.html) para radio móvil digital (DMR). Está estructurado en capas de **arquitectura limpia** (dominio, aplicación, infraestructura).
### Qué hace el servidor
- Termina enlaces **HBP** (HomeBrew Protocol) hacia sistemas **MASTER** y **PEER** (hotspots, repetidores).
- Termina enlaces **OpenBridge** hacia otros servidores por UDP — **DMRE v5** (recomendado en ADN) o **DMRD** v1 heredado.
- Ejecuta **enrutado de bridges** (`BRIDGES`): voz de grupo, control de bucle, ACL, **BCSQ** / **BCKA** opcionales.
- Soporta **llamadas privadas** (`SUB_MAP`), **voz**, **TTS**, **grabación** e **informes TCP** al monitor.
### Por dónde empezar (servidor)
| Quiero… | Empieza aquí |
|---------|----------------|
| Ejecutar y configurar | [Introducción](server/user-guide/introduction.md), [Configuración](server/user-guide/configuration.md) |
| 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) |
| 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) |
| Créditos, licencia, linaje | [Créditos y licencia](server/user-guide/attribution.md) |
### Inicio rápido (servidor)
```bash
pip install -r requirements.txt
cp adn-server.example.yaml adn-server.yaml
python adn-server.py -c adn-server.yaml
```
Más: [Introducción](server/user-guide/introduction.md).
---
## ADN Monitor
Panel, **WebSocket** en vivo, **API PHP** opcional, **MySQL** self-service y **proxy hotspot** — ver [Descripción general del monitor](monitor/index.md).
| Quiero… | Empieza aquí |
|---------|----------------|
| `adn-mon.yaml` y despliegue | [Configuración del monitor](monitor/configuration.md) |
| Proxy hotspot (UDP, `PROXY`, rango de puertos) | [Proxy hotspot](monitor/hotspot-proxy.md) |
| Self-service | [Self-service](monitor/self-service.md) |
| Cómo encaja con el servidor | [Monitor e informes](server/user-guide/monitoring.md) |
---
## Locales
- **Español** — **`docs/es/`** (este árbol).
- **English** — **`docs/en/`**.
Ver [Traducciones](server/contributing/translations.md).

@ -0,0 +1,66 @@
# Arquitectura e implantación
## Arquitectura limpia (monitor Python)
Bajo `monitor/src/adn_monitor/`:
- **Dominio** — objetos de valor, errores, tipos de opcode.
- **Aplicación** — `MonitorState`, `process_message` en `monitor_controller.py`, servicio de alias, casos de uso Last Heard / conteo TG, formato de hora.
- **Infraestructura** — `load_config` YAML, cliente Twisted **TCP** (`ReportClientFactory`) al peer server, fábrica **WebSocket** para el panel, repositorios MySQL, decodificadores pickle/json para `CONFIG_SND` / `BRIDGE_SND`.
El monitor **sale hacia** **`ADN_CONNECTION.ADN_IP:ADN_PORT`** y recibe mensajes con prefijo de longitud (estilo netstring). Actualiza **CTABLE** (masters/peers/OpenBridge) y **BTABLE** (bridges) en memoria, y persiste resultados de **BRDG_EVENT** cuando MySQL está configurado.
## Protocolo de informes (desde el peer server)
Los mismos opcodes que en la documentación del servidor: **CONFIG_SND**, **BRIDGE_SND**, **BRDG_EVENT**, etc. El monitor los decodifica y aplica en `process_message` — ver [Monitor e informes](../server/user-guide/monitoring.md).
## WebSocket
`monitor.py` ejecuta un **WebSocket** Twisted en **`WEBSOCKET_SERVER.WEBSOCKET_PORT`**, enviando instantáneas JSON a **`FREQUENCY`** para que la app React actualice sin polling del estado principal.
## Backend PHP
- **Slim 4** front controller: `backend/public/index.php`.
- Carga **`adn-mon.yaml`** vía **`ADN_CONFIG_PATH`** (igual que el monitor).
- **`/api/config/dashboard`** — título, idioma, flags (`selfService`, `showConsole`, …) desde **`DASHBOARD`**.
- **`/api/auth/*`** — sesión por cookie cuando hay BD **SELF_SERVICE**.
- **`/api/self-service/*`** — opciones de dispositivo (ver [Self-service](self-service.md)).
- **`/api/aliases/*`** — proxy opcional a URLs de listas TG/bridge desde **ALIASES**.
## Frontend
- **Vite + React** bajo `frontend/`; el build genera estáticos servidos por nginx/Apache o similar.
- Usa **`API_BASE`** (build) para la API PHP y **URL del WebSocket** para datos en vivo.
## Proxy hotspot
- Entrada: `proxy/proxy.py`; paquete `src/adn_proxy/` (dominio / aplicación / infraestructura).
- Lee **`PROXY`** y **`SELF_SERVICE`** del mismo YAML.
- Por cada cliente hotspot, asigna un puerto en **`DESTPORT_START`…`DEST_PORT_END`** y reenvía UDP a **`MASTER`**.
- Cuando **self-service** actualiza **`Clients.options`** y pone **`modified=1`**, el proxy envía **RPTO** al **master** en un temporizador (~10 s). El **peer server** aplica entonces las opciones al camino del hotspot (ver [Self-service](self-service.md)).
**Por qué no forma parte del binario del peer server:** comparte **`adn-mon.yaml`**, self-service MySQL e implantación con la pila del monitor — ver [Por qué va con el monitor](hotspot-proxy.md#why-it-ships-with-the-monitor-not-inside-the-peer-server).
**Detalle:** [Proxy hotspot](hotspot-proxy.md) (claves de config, rango de puertos del peer, arranque).
## Topología típica de despliegue
```text
[Hotspots] --UDP--> [Proxy :LISTEN_PORT] --UDP--> [Peer server :rango DESTPORT]
|
v
MySQL (Clients)
[Peer server :REPORT_PORT] <--- TCP --- [monitor.py : cliente que conecta]
[Navegador] --HTTPS--> [API PHP + frontend estático]
[Navegador] --WS----> [WebSocket del monitor :9000]
```
---
## Ver también
- [Inicio de la documentación](../README.md)
- [Configuración](configuration.md)
- [Self-service](self-service.md)

@ -0,0 +1,120 @@
# Configuración (`adn-mon.yaml`)
Todos los componentes leen el **mismo** YAML (ruta por defecto a menudo `monitor/adn-mon.yaml`; se puede sobrescribir con **`ADN_CONFIG_PATH`**). El ejemplo del repositorio **adn-monitor** es la plantilla autoritativa; las claves siguientes coinciden con `monitor/adn-mon.yaml` y `monitor/src/adn_monitor/infrastructure/config_loader.py` (los nombres internos pueden diferir).
---
## `GLOBAL`
| Clave | Significado |
|-------|-------------|
| **BRIDGES_INC** | Mostrar estado de bridges en el panel si es `true`. |
| **HOMEBREW_INC** | Incluir estado HBP peer/master. |
| **LASTHEARD_INC** | Activar funciones / tablas Last Heard. |
| **LASTHEARD_ROWS** | Filas para widgets Last Heard. |
| **EMPTY_MASTERS** | Mostrar masters sin peers. |
| **TGCOUNT_INC** | Activar página / estadísticas de conteo de TG. |
| **TGCOUNT_ROWS** | Filas para el conteo de TG. |
| **TIMEZONE** | Zona horaria IANA (p. ej. `America/Santiago`) para mostrar; vacío usa la hora local del servidor. |
---
## `ADN_CONNECTION` {#adn_connection}
Debe coincidir con la configuración de informes del **ADN DMR Peer Server**.
| Clave | Significado |
|-------|-------------|
| **ADN_IP** | Host/IP donde está el **listener TCP de informes** del peer server (vista de red desde el monitor). |
| **ADN_PORT** | Puerto TCP — debe ser igual a **`REPORTS.REPORT_PORT`** en el servidor y ser alcanzable. |
---
## `SELF_SERVICE`
Credenciales MySQL y parámetros PBKDF2 para **login** y acceso a la tabla **`Clients`**. **PBKDF2_SALT** y **PBKDF2_ITERATIONS** deben coincidir con **`hotspot_proxy_self_service.py`** (o tu herramienta de registro de contraseñas) para que los hashes verifiquen en PHP y Python.
| Clave | Significado |
|-------|-------------|
| **USE_SELFSERVICE** | Lo usa el cargador de config del **proxy** para rutas con BD / self-service (ver README del proxy). |
| **DB_SERVER**, **DB_USERNAME**, **DB_PASSWORD**, **DB_NAME**, **DB_PORT** | Conexión MySQL para **`Clients`** (y tablas relacionadas). |
Si el backend PHP no puede conectar, las rutas de **auth** y **self-service** no se registran (ver `backend/public/index.php`).
---
## `PROXY`
**Proxy UDP hotspot** — guía completa: [Proxy hotspot](hotspot-proxy.md). Resumen: reenvía cada cliente a **`MASTER:DESTPORT_START`…`DEST_PORT_END`**; el peer server debe **escuchar** en esa IP y rango de puertos (ver también [Arquitectura](architecture.md)).
| Clave | Significado |
|-------|-------------|
| **MASTER** | Host del peer server (IP o DNS; resuelto al arrancar el proxy). |
| **LISTEN_PORT** / **LISTEN_IP** | Dónde el proxy acepta UDP del hotspot (IP vacía suele significar todas las interfaces). |
| **DESTPORT_START** / **DEST_PORT_END** | Un puerto por cliente proxy hacia **`MASTER`**. |
| **TIMEOUT**, **STATS**, **DEBUG**, **CLIENT_INFO** | Comportamiento y registro. |
| **BLACK_LIST** / **IP_BLACK_LIST** | Listas de bloqueo opcionales. |
---
## `OPB_FILTER`
**Network IDs** separados por comas (como cadenas). El tráfico desde esas fuentes OpenBridge puede **ocultarse** de ciertas rutas de persistencia del panel (ver manejo de `OPB_FILTER` en el controlador del monitor).
---
## `ALIASES`
Misma idea que en el peer server: descargar JSON de **peer / subscriber / TGID** y checksums opcionales. Claves: **PATH**, **\*_FILE**, **\*_URL**, **STALE_HOURS**, **REVIEW_INTERVAL_MINUTES**, **CHECKSUM_***, **TG_LIST_URL**, **BRIDGE_LIST_URL** (proxy del backend para páginas del frontend).
---
## `LOGGER`
| Clave | Significado |
|-------|-------------|
| **LOG_PATH** | Directorio de ficheros de log. |
| **LOG_FILE** | Nombre del log del monitor (p. ej. `adn-mon.log`). |
| **PROXY_LOG_FILE** | Nombre de log separado para el proxy (cuando se ejecuta con logging del proxy). |
| **LOG_LEVEL** | p. ej. `INFO`, `DEBUG`. |
---
## `WEBSOCKET_SERVER`
| Clave | Significado |
|-------|-------------|
| **WEBSOCKET_PORT** | Puerto del WebSocket Twisted que envía estado JSON a los navegadores. |
| **FREQUENCY** | Intervalo de envío (segundos). |
| **CLIENT_TIMEOUT** | Cerrar clientes WS inactivos tras N segundos (`0` = desactivado). |
| **USE_SSL**, **SSL_PATH**, **SSL_CERTIFICATE**, **SSL_PRIVATEKEY** | WSS opcional. |
---
## `DASHBOARD`
| Clave | Significado |
|-------|-------------|
| **DASHTITLE** | Título de cabecera. |
| **BACKGROUND** | Usar fondo `bk.jpg` si es `true`. |
| **LANGUAGE** | Idioma por defecto de la UI (`en`, `es`, …). |
| **SELF_SERVICE** | Si es `true`, la UI puede mostrar la entrada **Self-service** (el backend debe exponer API + BD). |
| **SHOW_CONSOLE** | Mostrar página consola (mensajes inicio/fin de llamada). |
| **MIN_DURATION** | Duración mínima de llamada (segundos) para la tabla Last Heard del **panel** (la página Last Heard puede seguir mostrando más cortas). |
| **nav_links**, **footer**, **news** | Enlaces estructurados / marquee opcionales. |
---
## Entorno
- **`ADN_CONFIG_PATH`**: ruta absoluta a `adn-mon.yaml` para monitor, backend y proxy.
- El backend puede usar **`API_BASE_PATH`** si la API va bajo un prefijo.
---
## Ver también
- [Inicio de la documentación](../README.md)
- [Arquitectura](architecture.md)
- [Self-service](self-service.md)
- **`REPORTS`** en el peer server: [Monitor e informes](../server/user-guide/monitoring.md), [Configuración del servidor](../server/user-guide/configuration.md) (sección **`REPORTS`**).

@ -0,0 +1,114 @@
# Proxy hotspot
El **proxy hotspot** forma parte del repositorio **adn-monitor**. Es un **relé UDP** entre **hotspots DMR** (Homebrew / HBP) y el **MASTER** del **ADN DMR Peer Server**: cada hotspot conectado se asigna a un **puerto de destino** dedicado en el host del peer server, de modo que muchos hotspots puedan compartir una IP pública sin choques de puertos.
Estructura: `proxy/proxy.py`, paquete `proxy/src/adn_proxy/` (arquitectura limpia). **GPL v3** (derivado del proxy original de Simon Adlem, G7RZU).
### Por qué va con el monitor (y no dentro del peer server) {#why-it-ships-with-the-monitor-not-inside-the-peer-server}
No hay un despliegue obligatorio único, pero **hoy el proxy vive en el repo adn-monitor** a propósito:
- **Misma config y operativa** que el panel: **`adn-mon.yaml`**, **`ADN_CONFIG_PATH`**, y normalmente el mismo host que **PHP** y **MySQL**.
- El **self-service** (**`Clients`**, RPTO, **`modified`**) está montado sobre ese ecosistema; el binario del peer server no posee esa base de datos ni el bloque **`PROXY`**.
- **División de roles:** el **ADN DMR Peer Server** es el **núcleo de radio** (HBP/OpenBridge, bridges, voz, informes TCP). El proxy hotspot es un **frente UDP opcional** hacia un MASTER que ya escucha en un **rango** de puertos — útil cuando muchos hotspots comparten una dirección pública.
**Integrar el proxy en el peer server** (un binario, un `adn-server.yaml`) es imaginable para empaquetado, pero implica **unificar configuración**, **replantear el cableado de self-service** y más mantenimiento — solo compensa si quieres explícitamente un servidor “todo en uno” desplegable.
---
## Fichero de configuración (igual que el monitor)
El proxy **no** usa `adn-server.yaml`. Lee el YAML del **monitor**:
| Origen | Uso |
|--------|-----|
| **`ADN_CONFIG_PATH`** | Variable de entorno: ruta absoluta a **`adn-mon.yaml`** (compartida con **monitor**, **backend PHP**, **`.env`** opcional en la raíz del repo). |
| **`python proxy/proxy.py --config /ruta/a/adn-mon.yaml`** | Sobrescribe la ruta solo para este proceso. |
| **Por defecto** (sin definir) | `../monitor/adn-mon.yaml` relativo al directorio `proxy/` al ejecutar desde el árbol adn-monitor. |
Secciones usadas:
- **`PROXY`** — dirección de escucha, host master, **rango** de puertos de destino, timeouts, debug, listas negras.
- **`SELF_SERVICE`** — MySQL y **`USE_SELFSERVICE`** (tabla **`Clients`**, RPTO / opciones).
- **`LOGGER`** — **`LOG_PATH`** y **`PROXY_LOG_FILE`** (log del proxy separado de **`LOG_FILE`** de `monitor.py`).
**Entorno** opcional (ver `proxy/README.md` en el repo): p. ej. **`ADN_PROXY_DEBUG`**, **`ADN_PROXY_LISTENPORT`**.
---
## Claves `PROXY` (`adn-mon.yaml`)
| Clave | Rol |
|-------|-----|
| **MASTER** | IP o **nombre de host** del **ADN DMR Peer Server**. Se resuelve a IPv4 al arrancar (Twisted necesita IP para `write()`). |
| **LISTEN_PORT** | Puerto UDP donde los **hotspots** se conectan **al proxy** (lo que configuran en el hotspot). |
| **LISTEN_IP** | Vacío suele significar todas las interfaces; si no, enlazar a esa dirección. |
| **DESTPORT_START** / **DEST_PORT_END** | Rango inclusivo de puertos UDP en **`MASTER`**, **uno por hotspot** proxy (asignación secuencial dentro del proxy). |
| **TIMEOUT** | Tiempo de inactividad / sesión (segundos). |
| **STATS** | Registro extra de estadísticas. |
| **DEBUG** | Log detallado de paquetes (o **`ADN_PROXY_DEBUG=1`**). |
| **CLIENT_INFO** | Información por cliente en logs. |
| **BLACK_LIST** / **IP_BLACK_LIST** | Bloquear IDs de radio o IPs de origen. |
Las claves internas tras la carga usan nombres mixtos (`Master`, `ListenPort`, …) — ver `adn_proxy.infrastructure.config_loader`.
---
## El peer server (`adn-server.yaml`) debe cubrir el rango de puertos
El proxy reenvía tráfico a **`MASTER:DESTPORT`** por cliente, con **DESTPORT** en **[DESTPORT_START, DEST_PORT_END]**.
El **ADN DMR Peer Server** debe **escuchar UDP** en **ese host** en **cada puerto** del rango que vayas a usar (un listener **MASTER** por puerto, o equivalente).
- Un único `MODE: MASTER` con un solo **`PORT`** **no** basta para varios clientes proxy si usan **DESTPORT** distintos — hacen falta **varios listeners** en el rango.
- Enfoques típicos: **`GENERATOR`** en un sistema MASTER (se parte en `NAME-0`, `NAME-1`, … con **PORT** consecutivos — ver [Configuración del servidor](../server/user-guide/configuration.md)), y/o varias entradas **`SYSTEMS`**, alineadas con **`DESTPORT_START`…`DEST_PORT_END`** en **`PROXY`**.
Si el servidor solo escucha p. ej. en **56400** pero el proxy envía a **56401**, ese cliente no se registrará.
---
## Cómo arranca el proceso
1. Resolver ruta de config (`ADN_CONFIG_PATH`, `--config`, o por defecto).
2. **`load_config()`** parsea YAML → **`PROXY`**, **`SELF_SERVICE`**, **`LOG`**.
3. Pool **MySQL** opcional si self-service / funciones de BD están activas.
4. El **reactor** Twisted ejecuta UDP **ProxyProtocol** en **`LISTEN_IP:LISTEN_PORT`**, reenviando a **`MASTER:puerto_destino_asignado`**.
Ejecución (desde la raíz de adn-monitor, con entorno):
```bash
export ADN_CONFIG_PATH=/opt/adn-monitor/monitor/adn-mon.yaml
python proxy/proxy.py
# o
python proxy/proxy.py --config /ruta/a/adn-mon.yaml
```
Usar **systemd** u otro supervisor junto a **`monitor.py`** y la pila **PHP**.
---
## RPTO, opciones y self-service
El proxy **nunca** envía **RPTO** directamente al hotspot para actualizaciones self-service. Envía **RPTO al MASTER** (peer server); el servidor actualiza bridges/opciones y aplica el flujo HBP normal.
| Evento | Comportamiento del proxy |
|--------|---------------------------|
| ~**10 s** tras login del hotspot (**RPTC**) | Leer **`Clients.options`** de la BD → **RPTO** → master `(MASTER, dport)`. |
| Cada ~**10 s** | Filas con **`modified = 1`** → **RPTO** → master, luego limpiar **`modified`**. |
| El hotspot envía **RPTO** | Reenvío al master; actualizaciones de BD según implementación. |
Detalle: [Self-service](self-service.md) y **`proxy/README.md`** en adn-monitor.
---
## Visibilidad en el monitor
Los hotspots aparecen en el panel solo si el **peer server** envía **informes TCP** al mismo host/puerto que **`ADN_CONNECTION`** en **`adn-mon.yaml`**. Alinea **`REPORTS`** en el servidor con **`ADN_IP` / `ADN_PORT`**. Ver [Monitor e informes](../server/user-guide/monitoring.md).
---
## Ver también
- [Configuración del monitor](configuration.md) — referencia **`adn-mon.yaml`** (resumen PROXY).
- [Arquitectura](architecture.md) — dónde encaja el proxy en la pila.
- [Self-service](self-service.md) — BD, **`modified`**, temporización RPTO.

@ -0,0 +1,40 @@
# ADN Monitor (descripción general)
**ADN Monitor** es un proyecto distinto del **ADN DMR Peer Server**, pero ambos suelen desplegarse **juntos**: el servidor envía **informes TCP** (config, bridges, eventos de llamada) al monitor; el monitor alimenta el **panel web** (React) y las actualizaciones **WebSocket**. Los componentes opcionales incluyen la **API PHP** (Slim), **MySQL** (self-service / registro de dispositivos) y el **proxy hotspot** (UDP entre hotspots y el peer server).
Este capítulo documenta la pila **adn-monitor** con el mismo nivel de detalle que las guías del servidor. El código fuente está en el repositorio **adn-monitor**, no en el repositorio **adn-server** (donde se mantiene esta documentación).
## Qué hace cada parte
| Parte | Rol |
|-------|-----|
| **`monitor/monitor.py`** | Python (Twisted): se conecta al **puerto TCP de informes** del peer server, decodifica cargas netstring (`CONFIG_SND`, `BRIDGE_SND`, `BRDG_EVENT`), mantiene **CTABLE** / **BTABLE**, escribe **Last Heard** / estadísticas de TG en **MySQL** si está configurado, sirve JSON **WebSocket** al panel. |
| **`backend/`** | PHP **Slim**: `/api/config/dashboard`, auth, APIs **self-service** opcionales, proxy de alias. Lee el **mismo** `adn-mon.yaml` vía **`ADN_CONFIG_PATH`**. |
| **`frontend/`** | React (Vite): UI del panel; consume API del backend + WebSocket. |
| **`proxy/`** | Python (Twisted): **proxy hotspot** UDP; reenvía Homebrew entre hotspots y el peer server; lee **`Clients`** en MySQL para opciones **RPTO** (self-service). |
## Un solo fichero de configuración
**`adn-mon.yaml`** (ruta a menudo con **`ADN_CONFIG_PATH`** en `.env`) lo comparten:
- Monitor Python (`monitor.py`)
- Backend PHP (`backend/public/index.php`)
- Proxy hotspot (`proxy/proxy.py`)
Un **único** YAML define direcciones de informes, textos del panel, puerto WebSocket, credenciales **SELF_SERVICE** de la BD y ajustes **PROXY** de escucha/rango.
## Enlace con el peer server
| Servidor (`adn-server.yaml`) | Monitor (`adn-mon.yaml`) |
|------------------------------|---------------------------|
| **`REPORTS.REPORT_CLIENTS`** — lista de IPs permitidas para conectar **al** listener de informes, o el host del monitor | **`ADN_CONNECTION.ADN_IP`** / **`ADN_PORT`** — a dónde **se conecta** el monitor (debe coincidir con el bind y puerto de informes del servidor). |
| **`REPORTS.REPORT_PORT`** — puerto TCP en el que el **servidor escucha** conexiones de informes | El mismo puerto que **`ADN_PORT`**. |
Ver [Monitor e informes](../server/user-guide/monitoring.md) para los opcodes de informes y [Configuración del monitor](configuration.md) para cada sección de `adn-mon.yaml`.
## Ver también
- [Proxy hotspot](hotspot-proxy.md) — `PROXY`, rango de puertos del peer, carga de config y arranque
- [Arquitectura e implantación](architecture.md)
- [Configuración (`adn-mon.yaml`)](configuration.md)
- [Self-service](self-service.md)

@ -0,0 +1,77 @@
# Self-service
**Self-service** permite al dueño de un hotspot **iniciar sesión** en el panel, **editar las opciones de su dispositivo** (listas estáticas de TG, reflector por defecto, temporizador, idioma, etc.) y que esas opciones se **envíen al ADN DMR Peer Server** sin editar YAML a mano en el servidor.
Intervienen **cuatro** piezas: **MySQL** (tabla `Clients`), **API PHP** (sesión + REST), **React** (página `/self-service`) y el **proxy hotspot** (**RPTO** periódico al master). El **peer server** es quien aplica al final **RPTO** / **OPTIONS** al bridge en ejecución y al comportamiento del hotspot.
---
## Requisitos previos
1. Bloque **`SELF_SERVICE`** en **`adn-mon.yaml`** con credenciales **MySQL** válidas y parámetros **PBKDF2** alineados con tu herramienta de contraseñas (misma sal/iteraciones que **`hotspot_proxy_self_service.py`** cuando se use).
2. **`DASHBOARD.SELF_SERVICE: true`** para que la UI muestre la entrada **Self-service** (y el backend exponga `/api/self-service/*` cuando la BD conecta).
3. Tabla **`Clients`** con filas: **`callsign`**, **`int_id`** (ID DMR), **`psswd`** (hex PBKDF2-SHA256), **`options`** (línea `KEY=value` separada por `;`), **`logged_in`**, **`host`**, **`modified`**, etc. (ver esquema / migraciones en el repo adn-monitor).
4. El tráfico del hotspot debe pasar por el **proxy** si dependes de **`modified`** y del envío **RPTO** (ver flujo abajo).
---
## Autenticación
| Endpoint | Uso |
|----------|-----|
| **`POST /api/auth/login`** | Cuerpo: `callsign`, `password`. Verifica hash **PBKDF2** contra **`Clients.psswd`** en filas con **`logged_in = 1`**. Éxito: sesión PHP con **`user_id`**, **`int_ids`** (todos los DMR ID de ese indicativo). |
| **`GET /api/auth/login-by-ip`** | Opcional: una coincidencia de usuario por **`Clients.host`** = IP del cliente (misma forma de sesión). |
| **`POST /api/auth/logout`** | Cierra sesión. |
| **`GET /api/auth/me`** | Devuelve `{ callsign, int_ids, selected_int_id }` para React. |
La sesión se prolonga con actividad (**SelfServiceController** usa un timeout largo de inactividad).
---
## API de dispositivo (tras login)
| Método | Ruta | Uso |
|--------|------|-----|
| **GET** | `/api/self-service/device?int_id=` | Carga fila **`Clients`** para ese **`int_id`** (debe estar en **`int_ids`** de sesión). JSON: **`int_id`**, **`callsign`**, **`mode`**, **`options`** parseadas en listas TS1/TS2, DIAL, VOICE, LANG, SINGLE, TIMER. |
| **POST** | `/api/self-service/device/options` | Cuerpo: **`int_id`**, cadena **`options`** (línea Homebrew **OPTIONS**). Debe terminar en **`;`**. Longitud máx. **4096**. Actualiza BD: **`Clients.options`**, **`modified = 1`**. |
| **GET** | `/api/self-service/device/modified?int_id=` | Devuelve **`{ modified: 0|1 }`** desde **`Clients.modified`** (la UI puede hacer polling hasta que el proxy lo limpie). |
| **POST** | `/api/self-service/device/select` | Cuerpo: **`int_id`** — fija **`selected_int_id`** en sesión si el usuario tiene varios dispositivos. |
---
## Flujo extremo a extremo (cómo llegan las opciones al hotspot)
1. El usuario guarda opciones en la web → **PHP** 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).
4. El servidor envía la señalización adecuada al **hotspot** para que TG estáticos / reflector / temporizador surtan efecto **sin** reinicio completo del hotspot (el comportamiento coincide con el flujo HBP **OPTIONS** del servidor).
Importante: el proxy envía **RPTO solo al master**, no al hotspot directamente. Si el proxy no está en el camino, necesitas otro mecanismo que aplique **OPTIONS** o ejecutas el servidor sin este camino proxy.
---
## Hash de contraseñas
**`AuthenticateUser`** usa:
```text
hash_pbkdf2('sha256', password, PBKDF2_SALT, PBKDF2_ITERATIONS)
```
almacenado en hex en **`Clients.psswd`**. Los mismos parámetros deben usarse donde se **registran** contraseñas (p. ej. **hotspot_proxy_self_service.py** en el repo de tooling / adn-dmr-server).
---
## Interfaz
- Ruta **`/self-service`** en React (`SelfService.tsx`): carga **`/api/auth/me`**, luego detalle del dispositivo para **`selected_int_id`**, edita la cadena de opciones, guarda.
- Un enlace externo **SelfCare** (p. ej. `selfcare.adn.systems`) puede aparecer en el nav como producto aparte — no es el mismo que este **self-service local**.
---
## Ver también
- [Inicio de la documentación](../README.md)
- [Configuración](configuration.md) — `SELF_SERVICE`, `DASHBOARD`, `PROXY`
- [Arquitectura](architecture.md) — proxy + canal de informes
- Detalle del proxy: **`proxy/README.md`** en adn-monitor (tabla de temporización RPTO)

@ -0,0 +1,38 @@
# Traducciones
## Diseño actual
Las páginas **MkDocs** existen en árboles paralelos:
| Idioma | Ruta | Build |
|--------|------|-------|
| Inglés | **`docs/en/`** | `mkdocs.yml` → **`site/en/`** |
| Español | **`docs/es/`** | **`mkdocs.es.yml`** → **`site/es/`** |
- **`docs/en/server/`** — ADN DMR Peer Server (guía de usuario, protocolos, desarrollo, contribución).
- **`docs/en/monitor/`** — ADN Monitor (panel, `adn-mon.yaml`, self-service).
El español replica las mismas rutas relativas bajo **`docs/es/`**.
`mkdocs.yml` usa **`docs_dir: docs/en`** e **`theme.language: en`**. **`mkdocs.es.yml`** usa **`docs_dir: docs/es`** e **`theme.language: es`**.
Genera **las dos** salidas (`mkdocs build` y `mkdocs build -f mkdocs.es.yml`). Quedan en **`site/en/`** y **`site/es/`**. Publica la carpeta **`site/`** según el mapeo de tu servidor HTTP.
Prueba local rápida: `cd site && python -m http.server` y abre **`/en/`** y **`/es/`**. Opcional: **`mkdocs-static-i18n`** más adelante para un solo build con pares de idioma por página.
## Añadir o actualizar un idioma
1. Mantén la **estructura de navegación** alineada entre locales (las mismas rutas relativas: `server/user-guide/introduction.md`, etc.).
2. Los encabezados con enlaces cruzados usan anclas explícitas **`attr_list`** `{#id}` cuando los slugs deben permanecer estables — ver páginas en español.
3. Si añades un tercer idioma, añade otro fichero MkDocs y directorio de salida siguiendo el mismo esquema.
## Escritura para traductores
- Frases **cortas y claras**.
- Evitar modismos y humor cultural.
- **Terminología** coherente (OpenBridge, BCSQ, TG, `BRIDGES`).
- **Identificadores de código** y **claves YAML** entre backticks.
## No traducido por defecto
- El **`README.md` de la raíz** del repositorio puede permanecer solo en inglés o enlazar al sitio de documentación publicado.

@ -0,0 +1,26 @@
# Arquitectura (capas limpias)
## Capas
1. **Dominio** (`src/adn_server/domain/`) — entidades, objetos de valor, errores, `Result`. Sin E/S, sin Twisted.
2. **Aplicación** (`src/adn_server/application/`) — casos de uso (`BridgeUseCases`, `VoiceUseCases`, …) y **ports** (interfaces).
3. **Infraestructura** (`src/adn_server/infrastructure/`) — config YAML, Twisted UDP/TCP, voz, persistencia, adaptadores de seguridad.
**Regla de dependencias:** infraestructura → aplicación → dominio (solo hacia dentro).
## Punto de entrada
`main.py` cablea configuración, temporizadores **LoopingCall**, fábricas para **HBPProtocol**, servidor de informes e inyecta casos de uso.
## Dónde leer código
| Tema | Ubicación |
|------|-----------|
| Enrutado de bridge, `dmrd_received`, bucle OpenBridge | `application/bridge_use_cases.py` |
| HBP / OpenBridge UDP | `infrastructure/twisted_adapters/udp_hbp.py` |
| Informes TCP | `infrastructure/twisted_adapters/report_server.py` (fábrica), eventos de bridge desde casos de uso |
| Voz / TTS | `application/voice_use_cases.py`, `infrastructure/voice/` |
## Configuración como estado compartido
Un **`config` dict** mutable se pasa por adaptadores; actualizaciones en tiempo de ejecución (opciones, `SUB_MAP`, `_bcsq` OpenBridge) permanecen visibles globalmente durante la vida del proceso.

@ -0,0 +1,11 @@
# Comportamiento y temporizadores
## Bucles de control estables
El servidor usa tareas **Twisted** `LoopingCall` para trabajo periódico: reglas de bridge, poda de flujos, refresco de opciones OpenBridge, recarga de alias, recarga de config de voz, descargas de seguridad, informes y pings de mantenimiento.
Los intervalos forman parte del **comportamiento observable** del producto (operadores e integradores pueden apoyarse en la temporización para diagnóstico). Evita **refrescos extra** o trabajo duplicado en **rutas calientes** (p. ej. manejadores por paquete para OpenBridge) cuando la misma preocupación ya la cubre el bucle programado — mantiene la carga predecible y evita aplicar reglas dos veces.
## Visibilidad de la configuración
El estado en tiempo de ejecución vive en un **`config` dict** compartido: opciones de peers, `SUB_MAP`, campos de plano de control OpenBridge (`_bcsq`, `_bcka`), y similares. Los adaptadores actualizan esta estructura; los casos de uso la leen. Así coincide con cómo se inspecciona el proceso en ejecución en logs y escenarios de soporte.

@ -0,0 +1,33 @@
# Disposición de trama DMRE v5
Esta página describe el formato en cable **OpenBridge DMRE** cuando el **byte de versión de protocolo embebido** es **5** (a menudo llamado **«OpenBridge v5»** en documentación de operadores — igual que **DMRE v5**; ver [OpenBridge](openbridge.md#dmre-and-openbridge-v5)).
Los datagramas **OpenBridge** extendidos usan el opcode **`DMRE`** (bytes `D`,`M`,`R`,`E`) y un campo **versión**. Cuando el byte de versión embebido es **> 4**, el paquete tiene **89 bytes** con **saltos** en el byte **72** y **BLAKE2b** de **73** a **89**.
## Forma corta (85 bytes)
Cuando la versión embebida es **≤ 4**, se omite **repetidor fuente**; **saltos** y **MAC** se desplazan (ver implementación en `udp_hbp.py`).
## Resumen de campos (v5 de 89 bytes)
| Región | Contenido |
|--------|-----------|
| 0:4 | Opcode `DMRE` |
| 4:5 | Secuencia |
| 5:8 | Fuente RF |
| 8:11 | ID destino |
| 11:15 | ID servidor |
| 15:16 | Bits (slot, tipo llamada, tipo trama, dtype/vseq) |
| 16:20 | Stream ID |
| 20:53 | Carga de voz |
| 53:55 | BER, RSSI |
| 55:56 | Versión de protocolo embebida |
| 56:64 | Marca de tiempo (ns, big-endian) |
| 64:68 | ID servidor de origen |
| 68:72 | Repetidor fuente (extendido v5) |
| 72:73 | Saltos |
| 73:89 | MAC BLAKE2b (16 bytes) |
**Integridad:** BLAKE2b-128 con la **passphrase** como clave; la MAC cubre los bytes **antes** del campo MAC.
La disposición de bytes en código (`infrastructure/twisted_adapters/udp_hbp.py`) es autoritativa y puede recibir aclaraciones menores con el tiempo.

@ -0,0 +1,20 @@
# HBP (HomeBrew Protocol) — DMRD
## Rol
**HBP** es el encapsulado UDP entre este servidor y sistemas **MASTER** / **PEER**. Las cargas usan el opcode **`DMRD`** (cuatro bytes ASCII) seguido de campos de voz/datos DMR (fuente RF, destino, ID de flujo, bloque AMBE, etc.).
## Autenticación
- Lado **MASTER**: RPTL → salt → RPTK → config; opciones de peer (**RPTO**) refrescan opciones del bridge.
- Lado **PEER**: conecta aguas arriba, repite autenticación, pings de mantenimiento.
Implementación: `infrastructure/twisted_adapters/udp_hbp.py` (`HBPProtocol`).
## OpenBridge frente a HBP
OpenBridge usa **`DMRD`** v1 (HMAC-SHA1) o **`DMRE`** (extendido); ver [OpenBridge](openbridge.md). Los enlaces HBP **MASTER/PEER** usan reglas clásicas **DMRD**.
## BER / RSSI
Para fuentes que no son OpenBridge, bytes opcionales **BER/RSSI** pueden añadirse tras la carga de voz de 53 bytes en ingreso; el reenvío a OpenBridge puede recortar o conservar campos según destino y reglas `to_target`.

@ -0,0 +1,70 @@
# OpenBridge (FreeBridge / DMRE)
## DMRE y «OpenBridge v5» {#dmre-and-openbridge-v5}
En cable, OpenBridge extendido usa el opcode **`DMRE`**. El **byte de versión de protocolo embebido** dentro de la trama (ver [disposición DMRE v5](dmre-v5.md)) selecciona el diseño: **versión > 4** es el formato **v5 de 89 bytes** (saltos, campo repetidor fuente, MAC BLAKE2b). En documentación y conversaciones de operadores, **«DMRE v5»** y **«OpenBridge v5»** son lo mismo: **tramas DMRE con versión embebida 5** (no el camino corto solo DMRD antiguo).
**Recomendación (red ADN Systems):** todos los enlaces inter-servidor que participen en la malla **ADN Systems** deberían usar **DMRE v5** (`PROTO_VER: 5` en YAML, que fija **VER** / versión embebida negociada) y **`ENHANCED_OBP: true`** para que **BCSQ**, **BCKA** y el control de bucle multipath se comporten igual. Los **pares** (otros servidores) deben configurarse igual. **DMRD v1** (solo HMAC) sigue soportado para interoperar con pilas antiguas, pero **no** es el modo preferido para nuevos despliegues ADN.
## Qué es OpenBridge
**OpenBridge** es un protocolo UDP entre **servidores** (y algunos gateways). Transporta voz DMR usando:
- **`DMRD`** versión 1 — carga autenticada HMAC-SHA1 (interop heredada); o
- **`DMRE`** — trama extendida con MAC **BLAKE2b**, versión embebida, marcas de tiempo, **saltos**, IDs servidor/repetidor de origen, etc. (**DMRE v5** = versión embebida 5, recomendada arriba).
Esta pila implementa el modo par **OPENBRIDGE** en **`udp_hbp.py`** y el enrutado de bridges en **`BridgeUseCases`**.
## Ingreso (DMRE)
1. Verificar **BLAKE2b** sobre el prefijo autenticado.
2. Comprobar **NETWORK_ID**, socket **TARGET** / `RELAX_CHECKS`, **slot** (TS1 para ingreso OBP).
3. Incrementar **saltos**; si **> 10**, descartar y opcionalmente enviar **BCSQ**.
4. Reconstruir un pseudo-**DMRD** para el bridge y llamar **`dmrd_received`** con metadatos de salto.
## Egreso (`send_system`)
- Construir **DMRE** o **DMRD** v1 según **VER** negociada y versión embebida.
- Preservar **saltos**, **BER/RSSI**, **source_server** / **source_rptr** según haga falta para interoperar.
## Control de bucle (malla multipath)
Para voz de **grupo** en OpenBridge:
1. **Finalizado** / **timeout 180 s** — descartar flujos obsoletos.
2. **Eco HBP** — si un sistema no-OBP ya tiene este `stream_id` en RX, esta pata OBP se trata como eco.
3. **Varios OBP** — entre patas OBP con el mismo `stream_id` y TG, **solo el `1ST` más temprano** (`min(perf_counter)`) **reenvía**; las demás paran y pueden enviar **BCSQ** si **`ENHANCED_OBP`** es true.
## BCSQ (Bridge Control — Source Quench)
- **Significado:** «No reenvíes este **`stream_id`** en esta **TG** hacia mi pata.»
- **Lo envía** el OBP **perdedor** en control de bucle hacia su **par** (no es broadcast global).
- **Se respeta** al reenviar a otro OBP: si el `_bcsq` del destino coincide, **omitir** `send_system`.
## BCKA (keepalive)
- **ENHANCED_OBP** — si el keepalive del par está obsoleto, bloquear reenvío hasta que se refresque.
## Reenvío de bridge (`to_target`)
Por fila de destino: deduplicar `(SYSTEM, TS)`, comprobar **BCSQ**, **BCKA**, **ACL**, reescribir **LC** / **TGID**, forzar patrón de bit **TS1** para OBP, llamar **`send_system`**.
## Filtros de ingreso (TG de grupo) {#ingress-filters-group-tg}
Para tráfico de **grupo** (y **vcsbk**), OpenBridge aplica **filtros TG** antes de que el flujo llegue al router de bridges. Los flujos descartados pueden disparar **BCSQ** hacia el par. Las reglas exactas difieren entre **DMRD v1** y **DMRE** en `udp_hbp.py`; en general rechazan tráfico tratado como **solo local** o **servidor incorrecto** para el destino, por ejemplo:
- TG bajas (p. ej. **≤ 79** en DMRE; DMRD v1 también combina **9990–9999**, **92–199**, **900999** en una comprobación).
- **9990–9999** y **900999** (rangos de servicio / servidor local en DMRE).
- **92–199** salvo que el ID de **servidor de origen** coincida con el ID principal de tu servidor (DMRE).
- **80–89** y **800–899** salvo que el prefijo **MCC** coincida (DMRE).
Las llamadas **privadas (unitarias)** no están sujetas al mismo bloqueo de filtro TG de grupo de la misma forma; configura **ACL** por separado.
Resumen orientado al operador: [Números especiales — ingreso OpenBridge](../user-guide/special-numbers.md#openbridge-ingress--group-tg-filters).
## Eventos de monitor (adn-monitor)
- **INGRESS** — primera vista de depuración por pata.
- **START** — canónico **después** de ganar el bucle (estado del panel).
Ver también: [disposición DMRE v5](dmre-v5.md), [Monitor](../user-guide/monitoring.md).

@ -0,0 +1,19 @@
# Créditos, licencia e historia del proyecto
## Créditos y origen
**ADN Systems DMR Peer Server** parte de **FreeDMR**, reorganizado con **arquitectura limpia** (dominio, aplicación, infraestructura) y con funciones propias de ADN: anuncios programados, **TTS**, voz bajo demanda, fusión de **`adn-voice.yaml`**, informes TCP al **ADN Monitor**, integración con **`adn-mon.yaml`**, self-service y proxy hotspot, entre otras.
**[FreeDMR Peer Server](https://gitlab.hacknix.net/hacknix/FreeDMR)** es obra de **Simon Adlem, G7RZU** ([hacknix](https://gitlab.hacknix.net/hacknix/FreeDMR)). El proyecto se describe así:
**[hblink3](https://github.com/HBLink-org/hblink3)** — HBLink para Python3. Copyright (C) 2016-2020 Cortney T. Buffington, N0MJS n0mjs@me.com (según licencia y `README` del proyecto original). FreeDMR se define como un fork de esa base de código.
## Licencia
Este proyecto se publica bajo **GNU General Public License v3** (o posterior), en la misma línea que FreeDMR y el ecosistema asociado. El texto legal completo está en el repositorio (`LICENSE`, `COPYING` si aplica) junto a las condiciones de uso y redistribución.
## Ver también
- [Introducción](introduction.md)
- [Arquitectura](../development/architecture.md)

@ -0,0 +1,28 @@
# Bridges y talkgroups
## Modelo `BRIDGES`
La tabla de bridges asocia **claves de talkgroup** (cadenas, p. ej. `"26811"`, `"#reflector"`) con **filas**. Cada fila describe:
- **`SYSTEM`** — qué sistema configurado origina o recibe esta pata.
- **`TS`** — slot temporal (1 o 2). Las fuentes OpenBridge se normalizan a **TS1** en el enrutado (`bridge_match_slot`).
- **`TGID`** — bytes de ID de destino para reescritura LC hacia esa pata.
- **`ACTIVE`** — si esta pata participa.
- **`TIMEOUT`**, **`TO_TYPE`**, **`ON`/`OFF`/`RESET`** — semántica de activación (bridges activados por usuario, reflectores, etc.).
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`).
## 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`.
## OpenBridge y visualización de TG
En OpenBridge, el **destino DMR** en el paquete puede diferir del **TGID** en una fila de bridge (remap). El monitor puede mostrar **TG RX** (recibida) frente a **TG TX** (reescrita para un destino); correlaciona por **`stream_id`**, no solo por TG.
## Contención
La voz de grupo usa **hang time**, **`STREAM_TO`** y estado de slot `TX_*` / `RX_*` para evitar transmisiones simultáneas en los mismos recursos.
Ver también: [Números especiales](special-numbers.md), [Protocolo OpenBridge](../protocols/openbridge.md).

@ -0,0 +1,219 @@
# Configuración
## Ficheros y flujo de trabajo
| Fichero | En el repo | Rol |
|---------|-------------|-----|
| `adn-server.example.yaml` | Sí | Plantilla — copiar a `adn-server.yaml` y editar. |
| `adn-server.yaml` | **No** (gitignored) | Servidor principal: sistemas, globales, logging, alias, informes. |
| `adn-voice.example.yaml` | Sí | Plantilla de voz — copiar a `adn-voice.yaml`. |
| `adn-voice.yaml` | **No** (habitual) | Voz/TTS/grabación; se fusiona en `config["VOICE"]` al arranque y **recarga en caliente** (~cada 15 s) si cambia el fichero. |
Ejecución:
```bash
python adn-server.py -c /ruta/a/adn-server.yaml
```
Opcional: `--logging LEVEL` sobrescribe `LOGGER.LOG_LEVEL`.
Si `adn-voice.yaml` está junto a `adn-server.yaml`, se carga automáticamente. También puedes poner un bloque `VOICE:` dentro de `adn-server.yaml`; el fichero separado es la forma habitual de cambiar anuncios sin tocar la config principal.
**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.
---
## Arquitectura: ¿qué es un «sistema»?
Cada entrada bajo **`SYSTEMS`** es un **enlace lógico** con nombre (endpoint UDP) que habla **HBP** (HomeBrew Protocol) con hotspots/repetidores, u **OpenBridge** con otros servidores. Los nombres son libres (`SYSTEM`, `ECHO`, `OBP-UK`, …) y se usan en logs y en la **tabla de bridges** (`BRIDGES`) para identificar por dónde entra o sale el tráfico.
Existen tres **modos**:
| Modo | Uso típico | Escucha | Conecta aguas arriba |
|------|------------|---------|----------------------|
| **MASTER** | Servidor de conferencia para uno o más hotspots/repetidores | **Sí** — `IP` / `PORT`, los peers se registran con passphrase | No (los peers se conectan a ti) |
| **PEER** | Hotspot/repetidor o servicio (p. ej. parrot) como **cliente** de un MASTER | **Sí** — `IP` / `PORT` local | **Sí** — `MASTER_IP` / `MASTER_PORT` deben apuntar al MASTER |
| **OPENBRIDGE** | Enlace a otro **servidor** por OpenBridge (DMRD v1 / DMRE) | **Sí** — `IP` / `PORT` | **Sí** — `TARGET_IP` / `TARGET_PORT` (servidor par) |
**MASTER** mantiene la tabla **`PEERS`** en tiempo de ejecución (hotspots autenticados). **PEER** mantiene **STATS** (conexión, pings). **OPENBRIDGE** usa **NETWORK_ID**, **PASSPHRASE**, **TARGET_***, **PROTO_VER** / **VER**, y opcionalmente **ENHANCED_OBP**, **RELAX_CHECKS**, **TGID_ACL**.
Un solo proceso puede ejecutar **varios** sistemas a la vez (p. ej. un MASTER para usuarios, un ECHO para parrot, un OBP hacia una red asociada).
---
## `GLOBAL`
Valores por defecto de todo el servidor. Muchas claves pueden sobrescribirse por sistema si `USE_ACL` (o similar) está activo en ese sistema.
| Clave | Significado |
|-------|-------------|
| **PING_TIME** | Intervalo (segundos) para ping / keepalive del PEER hacia MASTER. |
| **MAX_MISSED** | Cuántos pings perdidos antes de considerar el enlace PEER no saludable (depende de la implementación con STATS). |
| **USE_ACL** | Si es true, se aplican **REG_ACL**, **SUB_ACL**, **TGID_TS1_ACL**, **TGID_TS2_ACL** (tras procesarlas en tuplas internas). |
| **REG_ACL** | Control de acceso para **registro** / IDs de peer (`PERMIT:…` / `DENY:…`; ver [Cadenas ACL](#cadenas-acl)). |
| **SUB_ACL** | ACL para IDs de **suscriptor** (radio) en tráfico recibido. |
| **TGID_TS1_ACL** | ACL para **talkgroup** en **slot temporal 1**. |
| **TGID_TS2_ACL** | ACL para **talkgroup** en **slot temporal 2**. |
| **GEN_STAT_BRIDGES** | Si es true, OpenBridge puede disparar filas de bridges **estáticos** para ciertas TG (ver [Bridges y talkgroups](bridges-and-talkgroups.md)). |
| **SERVER_ID** | ID numérico del servidor; valor de 4 bytes para OpenBridge / metadatos de voz. |
| **VALIDATE_SERVER_IDS** | Si es true (ruta DMRE), los IDs de **servidor de origen** pueden comprobarse contra una lista descargada (`ALIASES` **SERVER_ID_**\*). |
| **URL_SECURITY** / **PORT_SECURITY** / **PASS_SECURITY** | Si están definidos, habilitan descarga de claves/contraseñas desde el endpoint de seguridad (ver comentarios del ejemplo). Vacío = desactivado. |
| **USERS_PASS** | Nombre de fichero JSON de contraseñas por radio (opcional). |
| **HASH_ENCRYPT** | Ruta a la clave de cifrado para el manejo del fichero de contraseñas. |
---
## `SYSTEMS` — campos comunes
Aparecen principalmente en **MASTER** (y a menudo en **PEER**). OpenBridge usa un subconjunto distinto.
| Clave | Significado |
|-------|-------------|
| **MODE** | `MASTER`, `PEER` u `OPENBRIDGE`. |
| **ENABLED** | Si es `false`, el sistema se omite. |
| **IP** / **PORT** | Dirección UDP de escucha para el listener HBP u OpenBridge de este sistema. |
| **PASSPHRASE** | Secreto compartido para autenticación HBP (MASTER ↔ PEER). Debe coincidir entre PEER y su MASTER. |
| **USE_ACL** | Sobrescribe ACL por sistema si es true (usa `REG_ACL` / `SUB_ACL` / `TGID_TS*_ACL` del sistema). |
| **GROUP_HANGTIME** | Tiempo de hang (segundos) para el estado de voz de grupo. |
| **DEFAULT_UA_TIMER** | Tiempo de espera por defecto (minutos en muchos sitios) para bridges **activados por usuario**. |
| **ANNOUNCEMENT_LANGUAGE** | Carpeta de idioma por defecto bajo `Audio/<lang>/` para mensajes en este sistema. |
| **ALLOW_UNREG_ID** | Si se permiten IDs de suscriptor no registrados (MASTER). |
---
## `SYSTEMS` — MASTER
| Clave | Significado |
|-------|-------------|
| **REPEAT** | Si es true, el tráfico recibido puede **repetirse** a otros peers conectados al MASTER (comportamiento típico de conferencia). |
| **MAX_PEERS** | Máximo de hotspots conectados. |
| **EXPORT_AMBE** | Flag de exportación AMBE (si está habilitado en el build). |
| **SINGLE_MODE** | Afecta a OPTIONS / expansión del generador (estilo un solo usuario). |
| **VOICE_IDENT** | Habilita **identificación por voz** periódica cuando se cumplen condiciones (ver `IdentUseCases`). |
| **TS1_STATIC** / **TS2_STATIC** | Listas estáticas de TG separadas por comas, enviadas vía manejo OPTIONS (ver `options_config`). |
| **DEFAULT_REFLECTOR** | Número de **reflector** por defecto para bridges de marcado `#` (0 = ninguno). |
| **OVERRIDE_IDENT_TG** | TG opcional para ident por voz en lugar de all-call. |
| **GENERATOR** | Si es **> 1**, este MASTER se expande en **`NAME-0`**, **`NAME-1`**, … con puertos consecutivos (ver `expand_generator` en código). |
**MASTER** escucha conexiones PEER; cada peer autenticado se guarda en **`PEERS`** en tiempo de ejecución.
---
## `SYSTEMS` — PEER
Un **PEER** conecta **saliente** hacia un **MASTER** y escucha localmente para la radio o la app.
| Clave | Significado |
|-------|-------------|
| **MASTER_IP** / **MASTER_PORT** | Dirección del **MASTER** con el que registrarse (debe coincidir con `IP`/`PORT` de ese MASTER). |
| **RADIO_ID** | ID de este peer en HBP (4 bytes). |
| **CALLSIGN**, **RX_FREQ**, **TX_FREQ**, **COLORCODE**, **LATITUDE**, … | Campos del payload RPT enviados al MASTER en el registro (anchos fijos en protocolo). |
| **OPTIONS** | Cadena / línea de opciones (p. ej. `TS2=9990;`) para TG estáticas / comportamiento. |
| **LOOSE** | Flag de manejo relajado donde aplique. |
El ejemplo **parrot** (`adn-parrot.example.yaml`) es un PEER que se une al MASTER **ECHO**: mismo **PASSPHRASE**, **MASTER_PORT** = **PORT** del ECHO. Ver [Parrot](parrot.md).
---
## `SYSTEMS` — OPENBRIDGE
| Clave | Significado |
|-------|-------------|
| **NETWORK_ID** | Debe coincidir con el **NETWORK_ID** del par en paquetes OpenBridge. |
| **TARGET_IP** / **TARGET_PORT** | Par OpenBridge remoto (UDP). |
| **TGID_ACL** (o **TG1_ACL**) | ACL de talkgroup para OpenBridge (a menudo estilo `DENY:0-82,…` por rangos). |
| **RELAX_CHECKS** | Permitir paquetes cuando el socket del par no coincide estrictamente con `TARGET` (usar con cuidado). |
| **ENHANCED_OBP** | Habilita **BCSQ** / **BCKA** y control de bucle multipath — **debería ser `true`** en enlaces inter-servidor ADN Systems (ver abajo). |
| **PROTO_VER** | Versión de protocolo **DMRE** embebida; **`5`** selecciona **DMRE / OpenBridge v5** (trama de 89 bytes, BLAKE2b). El valor por defecto en código es **5**; usa **5** en nuevos despliegues ADN. |
**Recomendación ADN Systems:** para cada par OpenBridge en la malla ADN, configura **`PROTO_VER: 5`** (DMRE v5) y **`ENHANCED_OBP: true`**. Alinea la misma configuración en **ambos** extremos. Los pares solo **DMRD** v1 siguen siendo posibles por compatibilidad, pero no son el modo preferido para la red.
Filtros de ingreso y control de bucle: [Protocolo OpenBridge](../protocols/openbridge.md) (incl. [DMRE frente a OpenBridge v5](../protocols/openbridge.md#dmre-and-openbridge-v5)) y [Números especiales — ingreso OpenBridge](special-numbers.md#openbridge-ingress--group-tg-filters).
---
## `BRIDGES` (tiempo de ejecución)
La tabla de bridges asocia claves TG con filas de enrutado. Está **en memoria** en el proceso en ejecución.
El cargador YAML **no** carga un bloque `BRIDGES:` de nivel superior desde `adn-server.yaml` en el router hoy — las filas iniciales se crean en código (p. ej. arranque **9990 / ECHO** cuando existe un sistema **ECHO**), luego **OPTIONS**, bridges **activados por usuario**, bridges **estáticos** y la lógica **OpenBridge** añaden filas con el tiempo.
Para el modelo conceptual (ACTIVE, TS, TGID, timeouts): [Bridges y talkgroups](bridges-and-talkgroups.md).
---
## `REPORTS`
Canal TCP de informes para **adn-monitor** (o paneles compatibles).
| Clave | Significado |
|-------|-------------|
| **REPORT** | Activar/desactivar envío. |
| **REPORT_INTERVAL** | Intervalo de envío periódico (segundos). |
| **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).
---
## `LOGGER`
Implementado en `infrastructure/logging_config.py` (`setup_logging`). Los valores se leen del bloque **`LOGGER`** (o `--logging` solo para **LOG_LEVEL**).
| Clave | Significado |
|-------|-------------|
| **LOG_HANDLERS** | Lista separada por comas de **tokens** de manejador (espacios alrededor de las comas están bien). Cada token elige salidas; puedes combinar varios. Valores reconocidos: **`console-timed`** o **`console`** — log a **stderr** con formato `LEVEL asctime message`; **`file-timed`** o **`file`** — log a **LOG_FILE** con el mismo formato (UTF-8). **Por defecto** si falta: `console-timed`. Ejemplos: solo `console-timed`; solo `file-timed`; `console-timed,file-timed` para consola y fichero. |
| **LOG_FILE** | Ruta usada cuando **`file-timed`** o **`file`** está en **LOG_HANDLERS**. Si falta, el código usa por defecto `/dev/null`. Si la ruta es **`/dev/null`**, los manejadores de fichero **no** se adjuntan aunque estén listados. Si no se puede abrir el fichero (permisos, directorio inexistente), se escribe un aviso a stderr y el logging continúa sin ese manejador de fichero. |
| **LOG_LEVEL** | Nivel del logger raíz: **`DEBUG`**, **`INFO`**, **`WARNING`**, **`ERROR`**, **`CRITICAL`** (sin distinguir mayúsculas; por defecto **INFO**). Nombres desconocidos caen en **INFO**. Puedes sobrescribir al arranque con **`python adn-server.py --logging LEVEL`** (mismos nombres). Hay un nivel personalizado **`TRACE`** registrado para llamadas ocasionales `logger.trace(...)`; usa **`DEBUG`** para diagnóstico detallado en operación normal. |
| **LOG_NAME** | Nombre del logger devuelto a la aplicación (por defecto **`ADN`**). No cambia la lista de manejadores; selecciona qué logger nombrado recibe el nivel configurado. |
---
## `ALIASES`
Descargas y ficheros locales para **IDs de peer**, **IDs de suscriptor**, **etiquetas de talkgroup**, lista opcional de **IDs de servidor**, **checksums** y **claves**. Usados en paneles, validación y descargas de seguridad opcionales.
| Clave | Significado |
|-------|-------------|
| **PATH** | Directorio base para JSON/TSV/pickle. |
| **TRY_DOWNLOAD** | Si se deben obtener desde URLs cuando están obsoletos. |
| **PEER_FILE** / **SUBSCRIBER_FILE** / **TGID_FILE** | Nombres de ficheros locales. |
| **\*_URL** | Orígenes remotos para descargas. |
| **SUB_MAP_FILE** | Ruta pickle para **SUB_MAP** (enrutado de llamadas privadas); nombre por defecto si está vacío. |
| **STALE_DAYS** | Umbral de refresco para descargas. |
---
## `VOICE` (desde `adn-voice.yaml` o inline)
Fusionado en `config["VOICE"]`. Ver [Voz, anuncios y TTS](voice-and-tts.md) y `adn-voice.example.yaml`.
---
## Cadenas ACL
Procesadas por `acl_build`: `PERMIT:` o `DENY:` seguido de IDs o rangos separados por comas.
Ejemplos:
- `PERMIT:ALL` — permitir todos los IDs en rango.
- `DENY:1` — denegar solo el ID 1.
- `DENY:0-82,9990-9999` — denegar los rangos listados.
Las ACL globales aplican cuando `USE_ACL` es true; OpenBridge puede usar **TGID_ACL** en el sistema OBP.
---
## Entorno Python
Usa el intérprete del proyecto (ver reglas del workspace), p. ej. `python3.11` de pyenv, para un comportamiento alineado con producción.
---
## Ver también
- [Introducción](introduction.md) — rol del servidor.
- [Bridges y talkgroups](bridges-and-talkgroups.md) — semántica de `BRIDGES`.
- [Números especiales](special-numbers.md) — TG e IDs reservados.
- [Parrot](parrot.md) — ejemplo PEER (proceso parrot).

@ -0,0 +1,36 @@
# Introducción
## Propósito
Este servicio es un **peer y bridge DMR**. Implementa:
- **HBP** por UDP hacia sistemas **MASTER** y **PEER** (tramas DMRD, autenticación, pings).
- **OpenBridge** por UDP hacia otras redes — **DMRE v5** (versión embebida 5, BLAKE2b, saltos) es el modo **recomendado** entre servidores en ADN; **DMRD** v1 sigue para interoperabilidad heredada (ver [OpenBridge](../protocols/openbridge.md#dmre-and-openbridge-v5)).
La configuración es **YAML** (`adn-server.yaml`), fusionada en tiempo de ejecución con ajustes de voz opcionales (`adn-voice.yaml`). La plantilla incluida es `adn-server.example.yaml`.
## Diseño
Enrutado, temporizadores, control de bucle OpenBridge y manejo de protocolo están en módulos de **aplicación** e **infraestructura** detrás de **ports** estables; la capa de **dominio** contiene tipos y reglas sin E/S. Así el sistema es más fácil de seguir y extender.
## Subsistemas principales
| 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. |
| **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 `BRDG_EVENT`. |
## Programas relacionados
- **Parrot / reproducción** — punto de entrada aparte (`adn-parrot.py`) para grabar y reproducir; ver [Parrot](parrot.md).
## Siguientes pasos
- [Configuración](configuration.md) — ficheros, `GLOBAL`, **MASTER** / **PEER** / **OPENBRIDGE**, ACL, informes, 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.
- [ADN Monitor](../../monitor/index.md) — panel, `adn-mon.yaml`, self-service (repo aparte, desplegado con el servidor).
- [Créditos y licencia](attribution.md) — ADN → FreeDMR → hblink3, licencia.

@ -0,0 +1,29 @@
# Monitor e informes
## Canal TCP de informes
Cuando **`REPORTS`** está habilitado en la config del servidor, el **ADN DMR Peer Server** escucha en TCP y los **clientes de informes** (típicamente **adn-monitor**) se conectan y reciben:
- **CONFIG_SND** / **BRIDGE_SND** — instantáneas pickle de sistemas y bridges.
- **BRDG_EVENT** — eventos de texto para llamadas (`GROUP VOICE`, `PRIVATE VOICE`, etc.).
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 Python, WebSocket, API PHP, proxy y self-service opcionales).
## Semántica del monitor en OpenBridge
- **`GROUP VOICE,INGRESS,RX`** — primera aparición de un flujo en una pata OpenBridge (depuración; visibilidad completa en logs).
- **`GROUP VOICE,START,RX`** — inicio **canónico** tras **control de bucle** (alimenta chips del panel / CTABLE).
- **`GROUP VOICE,END,…`** — fin de llamada; variantes RX/TX según dirección.
El panel muestra el estado **operativo** desde **START** (canónico); el **log del Monitor** muestra **INGRESS** más **START** para depurar duplicados en malla.
## Requisitos
- Conectividad de red desde el **host del monitor** al **`REPORTS.REPORT_PORT`** del servidor (y la lista **`REPORT_CLIENTS`** del servidor debe incluir al monitor si se usa).
- **adn-monitor** `ADN_CONNECTION.ADN_IP` / **`ADN_PORT`** debe coincidir con el servidor — ver [Configuración del monitor](../../monitor/configuration.md#adn_connection).
## Self-service y hotspots
Los operadores que editan **opciones de dispositivo** desde el panel usan el flujo **self-service** (MySQL **`Clients`**, proxy **RPTO**). Está documentado en [Self-service](../../monitor/self-service.md); **no** forma parte solo del binario del peer server. Para la configuración del **proxy hotspot** (`PROXY` en `adn-mon.yaml`), enlace al rango **UDP** del peer server y arranque del proceso, ver [Proxy hotspot](../../monitor/hotspot-proxy.md).

@ -0,0 +1,22 @@
# Parrot (reproducción)
## Qué es
**Parrot** es un **punto de entrada separado** (`adn-parrot.py` / `parrot_main`) que graba voz de **grupo** entrante y la reproduce (eco / parrot), independiente del proceso principal del bridge.
## Configuración
- Copiar **`adn-parrot.example.yaml`** → **`adn-parrot.yaml`** (no versionada).
- Ejecutar:
```bash
python adn-parrot.py -c adn-parrot.yaml
```
## Relación con TG 9990 / ECHO
El servidor principal puede exponer un bridge **ECHO** en **TG 9990** para eco en banda. **Parrot** es un **servicio independiente** con su propia config — usa uno u otro según el despliegue.
## Documentación
Esta página es el resumen incluido en el repositorio; amplía las notas de despliegue localmente si hace falta.

@ -0,0 +1,24 @@
# Llamadas privadas
## Descripción general
Las llamadas **unitarias (privadas)** usan un camino distinto a la voz de **grupo**. El router usa **`SUB_MAP`** (suscriptor → último sistema/slot/tiempo conocido) y reglas de colisión para decidir si y dónde reenviar.
## SUB_MAP
- Se rellena cuando las estaciones registran tráfico; persiste vía ruta pickle **`SUB_MAP`** configurada bajo **`ALIASES`**.
- Sirve para resolver **ID de radio de destino** a un **sistema destino** y **slot** para el reenvío privado.
## OpenBridge frente a MASTER
El manejo privado usa ramas CSBK/datos/unit, búsqueda `SUB_MAP` y comprobaciones de slot ocupado donde aplique (ver `BridgeUseCases` en el código).
## TG / ID 4000 (unitaria)
Como en [Números especiales](special-numbers.md), una llamada **privada** a **4000** desactiva dinámicos y **no** se trata como ruta privada normal.
## Informes
Los eventos privados **START/END** pueden emitirse al cliente TCP de informes si **`REPORTS.REPORT`** está habilitado, análogo a voz de grupo (forma `PRIVATE VOICE,...` donde esté implementado).
Para detalles de ingreso de protocolo, ver [HBP](../protocols/hbp.md) y los casos de uso de bridge en código (`BridgeUseCases._pvt_call_received`).

@ -0,0 +1,103 @@
# Números especiales (TG / IDs)
Varios **IDs de destino** están reservados para **control o servicios**. Se gestionan en capas de protocolo y/o en el router de bridges, no como tráfico de grupo normal.
## ID 5000 — fuente de voz del servidor (no es «TG de anuncio») {#id-5000--server-voice-source-not-announcement-tg}
**Importante:** **5000** es el **ID de fuente RF** que el servidor usa cuando **transmite** voz automatizada. Las radios y paneles muestran **ID de llamada 5000** en ese tráfico.
| Tráfico | Destino en el paquete DMR | Notas |
|---------|---------------------------|--------|
| **AMBE programado** (`ANNOUNCEMENTS`) | El **`TG`** que definas en `adn-voice.yaml` | ID de fuente **5000**. |
| **TTS** (`TTS_ANNOUNCEMENTS`) | Igual — **`TG`** configurado | ID de fuente **5000**. |
| **Bajo demanda** (TG **9991–9999**) | **TG 9** | Clips informativos cortos; fuente **5000** (ver [Voz, anuncios y TTS](voice-and-tts.md)). |
| **Desconectado / reflector** | **TG 9** | Fuente **5000**. |
| **Ident por voz** | **All-call** (`16777215`) o **`OVERRIDE_IDENT_TG`** si está definido | Fuente **5000**. |
**No** «monitorizas TG 5000» para oír anuncios programados: monitorizas el **TG de anuncio configurado** (p. ej. 2, 9, 26811). **5000** aparece como **ID del transmisor** en esas llamadas.
### TG de destino 5000 (grupo entrante)
Si llega una llamada de grupo con **TG de destino 5000** y **no** hay fila `BRIDGES` existente, el servidor **no** crea automáticamente un bridge activado por usuario para esa TG (misma clase que IDs **0–4**, **9**, **4000**). Para llevar tráfico a TG 5000 necesitas una fila de bridge **explícita**.
## TG 9 — carril de servicio local (mensajes y cableado de bridge) {#tg-9-local-service-lane-prompts-and-bridge-plumbing}
**TG 9** se usa de dos formas: lo que **oyen** los operadores con mensajes cortos del servidor, y cómo se **conectan** filas internas del bridge.
### Lo que oyes (voz de salida del servidor)
Para reproducción **bajo demanda** (tras marcar **9991–9999**) y para líneas de voz de **desconectado / reflector**, el servidor transmite paquetes de **grupo** con:
- **ID de fuente 5000**
- **TG de destino 9**
- **Timeslot 2** (el código usa el slot **TS2** para ese hotspot)
Sigue una convención habitual **HomeBrew / conferencia**: mantener el audio **local de servicio** corto en **TG 9 / TS2** para separarlo del tráfico QSO normal en tu TG principal (a menudo TS1). Los hotspots deben pasar **TS2** y estar en una configuración donde **TG 9** no esté bloqueado, o esos mensajes no se oirán.
**Rojo frente a local:** En **OpenBridge**, el tráfico de **grupo entrante** a **TG 9** está en el rango **≤ 79** «local / repetidor» y **no** entra al bridge desde la malla IP (se descarta en ingreso). Los mensajes del servidor usan el **camino HBP local** hacia el hotspot/repetidor, no un TG de área amplia puenteado. Excepción: solo si **añades** filas `BRIDGES` que reenvíen TG 9 podría salir ese tráfico a otro sitio — no es lo por defecto.
Los **anuncios programados** y **TTS** usan el **`TG`** que configures en `adn-voice.yaml` — **no** se fuerzan a TG 9 salvo que tú configures ese TG.
### Comportamiento del router (TG reservada)
- **No** hay bridge activado por usuario automático si alguien transmite a **TG 9** y no existe fila `BRIDGES` (misma clase que **0–4**, **4000**, **5000**, etc.).
- Con **`GEN_STAT_BRIDGES`**, la creación automática de bridge **STAT** desde OpenBridge **no** aplica al TG de destino **9** (excluido a propósito).
- El **bucle de depuración de bridges** elimina bridges de conferencia inválidos keyed en **9** (y **0**–**8**) para que no se acumulen bridges de un dígito erróneos.
### Tabla de bridges (avanzado)
Muchas filas **reflector / marcado** guardan **`TGID` = 9** en **TS2** como **destino de pata** interno para enganchar el camino dinámico al TG real — es cableado dentro de `BRIDGES`, no un «número al que llamar» como un TG nacional normal.
## TG / ID 4000 — desactivar bridges dinámicos
**Propósito:** borrar **bridges dinámicos activados por usuario** para el sistema que recibe la llamada.
**Comportamiento:**
- 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.
Úsalo cuando los operadores necesiten **reiniciar** el enrutado dinámico sin reiniciar el servidor.
## TG 9991–9999 — audio informativo / bajo demanda
**Propósito:** **reproducir** ficheros AMBE pregenerados («ondemand») (p. ej. información de la estación, ayuda).
**Comportamiento:**
- Dispara manejo tipo **`playFileOnRequest`**: mapea los últimos dígitos al nombre de fichero bajo el árbol de audio configurado.
- Funciona desde rutas **MASTER** y **PEER**.
El **audio** se envía con **ID de fuente 5000** y **TG de destino 9** en el flujo generado. Estructura de ficheros: [Voz, anuncios y TTS](voice-and-tts.md).
## TG 9990 — eco / parrot (en banda)
**Propósito:** las filas de bridge para **eco** suelen usar **9990** con el sistema **ECHO** (ver `BRIDGES` y opciones en tu YAML).
**Nota:** Un **parrot independiente** también está disponible como proceso aparte — [Parrot](parrot.md).
## Llamada privada al ID 4000
Una llamada **unitaria** a **4000** se trata solo como **desconexión de dinámicos**; **no** se enruta como llamada privada normal.
## Ingreso OpenBridge — filtros de TG de grupo {#openbridge-ingress--group-tg-filters}
En **OpenBridge** (**DMRD** v1 y **DMRE**), el tráfico de **grupo** **no unitario** hacia ciertas TG puede **descartarse** antes del bridge (con **BCSQ** donde aplique). Las reglas difieren ligeramente entre DMRD y DMRE; incluyen TG de número bajo (p. ej. **≤ 79**), **9990–9999**, **900999**, rangos **92–199** (frente al servidor de origen), y rangos relacionados con MCC (**80–89**, **800–899**). Detalle: [Protocolo OpenBridge](../protocols/openbridge.md#ingress-filters-group-tg).
## TG prohibidos / reservados (creación de bridges)
Muchos IDs pequeños (0–5, 9, etc.) y el rango de **servicio 999x** quedan excluidos de ciertos caminos **automáticos** de creación de bridges; **5000** y **4000** están en el conjunto «sin bridge UA automático» si no existe fila. Los conjuntos exactos están definidos en el router de bridges y el manejo de opciones en el código.
## Tabla resumen
| ID / rango | Rol |
|------------|-----|
| **5000** (fuente) | Voz generada por el servidor (anuncios, TTS, mensajes, ident) — **ID de llamada** en receptores |
| **5000** (destino) | Sin bridge UA automático si falta en `BRIDGES` |
| **4000** (grupo) | Desactivar bridges dinámicos |
| **4000** (unitaria) | Desconectar dinámicos; no enrutada como PC |
| **9991–9999** | Audio informativo / bajo demanda (TG de disparo); reproducción usa fuente **5000** → TG **9** (TS2) |
| **9** | Carril de servicio/mensajes (audio corto del servidor); reservada para auto-bridges; **TGID** interno en patas TS2 |
| **9990** | TG de bridge de eco (con sistema ECHO) |
| **16777215** | All-call (destino por defecto de ident por voz salvo sobrescritura) |

@ -0,0 +1,57 @@
# Voz, anuncios y TTS
## Ficheros de configuración
- **`adn-voice.yaml`** (opcional, no versionada) — fusionada en `config["VOICE"]`.
- Recarga en caliente por **mtime** del fichero (~bucle de 15 s).
Plantilla: `adn-voice.example.yaml`.
## Identidad de voz del servidor (ID 5000)
Toda la voz **originada por el servidor** usa **ID de fuente RF 5000** en el flujo DMR para que los clientes distingan el tráfico de infraestructura del de usuarios:
- **ANNOUNCEMENTS** y **TTS_ANNOUNCEMENTS** programados (destino = el campo **`TG`** de cada ítem).
- Clips **bajo demanda** disparados marcando **9991–9999** (la reproducción usa TG de destino **9** en **TS2**; sigues marcando **999x** para solicitar el fichero).
- Líneas de voz de **desconectado / reflector** (igual: **TG 9** / **TS2**).
Ver [TG 9 — carril de servicio local](special-numbers.md#tg-9-local-service-lane-prompts-and-bridge-plumbing) por qué se usa TG 9 y qué debe estar habilitado en el hotspot.
- **Ident por voz** (destino **all-call** o **`OVERRIDE_IDENT_TG`**).
Ver [Números especiales — ID 5000](special-numbers.md#id-5000--server-voice-source-not-announcement-tg) para la tabla completa.
## Funciones
| Función | Descripción |
|---------|-------------|
| **Anuncios programados** | Frases AMBE desde `Audio/<lang>/ondemand/` según horario / intervalo. |
| **Anuncios TTS** | Texto → gTTS → MP3 → ffmpeg → WAV → vocoder → AMBE (ver tubería abajo). |
| **Ident por voz** | Identificación periódica (opcional); usa `VOICE_IDENT` en la config principal del servidor y `OVERRIDE_IDENT_TG` / all-call. |
| **Grabación** | Si está habilitada, graba AMBE a disco para tráfico en **`RECORDING_TG`** / **`RECORDING_TIMESLOT`** (ver plantilla `adn-voice.example.yaml`). |
| **Bajo demanda (9991–9999)** | Ver [Números especiales](special-numbers.md). |
## Tubería TTS (resumen)
1. Texto fuente en `.txt` bajo `Audio/<idioma>/ondemand/`.
2. **gTTS** produce **MP3**.
3. **ffmpeg** convierte a **WAV**.
4. Comando vocoder o **AMBEServer** produce **AMBE** para transmitir.
**ffmpeg** debe estar instalado en el sistema operativo.
Configura **`TTS_VOCODER_CMD`** o **`TTS_AMBESERVER_HOST`** / **`TTS_AMBESERVER_PORT`** en `adn-voice.yaml` para codificación; ver comentarios en `adn-voice.example.yaml`.
## Anti-colisión (QSO) con anuncios
Al programar anuncios, el servidor puede **esperar** si los slots objetivo están ocupados, **descartar** objetivos si aparece un QSO en vivo a mitad, y solo marcar estado de anuncio **horario** tras una lista de objetivos con éxito — evita pisar tráfico en vivo.
## Cola de emisión
Las **emisiones** en paralelo en **TG distintas** pueden ejecutarse a la vez; las emisiones en el **mismo TG** se **serializan** para que un anuncio termine antes que otro en ese TG.
## Ver también
- [Configuración](configuration.md) — rutas de ficheros de voz.
- [Parrot](parrot.md) — servicio de reproducción separado.
- [Números especiales](special-numbers.md) — 5000, 999x, comportamiento relacionado con grabación.

@ -0,0 +1,84 @@
# MkDocs — documentación en español. Build: mkdocs build -f mkdocs.es.yml
site_name: ADN Systems
site_description: ADN DMR Peer Server, ADN Monitor, HBP, OpenBridge, self-service.
site_author: ADN Server contributors
docs_dir: docs/es
site_dir: site/es
theme:
name: material
language: es
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- content.code.copy
- search.suggest
- search.highlight
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
toggle:
icon: material/brightness-7
name: Modo oscuro
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
toggle:
icon: material/brightness-4
name: Modo claro
plugins:
- search
markdown_extensions:
- attr_list
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- admonition
- tables
- toc:
permalink: true
- pymdownx.details
nav:
- Inicio: README.md
- Servidor (peer):
- Guía de usuario:
- Introducción: server/user-guide/introduction.md
- Configuración: server/user-guide/configuration.md
- Bridges y talkgroups: server/user-guide/bridges-and-talkgroups.md
- Números especiales (4000, 999x, eco): server/user-guide/special-numbers.md
- 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
- Parrot (reproducción): server/user-guide/parrot.md
- Créditos y licencia: server/user-guide/attribution.md
- Protocolos:
- HBP (DMRD): server/protocols/hbp.md
- OpenBridge (DMRE): server/protocols/openbridge.md
- Trama DMRE v5: server/protocols/dmre-v5.md
- Desarrollo:
- Arquitectura: server/development/architecture.md
- Comportamiento y temporizadores: server/development/behaviour-and-timers.md
- Contribuir:
- Traducciones: server/contributing/translations.md
- Monitor:
- Descripción general: monitor/index.md
- Configuración (adn-mon.yaml): monitor/configuration.md
- Proxy hotspot: monitor/hotspot-proxy.md
- Arquitectura e implantación: monitor/architecture.md
- Self-service: monitor/self-service.md
extra:
alternate:
- name: English
link: /en/
lang: en
- name: Español
link: /es/
lang: es
social: []

@ -0,0 +1,84 @@
# MkDocs site — public documentation (English under docs/en). Build: pip install -e '.[docs]' && mkdocs build
site_name: ADN Systems
site_description: ADN DMR Peer Server, ADN Monitor, HBP, OpenBridge, self-service.
site_author: ADN Server contributors
docs_dir: docs/en
site_dir: site/en
theme:
name: material
language: en
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- content.code.copy
- search.suggest
- search.highlight
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
toggle:
icon: material/brightness-4
name: Switch to light mode
plugins:
- search
markdown_extensions:
- attr_list
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- admonition
- tables
- toc:
permalink: true
- pymdownx.details
nav:
- Home: README.md
- Server (peer):
- User guide:
- Introduction: server/user-guide/introduction.md
- Configuration: server/user-guide/configuration.md
- Bridges and talkgroups: server/user-guide/bridges-and-talkgroups.md
- Special numbers (4000, 999x, echo): server/user-guide/special-numbers.md
- 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
- Parrot (playback): server/user-guide/parrot.md
- Credits & license: server/user-guide/attribution.md
- Protocols:
- HBP (DMRD): server/protocols/hbp.md
- OpenBridge (DMRE): server/protocols/openbridge.md
- DMRE v5 frame layout: server/protocols/dmre-v5.md
- Development:
- Architecture: server/development/architecture.md
- Behaviour and timers: server/development/behaviour-and-timers.md
- Contributing:
- Translations: server/contributing/translations.md
- Monitor:
- Overview: monitor/index.md
- Configuration (adn-mon.yaml): monitor/configuration.md
- Hotspot proxy: monitor/hotspot-proxy.md
- Architecture and deployment: monitor/architecture.md
- Self-service: monitor/self-service.md
extra:
alternate:
- name: English
link: /en/
lang: en
- name: Español
link: /es/
lang: es
social: []

@ -23,6 +23,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest>=7", "pytest-cov"] dev = ["pytest>=7", "pytest-cov"]
docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "pymdown-extensions>=10.3"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]

@ -0,0 +1,4 @@
# Optional: documentation site (MkDocs). Install: pip install -r requirements-docs.txt
mkdocs>=1.6
mkdocs-material>=9.5
pymdown-extensions>=10.3
Loading…
Cancel
Save

Powered by TurnKey Linux.