refactor: rename parrot deployment surface to echo

Unify playback naming on ECHO across CLI (--echo), adn-echo configs,
systemd unit, docs, and infrastructure/echo runtime.
pull/1/head
Rodrigo Pérez 4 months ago
parent a0bf476d6d
commit 04681bab14

2
.gitignore vendored

@ -4,7 +4,7 @@
# Production config (use *.example.yaml as template) # Production config (use *.example.yaml as template)
adn-server.yaml adn-server.yaml
adn-parrot.yaml adn-echo.yaml
adn-voice.yaml adn-voice.yaml
*.local.yaml *.local.yaml
*.local.yml *.local.yml

@ -11,7 +11,7 @@ First v2 release candidate since **1.0.0** (~70 commits). HBP/OpenBridge on-wire
### Added ### Added
- **Report v2** — JSON HELLO, slim `dashboard_state` TCP wire (no full topology/routing snapshots to monitor), bounded report queue, optional MQTT. - **Report v2** — JSON HELLO, slim `dashboard_state` TCP wire (no full topology/routing snapshots to monitor), bounded report queue, optional MQTT.
- **Unified binary** — `adn-server.py` with `--parrot`, `--doctor`, `--no-proxy`; wiring in `bootstrap/peer_server.py`. - **Unified binary** — `adn-server.py` with `--echo`, `--doctor`, `--no-proxy`; wiring in `bootstrap/peer_server.py`.
- **Integrated proxy** — in-process UDP fan-in (`PROXY`), inject-only MASTER, self-service MySQL OPTIONS, peer blacklist/timers parity with legacy proxy. - **Integrated proxy** — in-process UDP fan-in (`PROXY`), inject-only MASTER, self-service MySQL OPTIONS, peer blacklist/timers parity with legacy proxy.
- **Subscription routing** — `SubscriptionStore` + `SubscriptionRouter` as runtime authority; store-native timers, OPTIONS/static TG, in-band ON/OFF; `MeshCodecRegistry` on OpenBridge. - **Subscription routing** — `SubscriptionStore` + `SubscriptionRouter` as runtime authority; store-native timers, OPTIONS/static TG, in-band ON/OFF; `MeshCodecRegistry` on OpenBridge.
- **Inject-only production path** — per-peer OPTIONS downlink filter, monitor topology expansion (virtual SYSTEM slots), CONFIG push on peer connect. - **Inject-only production path** — per-peer OPTIONS downlink filter, monitor topology expansion (virtual SYSTEM slots), CONFIG push on peer connect.
@ -26,14 +26,14 @@ First v2 release candidate since **1.0.0** (~70 commits). HBP/OpenBridge on-wire
### Fixed ### Fixed
- OpenBridge packet control and rate-limit parity with legacy. - OpenBridge packet control and rate-limit parity with legacy.
- Parrot playback when sequence byte wraps; echo TG 9990 bootstrap. - Echo playback when sequence byte wraps; echo TG 9990 bootstrap.
- Inject TG 4000: clear dynamics once per PTT per peer. - Inject TG 4000: clear dynamics once per PTT per peer.
- Unit data (ARS/LRRP) downlink for 7-digit private calls; monitor TS chip (no spurious `PRIVATE VOICE` on unit data). - Unit data (ARS/LRRP) downlink for 7-digit private calls; monitor TS chip (no spurious `PRIVATE VOICE` on unit data).
- OBP → HBP voice forwarding and downlink CPU under multi-peer inject load. - OBP → HBP voice forwarding and downlink CPU under multi-peer inject load.
### Removed ### Removed
- Standalone **`adn-parrot.py`** — use `adn-server.py --parrot`. - Standalone **`adn-parrot.py`** — use `adn-server.py --echo`.
- Periodic **26 s `options_config_loop`**. - Periodic **26 s `options_config_loop`**.
- Legacy pickle CONFIG/BRIDGE snapshots on the **server → monitor** wire (monitor uses v2 slim ingest). - Legacy pickle CONFIG/BRIDGE snapshots on the **server → monitor** wire (monitor uses v2 slim ingest).
@ -54,7 +54,7 @@ First stable public release.
- HBP MASTER/PEER and OpenBridge forwarding with legacy parity (loops, BCSQ, packet control). - HBP MASTER/PEER and OpenBridge forwarding with legacy parity (loops, BCSQ, packet control).
- Talker Alias: DMRA packets and embedded LC overlay (UTF-8 / ISO-8859-1 / 7-bit formats). - Talker Alias: DMRA packets and embedded LC overlay (UTF-8 / ISO-8859-1 / 7-bit formats).
- Voice: announcements, TTS pipeline, on-demand AMBE playback, recording. - Voice: announcements, TTS pipeline, on-demand AMBE playback, recording.
- Parrot entrypoint (`adn-parrot.py`) for TG 9990 echo/playback. - Echo playback entrypoint (`adn-server.py --echo`, `adn-echo.yaml`) for TG 9990.
- Monitor report TCP: HELLO JSON (mode v2), CONFIG_SND / BRIDGE_SND (pickle), BRDG_EVENT. - Monitor report TCP: HELLO JSON (mode v2), CONFIG_SND / BRIDGE_SND (pickle), BRDG_EVENT.
- Hot reload: `adn-server.yaml` (SIGHUP), `adn-voice.yaml` (15 s loop), log reopen (SIGUSR2). - Hot reload: `adn-server.yaml` (SIGHUP), `adn-voice.yaml` (15 s loop), log reopen (SIGUSR2).
- MkDocs user guide (EN/ES). - MkDocs user guide (EN/ES).
@ -62,7 +62,7 @@ First stable public release.
### Fixed (highlights since parity baseline) ### Fixed (highlights since parity baseline)
- OpenBridge packet control and ingress timing. - OpenBridge packet control and ingress timing.
- Parrot playback sequence preservation on long QSOs. - Echo playback sequence preservation on long QSOs.
- Config validator accepts numeric MMDVM option fields in YAML. - Config validator accepts numeric MMDVM option fields in YAML.
- OBP END/TX reporting and STATUS lifecycle alignment with legacy. - OBP END/TX reporting and STATUS lifecycle alignment with legacy.

@ -70,15 +70,15 @@ python adn-server.py --doctor # config, ports, peers (exit 1 on errors)
python adn-server.py --no-proxy # disable integrated PROXY python adn-server.py --no-proxy # disable integrated PROXY
``` ```
## Parrot (playback) ## Echo (playback)
Separate process (same binary) records group voice and plays it back on TG 9990. Separate process (same binary) records group voice and plays it back on TG 9990.
```bash ```bash
cp adn-parrot.example.yaml adn-parrot.yaml cp adn-echo.example.yaml adn-echo.yaml
python adn-server.py --parrot -c adn-parrot.yaml python adn-server.py --echo -c adn-echo.yaml
``` ```
See [Parrot (playback)](docs/en/server/user-guide/parrot.md) in the docs site for an overview. See [Echo (playback)](docs/en/server/user-guide/echo.md) in the docs site for an overview.
**systemd:** example units in `examples/systemd/` (`adn-server.service`, `adn-parrot.service`). **systemd:** example units in `examples/systemd/` (`adn-server.service`, `adn-echo.service`).

@ -1,7 +1,7 @@
# ADN DMR Parrot — minimal PEER config (copy to adn-parrot.yaml; do not commit). # ADN DMR Echo — minimal PEER config (copy to adn-echo.yaml; do not commit).
# #
# Separate process, same binary as adn-server: # Separate process, same binary as adn-server:
# python adn-server.py --parrot -c adn-parrot.yaml # python adn-server.py --echo -c adn-echo.yaml
# #
# Connects to ECHO (MASTER) in adn-server.yaml — match MASTER_PORT and PASSPHRASE. # Connects to ECHO (MASTER) in adn-server.yaml — match MASTER_PORT and PASSPHRASE.
@ -9,11 +9,11 @@ GLOBAL:
SERVER_ID: 9990 SERVER_ID: 9990
LOGGER: LOGGER:
LOG_FILE: /var/log/adn-server/parrot.log LOG_FILE: /var/log/adn-server/echo.log
LOG_LEVEL: INFO LOG_LEVEL: INFO
SYSTEMS: SYSTEMS:
PARROT: ECHO:
MODE: PEER MODE: PEER
IP: 127.0.0.1 IP: 127.0.0.1
PORT: 54915 PORT: 54915

@ -28,10 +28,10 @@ Run the ADN DMR Peer Server from the project root.
python adn-server.py python adn-server.py
python adn-server.py -c adn-server.yaml python adn-server.py -c adn-server.yaml
python adn-server.py --logging DEBUG python adn-server.py --logging DEBUG
python adn-server.py --parrot -c adn-parrot.yaml python adn-server.py --echo -c adn-echo.yaml
python adn-server.py --doctor python adn-server.py --doctor
Config default: adn-server.yaml (or adn-parrot.yaml with --parrot). Config default: adn-server.yaml (or adn-echo.yaml with --echo).
""" """
from __future__ import annotations from __future__ import annotations

@ -25,14 +25,14 @@ python3 -m pytest tests/ -q
```bash ```bash
python3 -m pytest tests/routing/ -q python3 -m pytest tests/routing/ -q
python3 -m pytest tests/hbp/ -q python3 -m pytest tests/hbp/ -q
python3 -m pytest tests/parrot/ -q python3 -m pytest tests/echo/ -q
``` ```
**By file** (recommended for point checks): **By file** (recommended for point checks):
```bash ```bash
python3 -m pytest tests/routing/test_unit_data_routing.py -q python3 -m pytest tests/routing/test_unit_data_routing.py -q
python3 -m pytest tests/parrot/test_rekey_playback.py -q python3 -m pytest tests/echo/test_rekey_playback.py -q
``` ```
**Single test**: **Single test**:

@ -46,12 +46,12 @@ Three **modes** exist:
| Mode | Typical use | Listens | Connects upstream | | 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) | | **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 | | **PEER** | Hotspot/repeater or service (e.g. echo) 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) | | **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**. **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). A single process can run **several** systems at once (e.g. one MASTER for users, one ECHO for playback, one OBP to a partner network).
--- ---
@ -136,7 +136,7 @@ A **PEER** connects **outbound** to a **MASTER** and listens locally for the rad
| **OPTIONS** | Byte string / options line (e.g. `TS2=9990;`) for static TG / behaviour. | | **OPTIONS** | Byte string / options line (e.g. `TS2=9990;`) for static TG / behaviour. |
| **LOOSE** | Relaxed handling flag where applicable. | | **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). The **echo** example (`adn-echo.example.yaml`) is a PEER that attaches to the **ECHO** MASTER: same **PASSPHRASE**, **MASTER_PORT** = ECHO’s **PORT**. See [Echo](echo.md).
--- ---
@ -264,5 +264,5 @@ Use the project interpreter (see workspace rules), e.g. `python3.11` from pyenv,
- [Introduction](introduction.md) — role of the server. - [Introduction](introduction.md) — role of the server.
- [Bridges and talkgroups](bridges-and-talkgroups.md) — `BRIDGES` semantics. - [Bridges and talkgroups](bridges-and-talkgroups.md) — `BRIDGES` semantics.
- [Special numbers](special-numbers.md) — reserved TGs and server IDs. - [Special numbers](special-numbers.md) — reserved TGs and server IDs.
- [Parrot](parrot.md) — PEER example (parrot process). - [Echo](echo.md) — PEER example (echo process).
- [Hotspot proxy](hotspot-proxy.md) — integrated **`PROXY`** / **`SELF_SERVICE`**. - [Hotspot proxy](hotspot-proxy.md) — integrated **`PROXY`** / **`SELF_SERVICE`**.

@ -0,0 +1,42 @@
# Echo (playback)
## What it is
**Echo** records incoming **group** voice and plays it back on TG **9990**. It runs as a **PEER** process that connects to the **ECHO** master on the main peer server (TG 9990 bridge).
The playback runtime is part of **`adn-server`**; run **`adn-server.py --echo`** with minimal **`adn-echo.yaml`**.
## Configuration
Use a **small separate YAML** — only what the PEER needs to attach to **ECHO** in `adn-server.yaml`:
| Field | Role |
|-------|------|
| `GLOBAL.SERVER_ID` | Echo network identity (often `9990`) |
| `LOGGER` | Log file (optional but recommended) |
| `SYSTEMS.ECHO` | `MODE: PEER`, local `IP`/`PORT`, `MASTER_IP`/`MASTER_PORT`, `PASSPHRASE`, `RADIO_ID`, `CALLSIGN`, `OPTIONS` |
No `PROXY`, `ALIASES`, or `REPORTS` required. **`MASTER_PORT`** and **`PASSPHRASE`** must match **`ECHO`** on the main server.
- Copy **`adn-echo.example.yaml`** → **`adn-echo.yaml`** (not committed).
- Run:
```bash
python adn-server.py --echo -c adn-echo.yaml
```
Typical production: separate **systemd** unit (see `examples/systemd/adn-echo.service` in the repo), same binary:
```bash
sudo cp examples/systemd/adn-echo.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now adn-echo
```
## Relation to TG 9990
The main server exposes an **ECHO** master on **TG 9990** for the echo bridge. The standalone **echo** service is a separate process with its own config that connects to that master.
## Documentation
This page is the summary shipped with the repository; extend your deployment notes locally as needed.

@ -26,7 +26,7 @@ Routing, timers, OpenBridge loop control, and protocol handling are implemented
## Related programs ## Related programs
- **Parrot / playback** — `adn-server.py --parrot` with minimal `adn-parrot.yaml`; see [Parrot](parrot.md). - **Echo / playback** — `adn-server.py --echo` with minimal `adn-echo.yaml`; see [Echo](echo.md).
- **Integrated hotspot proxy** — `PROXY` in **`adn-server.yaml`**; see [Hotspot proxy](hotspot-proxy.md). - **Integrated hotspot proxy** — `PROXY` in **`adn-server.yaml`**; see [Hotspot proxy](hotspot-proxy.md).
## Next steps ## Next steps

@ -50,7 +50,7 @@ These processes handle **`SIGUSR2`** by reopening **`logging.FileHandler`** stre
| Process | Typical config keys | | Process | Typical config keys |
|---------|---------------------| |---------|---------------------|
| **`adn-server`** / **`adn-parrot`** | **`LOGGER.LOG_FILE`** (integrated proxy logs appear in the same file) | | **`adn-server`** / **`adn-echo`** | **`LOGGER.LOG_FILE`** (integrated proxy logs appear in the same file) |
| **`adn-monitor`** | **`LOG.PATH`** + **`LOG.LOG_FILE`** in `adn-monitor.yaml` | | **`adn-monitor`** | **`LOG.PATH`** + **`LOG.LOG_FILE`** in `adn-monitor.yaml` |
Example **`/etc/logrotate.d/adn`** fragment (adjust paths and service names): Example **`/etc/logrotate.d/adn`** fragment (adjust paths and service names):
@ -70,7 +70,7 @@ Example **`/etc/logrotate.d/adn`** fragment (adjust paths and service names):
} }
``` ```
Repeat **`postrotate`** with **`kill -USR2`** for **`adn-parrot`** and **`adn-monitor`** units if those logs are rotated on the same host. Use the correct **PID** (systemd **`MainPID`**, a pidfile, or **`kill`** targeting the process you manage). Repeat **`postrotate`** with **`kill -USR2`** for **`adn-echo`** and **`adn-monitor`** units if those logs are rotated on the same host. Use the correct **PID** (systemd **`MainPID`**, a pidfile, or **`kill`** targeting the process you manage).
## Requirements ## Requirements

@ -1,42 +0,0 @@
# Parrot (playback)
## What it is
**Parrot** records incoming **group** voice and plays it back (echo / parrot). It runs as a **PEER** that connects to the **ECHO** master on the main peer server (TG 9990 bridge).
The playback runtime is part of **`adn-server`**; run **`adn-server.py --parrot`** with minimal **`adn-parrot.yaml`**.
## Configuration
Use a **small separate YAML** — only what the PEER needs to attach to **ECHO** in `adn-server.yaml`:
| Field | Role |
|-------|------|
| `GLOBAL.SERVER_ID` | Parrot network identity (often `9990`) |
| `LOGGER` | Log file (optional but recommended) |
| `SYSTEMS.PARROT` | `MODE: PEER`, local `IP`/`PORT`, `MASTER_IP`/`MASTER_PORT`, `PASSPHRASE`, `RADIO_ID`, `CALLSIGN`, `OPTIONS` |
No `PROXY`, `ALIASES`, or `REPORTS` required. **`MASTER_PORT`** and **`PASSPHRASE`** must match **`ECHO`** on the main server.
- Copy **`adn-parrot.example.yaml`** → **`adn-parrot.yaml`** (not committed).
- Run:
```bash
python adn-server.py --parrot -c adn-parrot.yaml
```
Typical production: separate **systemd** unit (see `examples/systemd/adn-parrot.service` in the repo), same binary:
```bash
sudo cp examples/systemd/adn-parrot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now adn-parrot
```
## 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.

@ -89,11 +89,11 @@ Operationally: if users report “bridges drop too easily” after OPTIONS updat
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). 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) ## TG 9990 — echo (in-band)
**Purpose:** Bridge rows for **echo** often use **9990** with the **ECHO** system (see `BRIDGES` and options in your YAML). **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). **Note:** A **standalone echo** is also available as a separate process — [Echo](echo.md).
## Private call to ID 4000 ## Private call to ID 4000

@ -52,5 +52,5 @@ Parallel **broadcasts** on **different** TGs may run concurrently; **same-TG** b
## See also ## See also
- [Configuration](configuration.md) — voice file paths. - [Configuration](configuration.md) — voice file paths.
- [Parrot](parrot.md) — separate playback service. - [Echo](echo.md) — separate playback service.
- [Special numbers](special-numbers.md) — 5000, 999x, recording-related behaviour. - [Special numbers](special-numbers.md) — 5000, 999x, recording-related behaviour.

@ -46,12 +46,12 @@ Existen tres **modos**:
| Modo | Uso típico | Escucha | Conecta aguas arriba | | 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) | | **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 | | **PEER** | Hotspot/repetidor o servicio (p. ej. echo) 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) | | **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**. **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). Un solo proceso puede ejecutar **varios** sistemas a la vez (p. ej. un MASTER para usuarios, un ECHO para playback, un OBP hacia una red asociada).
--- ---
@ -136,7 +136,7 @@ Un **PEER** conecta **saliente** hacia un **MASTER** y escucha localmente para l
| **OPTIONS** | Cadena / línea de opciones (p. ej. `TS2=9990;`) para TG estáticas / comportamiento. | | **OPTIONS** | Cadena / línea de opciones (p. ej. `TS2=9990;`) para TG estáticas / comportamiento. |
| **LOOSE** | Flag de manejo relajado donde aplique. | | **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). El ejemplo **echo** (`adn-echo.example.yaml`) es un PEER que se une al MASTER **ECHO**: mismo **PASSPHRASE**, **MASTER_PORT** = **PORT** del ECHO. Ver [Echo](echo.md).
--- ---
@ -264,5 +264,5 @@ Usa el intérprete del proyecto (ver reglas del workspace), p. ej. `python3.11`
- [Introducción](introduction.md) — rol del servidor. - [Introducción](introduction.md) — rol del servidor.
- [Bridges y talkgroups](bridges-and-talkgroups.md) — semántica de `BRIDGES`. - [Bridges y talkgroups](bridges-and-talkgroups.md) — semántica de `BRIDGES`.
- [Números especiales](special-numbers.md) — TG e IDs reservados. - [Números especiales](special-numbers.md) — TG e IDs reservados.
- [Parrot](parrot.md) — ejemplo PEER (proceso parrot). - [Echo](echo.md) — ejemplo PEER (proceso echo).
- [Proxy hotspot](hotspot-proxy.md) — **`PROXY`** / **`SELF_SERVICE`** integrados. - [Proxy hotspot](hotspot-proxy.md) — **`PROXY`** / **`SELF_SERVICE`** integrados.

@ -0,0 +1,42 @@
# Echo (reproducción)
## Qué es
**Echo** graba voz de **grupo** entrante y la reproduce en el TG **9990**. Corre como **PEER** conectado al master **ECHO** del peer server principal (bridge TG 9990).
El runtime de playback forma parte de **`adn-server`**; ejecútalo con **`adn-server.py --echo`** y un **`adn-echo.yaml`** mínimo.
## Configuración
Usa un **YAML aparte y mínimo** — solo lo que el PEER necesita para unirse a **ECHO** en `adn-server.yaml`:
| Campo | Rol |
|-------|------|
| `GLOBAL.SERVER_ID` | Identidad de red del echo (suele ser `9990`) |
| `LOGGER` | Archivo de log (opcional pero recomendado) |
| `SYSTEMS.ECHO` | `MODE: PEER`, `IP`/`PORT` local, `MASTER_IP`/`MASTER_PORT`, `PASSPHRASE`, `RADIO_ID`, `CALLSIGN`, `OPTIONS` |
No hace falta `PROXY`, `ALIASES` ni `REPORTS`. **`MASTER_PORT`** y **`PASSPHRASE`** deben coincidir con **`ECHO`** en el servidor principal.
- Copia **`adn-echo.example.yaml`** → **`adn-echo.yaml`** (no se commitea).
- Ejecuta:
```bash
python adn-server.py --echo -c adn-echo.yaml
```
En producción suele ir en una unidad **systemd** aparte (ver `examples/systemd/adn-echo.service` en el repo), mismo binario:
```bash
sudo cp examples/systemd/adn-echo.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now adn-echo
```
## Relación con TG 9990
El servidor principal expone un master **ECHO** en **TG 9990** para el bridge de eco. El servicio **echo** independiente es un proceso aparte con su propia config que se conecta a ese master.
## Documentación
Esta página es el resumen incluido en el repositorio; amplía las notas de despliegue localmente según necesites.

@ -84,7 +84,7 @@ Detalle del flujo en el panel: [Self-service](../../monitor/self-service.md).
## Comportamiento con varios hotspots ## Comportamiento con varios hotspots
- Cada hotspot autenticado es un **peer** en el MASTER de inyección con sus **OPTIONS** (TG estáticas). **Repeat** y el fan-out del monitor respetan **OPTIONS por peer** — el tráfico de un TG no se envía a peers que no lo tienen seleccionado. - Cada hotspot autenticado es un **peer** en el MASTER de inyección con sus **OPTIONS** (TG estáticas). **Repeat** y el fan-out del monitor respetan **OPTIONS por peer** — el tráfico de un TG no se envía a peers que no lo tienen seleccionado.
- Los talkgroups **parrot / eco 9990–9999** omiten el filtro OPTIONS y vuelven al hotspot **llamante** (ver [Números especiales](special-numbers.md)). - Los talkgroups **eco 9990–9999** omiten el filtro OPTIONS y vuelven al hotspot **llamante** (ver [Números especiales](special-numbers.md)).
--- ---

@ -26,7 +26,7 @@ Enrutado, temporizadores, control de bucle OpenBridge y manejo de protocolo est
## Programas relacionados ## Programas relacionados
- **Parrot / reproducción** — `adn-server.py --parrot` con `adn-parrot.yaml` mínimo; ver [Parrot](parrot.md). - **Echo / playback** — `adn-server.py --echo` con `adn-echo.yaml` mínimo; ver [Echo](echo.md).
- **Proxy hotspot integrado** — `PROXY` en **`adn-server.yaml`**; ver [Proxy hotspot](hotspot-proxy.md). - **Proxy hotspot integrado** — `PROXY` en **`adn-server.yaml`**; ver [Proxy hotspot](hotspot-proxy.md).
## Siguientes pasos ## Siguientes pasos

@ -50,7 +50,7 @@ Estos procesos tratan **`SIGUSR2`** solo para **reabrir** los ficheros de log (`
| Proceso | Claves típicas de configuración | | Proceso | Claves típicas de configuración |
|---------|-----------------------------------| |---------|-----------------------------------|
| **`adn-server`** / **`adn-parrot`** | **`LOGGER.LOG_FILE`** (los logs del proxy integrado van al mismo fichero) | | **`adn-server`** / **`adn-echo`** | **`LOGGER.LOG_FILE`** (los logs del proxy integrado van al mismo fichero) |
| **`adn-monitor`** | **`LOG.PATH`** + **`LOG.LOG_FILE`** en `adn-monitor.yaml` | | **`adn-monitor`** | **`LOG.PATH`** + **`LOG.LOG_FILE`** en `adn-monitor.yaml` |
Ejemplo de fragmento en **`/etc/logrotate.d/adn`** (adaptar rutas y nombres de unidad): Ejemplo de fragmento en **`/etc/logrotate.d/adn`** (adaptar rutas y nombres de unidad):
@ -70,7 +70,7 @@ Ejemplo de fragmento en **`/etc/logrotate.d/adn`** (adaptar rutas y nombres de u
} }
``` ```
Repite **`postrotate`** con **`kill -USR2`** para **`adn-parrot`** y **`adn-monitor`** si rotas sus logs en el mismo host. Usa el **PID** correcto (**`MainPID`** de systemd, pidfile, o el proceso que gestiones). Repite **`postrotate`** con **`kill -USR2`** para **`adn-echo`** y **`adn-monitor`** si rotas sus logs en el mismo host. Usa el **PID** correcto (**`MainPID`** de systemd, pidfile, o el proceso que gestiones).
## Requisitos ## Requisitos

@ -1,42 +0,0 @@
# Parrot (reproducción)
## Qué es
**Parrot** graba voz de **grupo** entrante y la reproduce (eco / parrot). Corre como **PEER** conectado al master **ECHO** del peer server principal (bridge TG 9990).
El runtime de playback forma parte de **`adn-server`**; ejecuta **`adn-server.py --parrot`** con un **`adn-parrot.yaml`** mínimo.
## Configuración
Usa un **YAML aparte y mínimo** — solo lo que el PEER necesita para unirse a **ECHO** en `adn-server.yaml`:
| Campo | Rol |
|-------|-----|
| `GLOBAL.SERVER_ID` | Identidad de red del parrot (suele ser `9990`) |
| `LOGGER` | Fichero de log (opcional pero recomendado) |
| `SYSTEMS.PARROT` | `MODE: PEER`, `IP`/`PORT` local, `MASTER_IP`/`MASTER_PORT`, `PASSPHRASE`, `RADIO_ID`, `CALLSIGN`, `OPTIONS` |
No hace falta `PROXY`, `ALIASES` ni `REPORTS`. **`MASTER_PORT`** y **`PASSPHRASE`** deben coincidir con **`ECHO`** en el servidor principal.
- Copiar **`adn-parrot.example.yaml`** → **`adn-parrot.yaml`** (no versionada).
- Ejecutar:
```bash
python adn-server.py --parrot -c adn-parrot.yaml
```
En producción: unidad **systemd** aparte (plantilla `examples/systemd/adn-parrot.service`):
```bash
sudo cp examples/systemd/adn-parrot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now adn-parrot
```
## 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.

@ -89,11 +89,11 @@ A nivel operativo: si usuarios reportan «bridges que se caen demasiado fácil»
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). 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) ## TG 9990 — eco (en banda)
**Propósito:** las filas de bridge para **eco** suelen usar **9990** con el sistema **ECHO** (ver `BRIDGES` y opciones en tu YAML). **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). **Nota:** Un **echo independiente** también está disponible como proceso aparte — [Echo](echo.md).
## Llamada privada al ID 4000 ## Llamada privada al ID 4000

@ -53,5 +53,5 @@ Las **emisiones** en paralelo en **TG distintas** pueden ejecutarse a la vez; la
## Ver también ## Ver también
- [Configuración](configuration.md) — rutas de ficheros de voz. - [Configuración](configuration.md) — rutas de ficheros de voz.
- [Parrot](parrot.md) — servicio de reproducción separado. - [Echo](echo.md) — servicio de reproducción separado.
- [Números especiales](special-numbers.md) — 5000, 999x, comportamiento relacionado con grabación. - [Números especiales](special-numbers.md) — 5000, 999x, comportamiento relacionado con grabación.

@ -0,0 +1,32 @@
# ADN DMR Echo (playback) – systemd unit (PEER → ECHO master on adn-server)
#
# Copy:
# sudo cp /opt/adn-server/examples/systemd/adn-echo.service /etc/systemd/system/
# Edit User / paths if needed, then:
# sudo systemctl daemon-reload
# sudo systemctl enable --now adn-echo
#
# Minimal adn-echo.yaml — MASTER_PORT / PASSPHRASE must match ECHO in adn-server.yaml.
# Logrotate: SIGUSR2 reopens log files (not SIGHUP). postrotate example:
# kill -USR2 $(systemctl show adn-echo -p MainPID --value)
# Or: systemctl reload adn-echo (ExecReload below sends USR2)
[Unit]
Description=ADN DMR Echo (playback)
After=network-online.target adn-server.service
Wants=network-online.target
[Service]
User=adn
Type=simple
Restart=always
RestartSec=3
SyslogIdentifier=adn-echo
WorkingDirectory=/opt/adn-server
ExecStart=/opt/.pyenv/versions/3.11.8/bin/python3 /opt/adn-server/adn-server.py --echo -c /opt/adn-server/adn-echo.yaml
ExecReload=/bin/kill -USR2 $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target

@ -1,32 +0,0 @@
# ADN DMR Parrot (playback) – systemd unit (PEER → ECHO on adn-server)
#
# Copy:
# sudo cp /opt/adn-server/examples/systemd/adn-parrot.service /etc/systemd/system/
# Edit User / paths if needed, then:
# sudo systemctl daemon-reload
# sudo systemctl enable --now adn-parrot
#
# Minimal adn-parrot.yaml — MASTER_PORT / PASSPHRASE must match ECHO in adn-server.yaml.
# Logrotate: SIGUSR2 reopens log files (not SIGHUP). postrotate example:
# kill -USR2 $(systemctl show adn-parrot -p MainPID --value)
# Or: systemctl reload adn-parrot (ExecReload below sends USR2)
[Unit]
Description=ADN DMR Parrot (playback)
After=network-online.target adn-server.service
Wants=network-online.target
[Service]
User=adn
Type=simple
Restart=always
RestartSec=3
SyslogIdentifier=adn-parrot
WorkingDirectory=/opt/adn-server
ExecStart=/opt/.pyenv/versions/3.11.8/bin/python3 /opt/adn-server/adn-server.py --parrot -c /opt/adn-server/adn-parrot.yaml
ExecReload=/bin/kill -USR2 $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target

@ -57,7 +57,7 @@ nav:
- Voz, anuncios y TTS: server/user-guide/voice-and-tts.md - Voz, anuncios y TTS: server/user-guide/voice-and-tts.md
- Monitor e informes: server/user-guide/monitoring.md - Monitor e informes: server/user-guide/monitoring.md
- Proxy hotspot (integrado): server/user-guide/hotspot-proxy.md - Proxy hotspot (integrado): server/user-guide/hotspot-proxy.md
- Parrot (reproducción): server/user-guide/parrot.md - Echo (reproducción): server/user-guide/echo.md
- Créditos y licencia: server/user-guide/attribution.md - Créditos y licencia: server/user-guide/attribution.md
- Protocolos: - Protocolos:
- HBP (DMRD): server/protocols/hbp.md - HBP (DMRD): server/protocols/hbp.md

@ -57,7 +57,7 @@ nav:
- Voice, announcements, and TTS: server/user-guide/voice-and-tts.md - Voice, announcements, and TTS: server/user-guide/voice-and-tts.md
- Monitoring and reports: server/user-guide/monitoring.md - Monitoring and reports: server/user-guide/monitoring.md
- Hotspot proxy (integrated): server/user-guide/hotspot-proxy.md - Hotspot proxy (integrated): server/user-guide/hotspot-proxy.md
- Parrot (playback): server/user-guide/parrot.md - Echo (playback): server/user-guide/echo.md
- Credits & license: server/user-guide/attribution.md - Credits & license: server/user-guide/attribution.md
- Protocols: - Protocols:
- HBP (DMRD): server/protocols/hbp.md - HBP (DMRD): server/protocols/hbp.md

@ -1,4 +1,4 @@
# ADN DMR Peer Server - playback (parrot) use case # ADN DMR Peer Server - playback (echo) use case
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
# Derived from ADN DMR Server / FreeDMR / HBlink. Original license: # Derived from ADN DMR Server / FreeDMR / HBlink. Original license:

@ -32,7 +32,7 @@ def proxy_target_system(config: dict[str, Any]) -> str | None:
def config_has_enabled_master(config: dict[str, Any]) -> bool: def config_has_enabled_master(config: dict[str, Any]) -> bool:
"""True when config defines at least one enabled MASTER (adn-server, not parrot-only).""" """True when config defines at least one enabled MASTER (adn-server, not echo-only)."""
systems = config.get("SYSTEMS", {}) systems = config.get("SYSTEMS", {})
if not isinstance(systems, dict): if not isinstance(systems, dict):
return False return False

@ -543,7 +543,7 @@ class SubscriptionTableMixin:
self._sync_subscription_store() self._sync_subscription_store()
return return
if merged is None: if merged is None:
# Echo / parrot TGs (9990–9999) are excluded from merged lists but still need restore. # Echo TGs (9990–9999) are excluded from merged lists but still need restore.
self._restore_prohibited_static_bridge_legs(system_name) self._restore_prohibited_static_bridge_legs(system_name)
self._sync_subscription_store() self._sync_subscription_store()
return return

@ -212,7 +212,7 @@ class RoutingUseCases(
system_name, peer_id, rf_src, dst_id, seq, slot, stream_id, data, pkt_time, system_name, peer_id, rf_src, dst_id, seq, slot, stream_id, data, pkt_time,
): ):
return return
# Arm ON in-band rules on VHEAD (parrot 9990 and UA bridges); VTERM handled in udp_hbp too. # Arm ON in-band rules on VHEAD (echo 9990 and UA bridges); VTERM handled in udp_hbp too.
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD: if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
self.apply_in_band_signalling(system_name, slot, dst_id, pkt_time) self.apply_in_band_signalling(system_name, slot, dst_id, pkt_time)
from .subscription.subscription_queries import store_has_table from .subscription.subscription_queries import store_has_table
@ -264,7 +264,7 @@ class RoutingUseCases(
obp_hops if obp_use_parsed else b"", obp_hops if obp_use_parsed else b"",
): ):
return return
# SINGLE_MODE in-band VTERM on another TG (e.g. 9990 parrot) deactivates static OFF # SINGLE_MODE in-band VTERM on another TG (e.g. 9990 echo) deactivates static OFF
# legs; re-arm OPTIONS static destinations before forward (inject-only merged lists). # legs; re-arm OPTIONS static destinations before forward (inject-only merged lists).
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD: if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
self.apply_static_tg_to_bridge(dst_int) self.apply_static_tg_to_bridge(dst_int)
@ -279,7 +279,7 @@ class RoutingUseCases(
# Do not call ensure_dynamic_relay here for 9990–9999 when BRIDGES["9990"] already exists: # Do not call ensure_dynamic_relay here for 9990–9999 when BRIDGES["9990"] already exists:
# ensure_dynamic_relay replaces the whole table and only sets the source MASTER ACTIVE; every # ensure_dynamic_relay replaces the whole table and only sets the source MASTER ACTIVE; every
# other system (including ECHO with TO_TYPE NONE) becomes ACTIVE False — to_target then has # other system (including ECHO with TO_TYPE NONE) becomes ACTIVE False — to_target then has
# no active target for the parrot path (legacy _seed_echo_routing_table / make_bridges keeps ECHO # no active target for the echo path (legacy _seed_echo_routing_table / make_bridges keeps ECHO
# ACTIVE True; activation of the source row is via in-band ON on VTERM, bridge_master ~3465). # ACTIVE True; activation of the source row is via in-band ON on VTERM, bridge_master ~3465).
if not has_source: if not has_source:
logger.debug( logger.debug(

@ -130,7 +130,7 @@ def apply_in_band_signalling_store(
or dst_id_b != tgid_b or dst_id_b != tgid_b
or dst_id_b == bytes_3(4000) or dst_id_b == bytes_3(4000)
): ):
# OPTIONS static (OFF) legs stay armed when parrot/special TG ends (9990–9999). # OPTIONS static (OFF) legs stay armed when echo special TG ends (9990–9999).
if ( if (
sub.policy == ActivationPolicy.STATIC sub.policy == ActivationPolicy.STATIC
and dst_id_b != tgid_b and dst_id_b != tgid_b

@ -22,7 +22,7 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
############################################################################### ###############################################################################
"""Shared config normalization: PEER, OBP, MASTER runtime state. Used by main and parrot runtime.""" """Shared config normalization: PEER, OBP, MASTER runtime state. Used by main and echo runtime."""
from __future__ import annotations from __future__ import annotations

@ -98,22 +98,22 @@ def collect_findings(
*, *,
project_root: str, project_root: str,
config_path: str, config_path: str,
parrot: bool = False, echo: bool = False,
no_proxy: bool = False, no_proxy: bool = False,
) -> list[Finding]: ) -> list[Finding]:
findings: list[Finding] = [] findings: list[Finding] = []
logger = _NullLogger() logger = _NullLogger()
if parrot: if echo:
peers = [ peers = [
name name
for name, cfg in config.get("SYSTEMS", {}).items() for name, cfg in config.get("SYSTEMS", {}).items()
if isinstance(cfg, dict) and cfg.get("MODE") == "PEER" and cfg.get("ENABLED", True) if isinstance(cfg, dict) and cfg.get("MODE") == "PEER" and cfg.get("ENABLED", True)
] ]
if peers: if peers:
findings.append(Finding("ok", "parrot", f"{len(peers)} PEER system(s): {', '.join(peers)}")) findings.append(Finding("ok", "echo", f"{len(peers)} PEER system(s): {', '.join(peers)}"))
else: else:
findings.append(Finding("error", "parrot", "no enabled PEER systems in config")) findings.append(Finding("error", "echo", "no enabled PEER systems in config"))
else: else:
apply_talker_alias_defaults(config) apply_talker_alias_defaults(config)
expand_generator(config, logger) expand_generator(config, logger)
@ -196,7 +196,7 @@ def collect_findings(
else: else:
findings.append(Finding("warn", "systems", f"{name}: unknown MODE={mode}")) findings.append(Finding("warn", "systems", f"{name}: unknown MODE={mode}"))
if not parrot: if not echo:
reports = config.get("REPORTS", {}) reports = config.get("REPORTS", {})
if reports.get("REPORT", True): if reports.get("REPORT", True):
port = int(reports.get("REPORT_PORT", 4321)) port = int(reports.get("REPORT_PORT", 4321))
@ -253,7 +253,7 @@ def run_doctor(
config_path: str, config_path: str,
project_root: str, project_root: str,
*, *,
parrot: bool = False, echo: bool = False,
no_proxy: bool = False, no_proxy: bool = False,
version: str = "", version: str = "",
out: TextIO | None = None, out: TextIO | None = None,
@ -271,7 +271,7 @@ def run_doctor(
config, config,
project_root=project_root, project_root=project_root,
config_path=config_path, config_path=config_path,
parrot=parrot, echo=echo,
no_proxy=no_proxy, no_proxy=no_proxy,
) )
print(format_report(findings, version=version, config_path=config_path), file=stream) print(format_report(findings, version=version, config_path=config_path), file=stream)

@ -1,4 +1,4 @@
# ADN DMR Peer Server - infrastructure parrot init # ADN DMR Peer Server - infrastructure echo
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -18,8 +18,8 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
############################################################################### ###############################################################################
"""Integrated parrot (playback) runtime.""" """Integrated ECHO (playback) runtime."""
from .runtime import run_parrot from .runtime import run_echo
__all__ = ["run_parrot"] __all__ = ["run_echo"]

@ -1,4 +1,4 @@
# ADN DMR Peer Server - infrastructure parrot runtime # ADN DMR Peer Server - infrastructure echo runtime
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -18,7 +18,7 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
############################################################################### ###############################################################################
"""Wire parrot (playback) PEER systems at startup.""" """Wire ECHO playback PEER systems at startup."""
from __future__ import annotations from __future__ import annotations
@ -45,7 +45,7 @@ def _looping_errback(logger_obj: logging.Logger, failure: Any) -> None:
logger_obj.error("(GLOBAL) Unhandled error in timed loop: %s", failure.getTraceback()) logger_obj.error("(GLOBAL) Unhandled error in timed loop: %s", failure.getTraceback())
def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None: def run_echo(config: dict[str, Any], *, logger: logging.Logger) -> None:
"""Start playback PEER(s) and run the Twisted reactor (blocks until shutdown).""" """Start playback PEER(s) and run the Twisted reactor (blocks until shutdown)."""
ensure_system_runtime_config(config) ensure_system_runtime_config(config)
normalize_peer_config(config) normalize_peer_config(config)
@ -74,7 +74,7 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None:
task.LoopingCall(reporting_loop).start(report_interval).addErrback(_looping_errback, logger) task.LoopingCall(reporting_loop).start(report_interval).addErrback(_looping_errback, logger)
def sig_handler(sig: int, _frame: Any) -> None: def sig_handler(sig: int, _frame: Any) -> None:
logger.info("SHUTDOWN: PARROT IS TERMINATING WITH SIGNAL %s", sig) logger.info("SHUTDOWN: ECHO IS TERMINATING WITH SIGNAL %s", sig)
if reactor.running: if reactor.running:
reactor.stop() reactor.stop()
@ -83,7 +83,7 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None:
logger.info("(LOGGER) Reopened %s file log handler(s) after SIGUSR2", n) logger.info("(LOGGER) Reopened %s file log handler(s) after SIGUSR2", n)
def sighup_ignore(_sig: int, _frame: Any) -> None: def sighup_ignore(_sig: int, _frame: Any) -> None:
logger.debug("(PARROT) SIGHUP ignored (no config hot-reload; use SIGUSR2 for logrotate)") logger.debug("(ECHO) SIGHUP ignored (no config hot-reload; use SIGUSR2 for logrotate)")
signal.signal(signal.SIGTERM, sig_handler) signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler) signal.signal(signal.SIGINT, sig_handler)
@ -91,13 +91,13 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None:
if hasattr(signal, "SIGHUP"): if hasattr(signal, "SIGHUP"):
signal.signal(signal.SIGHUP, sighup_ignore) signal.signal(signal.SIGHUP, sighup_ignore)
logger.info("ADN Parrot -- SYSTEM STARTING...") logger.info("ADN Echo -- SYSTEM STARTING...")
started = 0 started = 0
for system_name, sys_cfg in systems_cfg.items(): for system_name, sys_cfg in systems_cfg.items():
if not sys_cfg.get("ENABLED", True): if not sys_cfg.get("ENABLED", True):
continue continue
if sys_cfg.get("MODE") != "PEER": if sys_cfg.get("MODE") != "PEER":
logger.debug("(PARROT) skip %s (MODE=%s; parrot only runs PEER systems)", system_name, sys_cfg.get("MODE")) logger.debug("(ECHO) skip %s (MODE=%s; echo only runs PEER systems)", system_name, sys_cfg.get("MODE"))
continue continue
pb = PlaybackUseCases(system_name, get_protocol=lambda sn=system_name: protocols.get(sn)) pb = PlaybackUseCases(system_name, get_protocol=lambda sn=system_name: protocols.get(sn))
@ -115,7 +115,7 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None:
reactor.listenUDP(udp_port, protocol, interface=ip or "0.0.0.0") reactor.listenUDP(udp_port, protocol, interface=ip or "0.0.0.0")
started += 1 started += 1
logger.info( logger.info(
"(PARROT) %s PEER on %s:%s → master %s:%s", "(ECHO) %s PEER on %s:%s → master %s:%s",
system_name, system_name,
ip or "0.0.0.0", ip or "0.0.0.0",
udp_port, udp_port,
@ -124,7 +124,7 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None:
) )
if started == 0: if started == 0:
logger.critical("(PARROT) no enabled PEER systems in config") logger.critical("(ECHO) no enabled PEER systems in config")
sys.exit("parrot config must define at least one enabled SYSTEMS.<name> with MODE: PEER") sys.exit("echo config must define at least one enabled SYSTEMS.<name> with MODE: PEER")
reactor.run() reactor.run()

@ -568,7 +568,7 @@ class HBPProtocol(DatagramProtocol):
self._ta_decoded_logged.discard(stream_id) self._ta_decoded_logged.discard(stream_id)
def copy_ta_stream_buffer(self, from_stream: bytes, to_stream: bytes) -> None: def copy_ta_stream_buffer(self, from_stream: bytes, to_stream: bytes) -> None:
"""Carry decoded TA blocks from recording stream to parrot playback stream.""" """Carry decoded TA blocks from recording stream to echo playback stream."""
entry = self._dmra_by_stream.get(from_stream) entry = self._dmra_by_stream.get(from_stream)
if not entry or not entry.get("blocks"): if not entry or not entry.get("blocks"):
return return

@ -26,9 +26,9 @@
ADN DMR Peer Server entrypoint. ADN DMR Peer Server entrypoint.
Run: python -m adn_server.main [-c adn-server.yaml] [--logging LEVEL] Run: python -m adn_server.main [-c adn-server.yaml] [--logging LEVEL]
python -m adn_server.main --parrot [-c adn-parrot.yaml] python -m adn_server.main --echo [-c adn-echo.yaml]
python -m adn_server.main --doctor [-c adn-server.yaml] python -m adn_server.main --doctor [-c adn-server.yaml]
Config default: adn-server.yaml (or adn-parrot.yaml with --parrot). Config default: adn-server.yaml (or adn-echo.yaml with --echo).
""" """
from __future__ import annotations from __future__ import annotations
@ -48,7 +48,7 @@ from adn_server.infrastructure import YamlConfigLoader, setup_logging
from adn_server.infrastructure.bootstrap import run_peer_server from adn_server.infrastructure.bootstrap import run_peer_server
from adn_server.infrastructure.config_normalizer import apply_talker_alias_defaults from adn_server.infrastructure.config_normalizer import apply_talker_alias_defaults
from adn_server.infrastructure.doctor import run_doctor from adn_server.infrastructure.doctor import run_doctor
from adn_server.infrastructure.parrot import run_parrot from adn_server.infrastructure.echo import run_echo
def _project_root() -> str: def _project_root() -> str:
@ -65,9 +65,9 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument("-c", "--config", dest="CONFIG_FILE", default=None, help="Path to YAML config") parser.add_argument("-c", "--config", dest="CONFIG_FILE", default=None, help="Path to YAML config")
parser.add_argument("--logging", dest="LOG_LEVEL", default=None, help="Override log level") parser.add_argument("--logging", dest="LOG_LEVEL", default=None, help="Override log level")
parser.add_argument( parser.add_argument(
"--parrot", "--echo",
action="store_true", action="store_true",
help="Playback mode: run parrot PEER(s) from adn-parrot.yaml (default config)", help="Playback mode: run ECHO PEER(s) from adn-echo.yaml (default config)",
) )
parser.add_argument( parser.add_argument(
"--no-proxy", "--no-proxy",
@ -98,7 +98,7 @@ def main() -> None:
args = _parse_args() args = _parse_args()
project_root = _project_root() project_root = _project_root()
default_config = "adn-parrot.yaml" if args.parrot else "adn-server.yaml" default_config = "adn-echo.yaml" if args.echo else "adn-server.yaml"
config_path = args.CONFIG_FILE or os.path.join(project_root, default_config) config_path = args.CONFIG_FILE or os.path.join(project_root, default_config)
if args.doctor: if args.doctor:
@ -106,7 +106,7 @@ def main() -> None:
run_doctor( run_doctor(
config_path, config_path,
project_root, project_root,
parrot=args.parrot, echo=args.echo,
no_proxy=args.no_proxy, no_proxy=args.no_proxy,
version=__version__, version=__version__,
) )
@ -119,11 +119,11 @@ def main() -> None:
print(f"(CONFIG) {exc}", file=sys.stderr) print(f"(CONFIG) {exc}", file=sys.stderr)
sys.exit(1) sys.exit(1)
if not args.parrot: if not args.echo:
apply_talker_alias_defaults(config) apply_talker_alias_defaults(config)
voice_config_path = os.path.join(os.path.dirname(os.path.abspath(config_path)), "adn-voice.yaml") voice_config_path = os.path.join(os.path.dirname(os.path.abspath(config_path)), "adn-voice.yaml")
voice_data = loader.load_voice_config(voice_config_path) if not args.parrot else None voice_data = loader.load_voice_config(voice_config_path) if not args.echo else None
if voice_data: if voice_data:
config.setdefault("VOICE", {}).update(voice_data) config.setdefault("VOICE", {}).update(voice_data)
@ -132,8 +132,8 @@ def main() -> None:
logger = setup_logging(config.get("LOGGER", {})) logger = setup_logging(config.get("LOGGER", {}))
_log_copyright(logger) _log_copyright(logger)
if args.parrot: if args.echo:
run_parrot(config, logger=logger) run_echo(config, logger=logger)
return return
run_peer_server( run_peer_server(

@ -23,7 +23,7 @@ Use the project interpreter, e.g. `/opt/.pyenv/versions/3.11.8/bin/python3`.
| `obp/` | OpenBridge loop, rate limit, unit-data loop | | `obp/` | OpenBridge loop, rate limit, unit-data loop |
| `voice/` | Announcements, TTS schedule, broadcast queue, disconnected voice, in-band signalling | | `voice/` | Announcements, TTS schedule, broadcast queue, disconnected voice, in-band signalling |
| `talker_alias/` | Encode/decode, passthrough, MMDVM wire, routing inject (DeterministicScenario) | | `talker_alias/` | Encode/decode, passthrough, MMDVM wire, routing inject (DeterministicScenario) |
| `parrot/` | Recording timers, playback loop, seq preservation, ingress path | | `echo/` | Recording timers, playback loop, seq preservation, ingress path |
| `replay/` | JSONL session replay | | `replay/` | JSONL session replay |
| `schemas/` | Report v2 JSON Schema validation (`jsonschema` dev dep) | | `schemas/` | Report v2 JSON Schema validation (`jsonschema` dev dep) |
| `application/` | Report payloads, monitor topology, proxy use cases, subscription store/router | | `application/` | Report payloads, monitor topology, proxy use cases, subscription store/router |
@ -106,7 +106,7 @@ Mark new stack tests with `@pytest.mark.integration`.
| `test_mmdvm_wire.py` | 10 | MMDVM wire blocks | | `test_mmdvm_wire.py` | 10 | MMDVM wire blocks |
| `test_passthrough.py` | 9 | Passthrough / both modes | | `test_passthrough.py` | 9 | Passthrough / both modes |
### parrot/ ### echo/
| File | Tests | Topic | | File | Tests | Topic |
|------|-------|-------| |------|-------|-------|
@ -156,8 +156,8 @@ Session replay JSONL meta accepts `routing_table` (preferred) or legacy `bridges
# After changing unit-data routing # After changing unit-data routing
python3 -m pytest tests/routing/test_unit_data_routing.py -q python3 -m pytest tests/routing/test_unit_data_routing.py -q
# After parrot seq fix # After echo seq fix
python3 -m pytest tests/parrot/test_rekey_playback.py -q python3 -m pytest tests/echo/test_rekey_playback.py -q
# Talker Alias REPEAT (real stack) # Talker Alias REPEAT (real stack)
python3 -m pytest tests/infrastructure/test_hbp_repeat_talker_alias.py -q python3 -m pytest tests/infrastructure/test_hbp_repeat_talker_alias.py -q

@ -72,7 +72,7 @@ def test_store_non_single_deactivates_on_tg4000() -> None:
def test_store_single_mode_keeps_static_off_on_unrelated_vterm() -> None: def test_store_single_mode_keeps_static_off_on_unrelated_vterm() -> None:
"""OPTIONS static OFF legs must survive parrot/other-TG VTERM for OBP downlink.""" """OPTIONS static OFF legs must survive echo other-TG VTERM for OBP downlink."""
store = InMemorySubscriptionStore() store = InMemorySubscriptionStore()
systems_cfg = {"MASTER-A": {"MODE": "MASTER", "SINGLE_MODE": True, "DEFAULT_UA_TIMER": 10}} systems_cfg = {"MASTER-A": {"MODE": "MASTER", "SINGLE_MODE": True, "DEFAULT_UA_TIMER": 10}}
make_static_tg_store(store, 52090, 2, 10.0, "MASTER-A", systems_cfg, 1000.0, single_mode=True) make_static_tg_store(store, 52090, 2, 10.0, "MASTER-A", systems_cfg, 1000.0, single_mode=True)

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests parrot playback ingress # ADN DMR Peer Server - tests echo playback ingress
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -32,14 +32,14 @@ from adn_server.application.playback_use_cases import _PLAYBACK_DELAY_S, _RECORD
def test_dmrd_received_accepts_ingress_pkt_time_kwarg() -> None: def test_dmrd_received_accepts_ingress_pkt_time_kwarg() -> None:
"""Regression: PEER udp_hbp passes ingress_pkt_time (parrot must not TypeError).""" """Regression: PEER udp_hbp passes ingress_pkt_time (echo must not TypeError)."""
proto = FakePlaybackProtocol() proto = FakePlaybackProtocol()
pb = PlaybackUseCases("PARROT", get_protocol=lambda: proto) pb = PlaybackUseCases("ECHO", get_protocol=lambda: proto)
base = PacketSpec(dst_id=9990, stream_id=0x12121212, slot=2) base = PacketSpec(dst_id=9990, stream_id=0x12121212, slot=2)
args = DeterministicScenario.voice_head_spec(base).decoded_hbp_args() args = DeterministicScenario.voice_head_spec(base).decoded_hbp_args()
pb.dmrd_received( pb.dmrd_received(
"PARROT", "ECHO",
args["peer_id"], args["peer_id"],
args["rf_src"], args["rf_src"],
args["dst_id"], args["dst_id"],
@ -60,17 +60,17 @@ def test_dmrd_received_accepts_ingress_pkt_time_kwarg() -> None:
def test_ingress_pkt_time_enables_record_to_playback(caplog) -> None: def test_ingress_pkt_time_enables_record_to_playback(caplog) -> None:
"""Regression: PEER path (ingress_pkt_time) records voice and schedules playback.""" """Regression: PEER path (ingress_pkt_time) records voice and schedules playback."""
proto = FakePlaybackProtocol() proto = FakePlaybackProtocol()
pb = PlaybackUseCases("PARROT", get_protocol=lambda: proto) pb = PlaybackUseCases("ECHO", get_protocol=lambda: proto)
base = PacketSpec(dst_id=9990, stream_id=0x34343434, slot=2) base = PacketSpec(dst_id=9990, stream_id=0x34343434, slot=2)
mock_reactor, scheduled = install_reactor_capture() mock_reactor, scheduled = install_reactor_capture()
t0 = 1_700_000_000.0 t0 = 1_700_000_000.0
with patch("adn_server.application.playback_use_cases.reactor", mock_reactor): with patch("adn_server.application.playback_use_cases.reactor", mock_reactor):
with patch("adn_server.application.playback_use_cases.time", return_value=t0): with patch("adn_server.application.playback_use_cases.time", return_value=t0):
send_playback(pb, "PARROT", DeterministicScenario.voice_head_spec(base)) send_playback(pb, "ECHO", DeterministicScenario.voice_head_spec(base))
send_playback( send_playback(
pb, pb,
"PARROT", "ECHO",
DeterministicScenario.voice_burst_spec(base, seq=1, dtype_vseq=1), DeterministicScenario.voice_burst_spec(base, seq=1, dtype_vseq=1),
ingress_pkt_time=t0 + 2.0, ingress_pkt_time=t0 + 2.0,
) )

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests parrot playback logging # ADN DMR Peer Server - tests echo playback logging
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -35,7 +35,7 @@ from adn_server.domain import bytes_3, bytes_4
def test_start_playback_logs_duration_with_two_decimals(caplog) -> None: def test_start_playback_logs_duration_with_two_decimals(caplog) -> None:
"""PLAYBACK duration matches bridge-style %.2f (no float noise in logs).""" """PLAYBACK duration matches bridge-style %.2f (no float noise in logs)."""
proto = FakePlaybackProtocol() proto = FakePlaybackProtocol()
pb = PlaybackUseCases("PARROT", get_protocol=lambda: proto) pb = PlaybackUseCases("ECHO", get_protocol=lambda: proto)
base = PacketSpec(dst_id=9990, stream_id=0x88888888, slot=2) base = PacketSpec(dst_id=9990, stream_id=0x88888888, slot=2)
recorded = [DeterministicScenario.voice_head_spec(base).data()] recorded = [DeterministicScenario.voice_head_spec(base).data()]

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests parrot playback send loop # ADN DMR Peer Server - tests echo playback send loop
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests parrot recording timers # ADN DMR Peer Server - tests echo recording timers
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests parrot rekey playback # ADN DMR Peer Server - tests echo rekey playback
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #

@ -28,8 +28,8 @@ import textwrap
from adn_server.infrastructure.doctor import collect_findings, run_doctor from adn_server.infrastructure.doctor import collect_findings, run_doctor
def test_doctor_parrot_requires_peer_system(tmp_path) -> None: def test_doctor_echo_requires_peer_system(tmp_path) -> None:
cfg = tmp_path / "adn-parrot.yaml" cfg = tmp_path / "adn-echo.yaml"
cfg.write_text( cfg.write_text(
textwrap.dedent( textwrap.dedent(
""" """
@ -45,7 +45,7 @@ def test_doctor_parrot_requires_peer_system(tmp_path) -> None:
), ),
encoding="utf-8", encoding="utf-8",
) )
code = run_doctor(str(cfg), str(tmp_path), parrot=True, version="test") code = run_doctor(str(cfg), str(tmp_path), echo=True, version="test")
assert code == 1 assert code == 1

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests infrastructure parrot cli # ADN DMR Peer Server - tests infrastructure echo cli
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -18,7 +18,7 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
############################################################################### ###############################################################################
"""CLI and integrated parrot entry (Phase 4).""" """CLI and integrated echo entry."""
from __future__ import annotations from __future__ import annotations
@ -30,15 +30,15 @@ import pytest
from adn_server.main import main from adn_server.main import main
def test_parrot_flag_routes_to_run_parrot() -> None: def test_echo_flag_routes_to_run_echo() -> None:
with ( with (
patch("adn_server.main.run_parrot") as mock_run, patch("adn_server.main.run_echo") as mock_run,
patch("adn_server.main.YamlConfigLoader") as mock_loader_cls, patch("adn_server.main.YamlConfigLoader") as mock_loader_cls,
patch("adn_server.main.setup_logging") as mock_log, patch("adn_server.main.setup_logging") as mock_log,
): ):
mock_loader_cls.return_value.load.return_value = {"GLOBAL": {"SERVER_ID": 9990}, "LOGGER": {}} mock_loader_cls.return_value.load.return_value = {"GLOBAL": {"SERVER_ID": 9990}, "LOGGER": {}}
mock_log.return_value = _FakeLogger() mock_log.return_value = _FakeLogger()
with patch.object(sys, "argv", ["adn-server", "--parrot", "-c", "/tmp/adn-parrot.yaml"]): with patch.object(sys, "argv", ["adn-server", "--echo", "-c", "/tmp/adn-echo.yaml"]):
main() main()
mock_run.assert_called_once() mock_run.assert_called_once()
@ -56,7 +56,7 @@ def test_no_proxy_skips_proxy_startup(monkeypatch: pytest.MonkeyPatch) -> None:
raise AssertionError("proxy should not start") raise AssertionError("proxy should not start")
monkeypatch.setattr(peer_mod, "start_proxy_service", _track) monkeypatch.setattr(peer_mod, "start_proxy_service", _track)
monkeypatch.setattr(main_mod, "run_parrot", lambda *_a, **_k: None) monkeypatch.setattr(main_mod, "run_echo", lambda *_a, **_k: None)
monkeypatch.setattr(reactor, "run", lambda: None) monkeypatch.setattr(reactor, "run", lambda: None)
monkeypatch.setattr(peer_mod, "ReportServerFactory", lambda *a, **k: _FakeReportFactory()) monkeypatch.setattr(peer_mod, "ReportServerFactory", lambda *a, **k: _FakeReportFactory())
monkeypatch.setattr(peer_mod, "create_report_mqtt_publisher", lambda *_: None) monkeypatch.setattr(peer_mod, "create_report_mqtt_publisher", lambda *_: None)

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests infrastructure parrot config # ADN DMR Peer Server - tests infrastructure echo config
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -18,18 +18,18 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
############################################################################### ###############################################################################
"""Minimal adn-parrot.yaml validation.""" """Minimal adn-echo.yaml validation."""
from __future__ import annotations from __future__ import annotations
from adn_server.infrastructure.config_validator import validate_config from adn_server.infrastructure.config_validator import validate_config
MINIMAL_PARROT = { MINIMAL_ECHO = {
"GLOBAL": {"SERVER_ID": 9990}, "GLOBAL": {"SERVER_ID": 9990},
"LOGGER": {"LOG_FILE": "/var/log/adn-server/parrot.log"}, "LOGGER": {"LOG_FILE": "/var/log/adn-server/echo.log"},
"SYSTEMS": { "SYSTEMS": {
"PARROT": { "ECHO": {
"MODE": "PEER", "MODE": "PEER",
"IP": "127.0.0.1", "IP": "127.0.0.1",
"PORT": 54915, "PORT": 54915,
@ -44,11 +44,11 @@ MINIMAL_PARROT = {
} }
def test_minimal_parrot_config_validates_without_proxy() -> None: def test_minimal_echo_config_validates_without_proxy() -> None:
validate_config(MINIMAL_PARROT) validate_config(MINIMAL_ECHO)
def test_minimal_parrot_has_no_reports_or_aliases() -> None: def test_minimal_echo_has_no_reports_or_aliases() -> None:
assert "REPORTS" not in MINIMAL_PARROT assert "REPORTS" not in MINIMAL_ECHO
assert "ALIASES" not in MINIMAL_PARROT assert "ALIASES" not in MINIMAL_ECHO
assert "PROXY" not in MINIMAL_PARROT assert "PROXY" not in MINIMAL_ECHO

@ -85,8 +85,8 @@ def test_bridge_downlink_send_peers_filters_by_options() -> None:
assert len(tx_pkts) == 1 assert len(tx_pkts) == 1
def test_parrot_tg_9990_reaches_caller_without_9990_in_options() -> None: def test_echo_tg_9990_reaches_caller_without_9990_in_options() -> None:
"""Echo/parrot downlink must not be blocked by static OPTIONS (TG 9990).""" """Echo/echo downlink must not be blocked by static OPTIONS (TG 9990)."""
stack = _inject_proxy_stack() stack = _inject_proxy_stack()
stack.register_peer( stack.register_peer(
bytes_4(730039101), bytes_4(730039101),

@ -47,10 +47,10 @@ def test_proxy_section_required_when_master_present() -> None:
assert "PROXY" in str(exc.value) assert "PROXY" in str(exc.value)
def test_parrot_config_without_proxy_is_valid() -> None: def test_echo_config_without_proxy_is_valid() -> None:
validate_config({ validate_config({
"GLOBAL": {"SERVER_ID": 9990}, "GLOBAL": {"SERVER_ID": 9990},
"SYSTEMS": {"PARROT": {"MODE": "PEER", "ENABLED": True, "PORT": 54915}}, "SYSTEMS": {"ECHO": {"MODE": "PEER", "ENABLED": True, "PORT": 54915}},
}) })

@ -1,4 +1,4 @@
# ADN DMR Peer Server - tests routing parrot bootstrap seed # ADN DMR Peer Server - tests routing echo bootstrap seed
# #
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl> # Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
# #
@ -36,7 +36,7 @@ def _prod_like_config() -> dict:
return config return config
def test_parrot_routing_missing_without_echo_store_seed() -> None: def test_echo_routing_missing_without_echo_store_seed() -> None:
scenario = DeterministicScenario(config=_prod_like_config(), routing_table={}) scenario = DeterministicScenario(config=_prod_like_config(), routing_table={})
scenario.routing.apply_startup_subscriptions() scenario.routing.apply_startup_subscriptions()
@ -46,7 +46,7 @@ def test_parrot_routing_missing_without_echo_store_seed() -> None:
assert scenario.capture.for_system("ECHO") == [] assert scenario.capture.for_system("ECHO") == []
def test_parrot_routing_works_after_echo_seed_on_vhead() -> None: def test_echo_routing_works_after_echo_seed_on_vhead() -> None:
config = _prod_like_config() config = _prod_like_config()
scenario = DeterministicScenario(config=config, routing_table=_seed_echo_routing_table(config)) scenario = DeterministicScenario(config=config, routing_table=_seed_echo_routing_table(config))
scenario.routing.apply_startup_subscriptions() scenario.routing.apply_startup_subscriptions()

@ -18,7 +18,7 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
############################################################################### ###############################################################################
"""ECHO / parrot bridge leg restore after BRIDGERESET (regression for production outage).""" """ECHO / echo bridge leg restore after BRIDGERESET (regression for production outage)."""
from __future__ import annotations from __future__ import annotations

Loading…
Cancel
Save

Powered by TurnKey Linux.