From 04681bab142374372e8cb50ca6d3c9054fc96991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Sat, 13 Jun 2026 01:07:25 -0400 Subject: [PATCH] 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. --- .gitignore | 2 +- CHANGELOG.md | 10 ++--- README.md | 10 ++--- ...rrot.example.yaml => adn-echo.example.yaml | 8 ++-- adn-server.py | 4 +- docs/en/server/development/testing.md | 4 +- docs/en/server/user-guide/configuration.md | 8 ++-- docs/en/server/user-guide/echo.md | 42 +++++++++++++++++++ docs/en/server/user-guide/introduction.md | 2 +- docs/en/server/user-guide/monitoring.md | 4 +- docs/en/server/user-guide/parrot.md | 42 ------------------- docs/en/server/user-guide/special-numbers.md | 4 +- docs/en/server/user-guide/voice-and-tts.md | 2 +- docs/es/server/user-guide/configuration.md | 8 ++-- docs/es/server/user-guide/echo.md | 42 +++++++++++++++++++ docs/es/server/user-guide/hotspot-proxy.md | 2 +- docs/es/server/user-guide/introduction.md | 2 +- docs/es/server/user-guide/monitoring.md | 4 +- docs/es/server/user-guide/parrot.md | 42 ------------------- docs/es/server/user-guide/special-numbers.md | 4 +- docs/es/server/user-guide/voice-and-tts.md | 2 +- examples/systemd/adn-echo.service | 32 ++++++++++++++ examples/systemd/adn-parrot.service | 32 -------------- mkdocs.es.yml | 2 +- mkdocs.yml | 2 +- .../application/playback_use_cases.py | 2 +- .../application/proxy/deployment.py | 2 +- .../application/routing/subscription_table.py | 2 +- .../application/routing_use_cases.py | 6 +-- .../subscription/in_band_signalling_ops.py | 2 +- .../infrastructure/config_normalizer.py | 2 +- src/adn_server/infrastructure/doctor.py | 14 +++---- .../{parrot => echo}/__init__.py | 8 ++-- .../{parrot => echo}/runtime.py | 20 ++++----- .../twisted_adapters/udp_hbp.py | 2 +- src/adn_server/main.py | 22 +++++----- tests/README.md | 8 ++-- .../test_in_band_signalling_store_ops.py | 2 +- .../{parrot => echo}/test_playback_ingress.py | 14 +++---- .../{parrot => echo}/test_playback_logging.py | 4 +- .../test_playback_send_loop.py | 2 +- .../{parrot => echo}/test_recording_timers.py | 2 +- tests/{parrot => echo}/test_rekey_playback.py | 2 +- tests/infrastructure/test_doctor.py | 6 +-- .../{test_parrot_cli.py => test_echo_cli.py} | 12 +++--- ...t_parrot_config.py => test_echo_config.py} | 22 +++++----- .../test_hbp_repeat_options_filter.py | 4 +- tests/infrastructure/test_proxy_config.py | 4 +- ...ap_seed.py => test_echo_bootstrap_seed.py} | 6 +-- tests/routing/test_echo_subscription_reset.py | 2 +- 50 files changed, 244 insertions(+), 244 deletions(-) rename adn-parrot.example.yaml => adn-echo.example.yaml (70%) create mode 100644 docs/en/server/user-guide/echo.md delete mode 100644 docs/en/server/user-guide/parrot.md create mode 100644 docs/es/server/user-guide/echo.md delete mode 100644 docs/es/server/user-guide/parrot.md create mode 100644 examples/systemd/adn-echo.service delete mode 100644 examples/systemd/adn-parrot.service rename src/adn_server/infrastructure/{parrot => echo}/__init__.py (85%) rename src/adn_server/infrastructure/{parrot => echo}/runtime.py (85%) rename tests/{parrot => echo}/test_playback_ingress.py (89%) rename tests/{parrot => echo}/test_playback_logging.py (94%) rename tests/{parrot => echo}/test_playback_send_loop.py (98%) rename tests/{parrot => echo}/test_recording_timers.py (98%) rename tests/{parrot => echo}/test_rekey_playback.py (99%) rename tests/infrastructure/{test_parrot_cli.py => test_echo_cli.py} (92%) rename tests/infrastructure/{test_parrot_config.py => test_echo_config.py} (74%) rename tests/routing/{test_parrot_bootstrap_seed.py => test_echo_bootstrap_seed.py} (92%) diff --git a/.gitignore b/.gitignore index 5e9d9ed..bbea7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ # Production config (use *.example.yaml as template) adn-server.yaml -adn-parrot.yaml +adn-echo.yaml adn-voice.yaml *.local.yaml *.local.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5393340..de9b695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ First v2 release candidate since **1.0.0** (~70 commits). HBP/OpenBridge on-wire ### Added - **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. - **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. @@ -26,14 +26,14 @@ First v2 release candidate since **1.0.0** (~70 commits). HBP/OpenBridge on-wire ### Fixed - 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. - 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. ### 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`**. - 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). - 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. -- 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. - Hot reload: `adn-server.yaml` (SIGHUP), `adn-voice.yaml` (15 s loop), log reopen (SIGUSR2). - MkDocs user guide (EN/ES). @@ -62,7 +62,7 @@ First stable public release. ### Fixed (highlights since parity baseline) - 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. - OBP END/TX reporting and STATUS lifecycle alignment with legacy. diff --git a/README.md b/README.md index f80e21e..e9a4859 100644 --- a/README.md +++ b/README.md @@ -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 ``` -## Parrot (playback) +## Echo (playback) Separate process (same binary) records group voice and plays it back on TG 9990. ```bash -cp adn-parrot.example.yaml adn-parrot.yaml -python adn-server.py --parrot -c adn-parrot.yaml +cp adn-echo.example.yaml adn-echo.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`). diff --git a/adn-parrot.example.yaml b/adn-echo.example.yaml similarity index 70% rename from adn-parrot.example.yaml rename to adn-echo.example.yaml index f8dc240..6338b84 100644 --- a/adn-parrot.example.yaml +++ b/adn-echo.example.yaml @@ -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: -# 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. @@ -9,11 +9,11 @@ GLOBAL: SERVER_ID: 9990 LOGGER: - LOG_FILE: /var/log/adn-server/parrot.log + LOG_FILE: /var/log/adn-server/echo.log LOG_LEVEL: INFO SYSTEMS: - PARROT: + ECHO: MODE: PEER IP: 127.0.0.1 PORT: 54915 diff --git a/adn-server.py b/adn-server.py index c19f396..61b845c 100644 --- a/adn-server.py +++ b/adn-server.py @@ -28,10 +28,10 @@ Run the ADN DMR Peer Server from the project root. python adn-server.py python adn-server.py -c adn-server.yaml 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 -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 diff --git a/docs/en/server/development/testing.md b/docs/en/server/development/testing.md index 845915c..995b49f 100644 --- a/docs/en/server/development/testing.md +++ b/docs/en/server/development/testing.md @@ -25,14 +25,14 @@ python3 -m pytest tests/ -q ```bash python3 -m pytest tests/routing/ -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): ```bash 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**: diff --git a/docs/en/server/user-guide/configuration.md b/docs/en/server/user-guide/configuration.md index 24086a7..143fc8a 100644 --- a/docs/en/server/user-guide/configuration.md +++ b/docs/en/server/user-guide/configuration.md @@ -46,12 +46,12 @@ 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 | +| **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) | **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. | | **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. - [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). +- [Echo](echo.md) — PEER example (echo process). - [Hotspot proxy](hotspot-proxy.md) — integrated **`PROXY`** / **`SELF_SERVICE`**. diff --git a/docs/en/server/user-guide/echo.md b/docs/en/server/user-guide/echo.md new file mode 100644 index 0000000..123f486 --- /dev/null +++ b/docs/en/server/user-guide/echo.md @@ -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. diff --git a/docs/en/server/user-guide/introduction.md b/docs/en/server/user-guide/introduction.md index 5a588f6..4fa25f5 100644 --- a/docs/en/server/user-guide/introduction.md +++ b/docs/en/server/user-guide/introduction.md @@ -26,7 +26,7 @@ Routing, timers, OpenBridge loop control, and protocol handling are implemented ## 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). ## Next steps diff --git a/docs/en/server/user-guide/monitoring.md b/docs/en/server/user-guide/monitoring.md index 3cc97aa..fa7390f 100644 --- a/docs/en/server/user-guide/monitoring.md +++ b/docs/en/server/user-guide/monitoring.md @@ -50,7 +50,7 @@ These processes handle **`SIGUSR2`** by reopening **`logging.FileHandler`** stre | 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` | 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 diff --git a/docs/en/server/user-guide/parrot.md b/docs/en/server/user-guide/parrot.md deleted file mode 100644 index 56b1ea2..0000000 --- a/docs/en/server/user-guide/parrot.md +++ /dev/null @@ -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. diff --git a/docs/en/server/user-guide/special-numbers.md b/docs/en/server/user-guide/special-numbers.md index 953a828..3f70ec1 100644 --- a/docs/en/server/user-guide/special-numbers.md +++ b/docs/en/server/user-guide/special-numbers.md @@ -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). -## 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). -**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 diff --git a/docs/en/server/user-guide/voice-and-tts.md b/docs/en/server/user-guide/voice-and-tts.md index 2845cc3..d8f8076 100644 --- a/docs/en/server/user-guide/voice-and-tts.md +++ b/docs/en/server/user-guide/voice-and-tts.md @@ -52,5 +52,5 @@ Parallel **broadcasts** on **different** TGs may run concurrently; **same-TG** b ## See also - [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. diff --git a/docs/es/server/user-guide/configuration.md b/docs/es/server/user-guide/configuration.md index fa971cd..7262cce 100644 --- a/docs/es/server/user-guide/configuration.md +++ b/docs/es/server/user-guide/configuration.md @@ -46,12 +46,12 @@ 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 | +| **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) | **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. | | **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. - [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). +- [Echo](echo.md) — ejemplo PEER (proceso echo). - [Proxy hotspot](hotspot-proxy.md) — **`PROXY`** / **`SELF_SERVICE`** integrados. diff --git a/docs/es/server/user-guide/echo.md b/docs/es/server/user-guide/echo.md new file mode 100644 index 0000000..b3fed9a --- /dev/null +++ b/docs/es/server/user-guide/echo.md @@ -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. diff --git a/docs/es/server/user-guide/hotspot-proxy.md b/docs/es/server/user-guide/hotspot-proxy.md index 05276ae..6614ca0 100644 --- a/docs/es/server/user-guide/hotspot-proxy.md +++ b/docs/es/server/user-guide/hotspot-proxy.md @@ -84,7 +84,7 @@ Detalle del flujo en el panel: [Self-service](../../monitor/self-service.md). ## 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. -- 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)). --- diff --git a/docs/es/server/user-guide/introduction.md b/docs/es/server/user-guide/introduction.md index edafaa0..645a660 100644 --- a/docs/es/server/user-guide/introduction.md +++ b/docs/es/server/user-guide/introduction.md @@ -26,7 +26,7 @@ Enrutado, temporizadores, control de bucle OpenBridge y manejo de protocolo est ## 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). ## Siguientes pasos diff --git a/docs/es/server/user-guide/monitoring.md b/docs/es/server/user-guide/monitoring.md index f98500e..0eca217 100644 --- a/docs/es/server/user-guide/monitoring.md +++ b/docs/es/server/user-guide/monitoring.md @@ -50,7 +50,7 @@ Estos procesos tratan **`SIGUSR2`** solo para **reabrir** los ficheros de log (` | 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` | 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 diff --git a/docs/es/server/user-guide/parrot.md b/docs/es/server/user-guide/parrot.md deleted file mode 100644 index fcd8b98..0000000 --- a/docs/es/server/user-guide/parrot.md +++ /dev/null @@ -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. diff --git a/docs/es/server/user-guide/special-numbers.md b/docs/es/server/user-guide/special-numbers.md index d3f7294..b74bec8 100644 --- a/docs/es/server/user-guide/special-numbers.md +++ b/docs/es/server/user-guide/special-numbers.md @@ -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). -## 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). -**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 diff --git a/docs/es/server/user-guide/voice-and-tts.md b/docs/es/server/user-guide/voice-and-tts.md index a2e385a..f700589 100644 --- a/docs/es/server/user-guide/voice-and-tts.md +++ b/docs/es/server/user-guide/voice-and-tts.md @@ -53,5 +53,5 @@ Las **emisiones** en paralelo en **TG distintas** pueden ejecutarse a la vez; la ## Ver también - [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. diff --git a/examples/systemd/adn-echo.service b/examples/systemd/adn-echo.service new file mode 100644 index 0000000..9f0cdc4 --- /dev/null +++ b/examples/systemd/adn-echo.service @@ -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 diff --git a/examples/systemd/adn-parrot.service b/examples/systemd/adn-parrot.service deleted file mode 100644 index 8540c70..0000000 --- a/examples/systemd/adn-parrot.service +++ /dev/null @@ -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 diff --git a/mkdocs.es.yml b/mkdocs.es.yml index 14cead4..81c53d4 100644 --- a/mkdocs.es.yml +++ b/mkdocs.es.yml @@ -57,7 +57,7 @@ nav: - Voz, anuncios y TTS: server/user-guide/voice-and-tts.md - Monitor e informes: server/user-guide/monitoring.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 - Protocolos: - HBP (DMRD): server/protocols/hbp.md diff --git a/mkdocs.yml b/mkdocs.yml index 994521a..eecd9fc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,7 +57,7 @@ nav: - Voice, announcements, and TTS: server/user-guide/voice-and-tts.md - Monitoring and reports: server/user-guide/monitoring.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 - Protocols: - HBP (DMRD): server/protocols/hbp.md diff --git a/src/adn_server/application/playback_use_cases.py b/src/adn_server/application/playback_use_cases.py index db8269c..8b8d769 100644 --- a/src/adn_server/application/playback_use_cases.py +++ b/src/adn_server/application/playback_use_cases.py @@ -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 # # Derived from ADN DMR Server / FreeDMR / HBlink. Original license: diff --git a/src/adn_server/application/proxy/deployment.py b/src/adn_server/application/proxy/deployment.py index 6098c90..c6ae37f 100644 --- a/src/adn_server/application/proxy/deployment.py +++ b/src/adn_server/application/proxy/deployment.py @@ -32,7 +32,7 @@ def proxy_target_system(config: dict[str, Any]) -> str | None: 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", {}) if not isinstance(systems, dict): return False diff --git a/src/adn_server/application/routing/subscription_table.py b/src/adn_server/application/routing/subscription_table.py index d3eef53..cc8924a 100644 --- a/src/adn_server/application/routing/subscription_table.py +++ b/src/adn_server/application/routing/subscription_table.py @@ -543,7 +543,7 @@ class SubscriptionTableMixin: self._sync_subscription_store() return 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._sync_subscription_store() return diff --git a/src/adn_server/application/routing_use_cases.py b/src/adn_server/application/routing_use_cases.py index 4827fe5..906bc4c 100644 --- a/src/adn_server/application/routing_use_cases.py +++ b/src/adn_server/application/routing_use_cases.py @@ -212,7 +212,7 @@ class RoutingUseCases( system_name, peer_id, rf_src, dst_id, seq, slot, stream_id, data, pkt_time, ): 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: self.apply_in_band_signalling(system_name, slot, dst_id, pkt_time) from .subscription.subscription_queries import store_has_table @@ -264,7 +264,7 @@ class RoutingUseCases( obp_hops if obp_use_parsed else b"", ): 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). if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD: 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: # 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 - # 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). if not has_source: logger.debug( diff --git a/src/adn_server/application/subscription/in_band_signalling_ops.py b/src/adn_server/application/subscription/in_band_signalling_ops.py index 19d87f5..87b9c2f 100644 --- a/src/adn_server/application/subscription/in_band_signalling_ops.py +++ b/src/adn_server/application/subscription/in_band_signalling_ops.py @@ -130,7 +130,7 @@ def apply_in_band_signalling_store( or dst_id_b != tgid_b 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 ( sub.policy == ActivationPolicy.STATIC and dst_id_b != tgid_b diff --git a/src/adn_server/infrastructure/config_normalizer.py b/src/adn_server/infrastructure/config_normalizer.py index 46c819b..d81ade8 100644 --- a/src/adn_server/infrastructure/config_normalizer.py +++ b/src/adn_server/infrastructure/config_normalizer.py @@ -22,7 +22,7 @@ # 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 diff --git a/src/adn_server/infrastructure/doctor.py b/src/adn_server/infrastructure/doctor.py index a952069..1618260 100644 --- a/src/adn_server/infrastructure/doctor.py +++ b/src/adn_server/infrastructure/doctor.py @@ -98,22 +98,22 @@ def collect_findings( *, project_root: str, config_path: str, - parrot: bool = False, + echo: bool = False, no_proxy: bool = False, ) -> list[Finding]: findings: list[Finding] = [] logger = _NullLogger() - if parrot: + if echo: peers = [ name for name, cfg in config.get("SYSTEMS", {}).items() if isinstance(cfg, dict) and cfg.get("MODE") == "PEER" and cfg.get("ENABLED", True) ] 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: - findings.append(Finding("error", "parrot", "no enabled PEER systems in config")) + findings.append(Finding("error", "echo", "no enabled PEER systems in config")) else: apply_talker_alias_defaults(config) expand_generator(config, logger) @@ -196,7 +196,7 @@ def collect_findings( else: findings.append(Finding("warn", "systems", f"{name}: unknown MODE={mode}")) - if not parrot: + if not echo: reports = config.get("REPORTS", {}) if reports.get("REPORT", True): port = int(reports.get("REPORT_PORT", 4321)) @@ -253,7 +253,7 @@ def run_doctor( config_path: str, project_root: str, *, - parrot: bool = False, + echo: bool = False, no_proxy: bool = False, version: str = "", out: TextIO | None = None, @@ -271,7 +271,7 @@ def run_doctor( config, project_root=project_root, config_path=config_path, - parrot=parrot, + echo=echo, no_proxy=no_proxy, ) print(format_report(findings, version=version, config_path=config_path), file=stream) diff --git a/src/adn_server/infrastructure/parrot/__init__.py b/src/adn_server/infrastructure/echo/__init__.py similarity index 85% rename from src/adn_server/infrastructure/parrot/__init__.py rename to src/adn_server/infrastructure/echo/__init__.py index 4729d8a..f212f29 100644 --- a/src/adn_server/infrastructure/parrot/__init__.py +++ b/src/adn_server/infrastructure/echo/__init__.py @@ -1,4 +1,4 @@ -# ADN DMR Peer Server - infrastructure parrot init +# ADN DMR Peer Server - infrastructure echo # # Copyright (C) 2026 Rodrigo Pérez, CE5RPY # @@ -18,8 +18,8 @@ # 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"] diff --git a/src/adn_server/infrastructure/parrot/runtime.py b/src/adn_server/infrastructure/echo/runtime.py similarity index 85% rename from src/adn_server/infrastructure/parrot/runtime.py rename to src/adn_server/infrastructure/echo/runtime.py index 8ae7017..b8f9dcf 100644 --- a/src/adn_server/infrastructure/parrot/runtime.py +++ b/src/adn_server/infrastructure/echo/runtime.py @@ -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 # @@ -18,7 +18,7 @@ # 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 @@ -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()) -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).""" ensure_system_runtime_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) 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: 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) 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.SIGINT, sig_handler) @@ -91,13 +91,13 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None: if hasattr(signal, "SIGHUP"): signal.signal(signal.SIGHUP, sighup_ignore) - logger.info("ADN Parrot -- SYSTEM STARTING...") + logger.info("ADN Echo -- SYSTEM STARTING...") started = 0 for system_name, sys_cfg in systems_cfg.items(): if not sys_cfg.get("ENABLED", True): continue 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 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") started += 1 logger.info( - "(PARROT) %s PEER on %s:%s → master %s:%s", + "(ECHO) %s PEER on %s:%s → master %s:%s", system_name, ip or "0.0.0.0", udp_port, @@ -124,7 +124,7 @@ def run_parrot(config: dict[str, Any], *, logger: logging.Logger) -> None: ) if started == 0: - logger.critical("(PARROT) no enabled PEER systems in config") - sys.exit("parrot config must define at least one enabled SYSTEMS. with MODE: PEER") + logger.critical("(ECHO) no enabled PEER systems in config") + sys.exit("echo config must define at least one enabled SYSTEMS. with MODE: PEER") reactor.run() diff --git a/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py b/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py index 581b869..013c696 100644 --- a/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py +++ b/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py @@ -568,7 +568,7 @@ class HBPProtocol(DatagramProtocol): self._ta_decoded_logged.discard(stream_id) 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) if not entry or not entry.get("blocks"): return diff --git a/src/adn_server/main.py b/src/adn_server/main.py index 46edfea..2704100 100644 --- a/src/adn_server/main.py +++ b/src/adn_server/main.py @@ -26,9 +26,9 @@ ADN DMR Peer Server entrypoint. 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] -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 @@ -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.config_normalizer import apply_talker_alias_defaults 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: @@ -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("--logging", dest="LOG_LEVEL", default=None, help="Override log level") parser.add_argument( - "--parrot", + "--echo", 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( "--no-proxy", @@ -98,7 +98,7 @@ def main() -> None: args = _parse_args() 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) if args.doctor: @@ -106,7 +106,7 @@ def main() -> None: run_doctor( config_path, project_root, - parrot=args.parrot, + echo=args.echo, no_proxy=args.no_proxy, version=__version__, ) @@ -119,11 +119,11 @@ def main() -> None: print(f"(CONFIG) {exc}", file=sys.stderr) sys.exit(1) - if not args.parrot: + if not args.echo: apply_talker_alias_defaults(config) 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: config.setdefault("VOICE", {}).update(voice_data) @@ -132,8 +132,8 @@ def main() -> None: logger = setup_logging(config.get("LOGGER", {})) _log_copyright(logger) - if args.parrot: - run_parrot(config, logger=logger) + if args.echo: + run_echo(config, logger=logger) return run_peer_server( diff --git a/tests/README.md b/tests/README.md index 2c28505..e40eb19 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 | | `voice/` | Announcements, TTS schedule, broadcast queue, disconnected voice, in-band signalling | | `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 | | `schemas/` | Report v2 JSON Schema validation (`jsonschema` dev dep) | | `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_passthrough.py` | 9 | Passthrough / both modes | -### parrot/ +### echo/ | File | Tests | Topic | |------|-------|-------| @@ -156,8 +156,8 @@ Session replay JSONL meta accepts `routing_table` (preferred) or legacy `bridges # After changing unit-data routing python3 -m pytest tests/routing/test_unit_data_routing.py -q -# After parrot seq fix -python3 -m pytest tests/parrot/test_rekey_playback.py -q +# After echo seq fix +python3 -m pytest tests/echo/test_rekey_playback.py -q # Talker Alias REPEAT (real stack) python3 -m pytest tests/infrastructure/test_hbp_repeat_talker_alias.py -q diff --git a/tests/application/test_in_band_signalling_store_ops.py b/tests/application/test_in_band_signalling_store_ops.py index 972e808..b516418 100644 --- a/tests/application/test_in_band_signalling_store_ops.py +++ b/tests/application/test_in_band_signalling_store_ops.py @@ -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: - """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() 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) diff --git a/tests/parrot/test_playback_ingress.py b/tests/echo/test_playback_ingress.py similarity index 89% rename from tests/parrot/test_playback_ingress.py rename to tests/echo/test_playback_ingress.py index b718272..3dd0207 100644 --- a/tests/parrot/test_playback_ingress.py +++ b/tests/echo/test_playback_ingress.py @@ -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 # @@ -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: - """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() - pb = PlaybackUseCases("PARROT", get_protocol=lambda: proto) + pb = PlaybackUseCases("ECHO", get_protocol=lambda: proto) base = PacketSpec(dst_id=9990, stream_id=0x12121212, slot=2) args = DeterministicScenario.voice_head_spec(base).decoded_hbp_args() pb.dmrd_received( - "PARROT", + "ECHO", args["peer_id"], args["rf_src"], 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: """Regression: PEER path (ingress_pkt_time) records voice and schedules playback.""" 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) mock_reactor, scheduled = install_reactor_capture() 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.time", return_value=t0): - send_playback(pb, "PARROT", DeterministicScenario.voice_head_spec(base)) + send_playback(pb, "ECHO", DeterministicScenario.voice_head_spec(base)) send_playback( pb, - "PARROT", + "ECHO", DeterministicScenario.voice_burst_spec(base, seq=1, dtype_vseq=1), ingress_pkt_time=t0 + 2.0, ) diff --git a/tests/parrot/test_playback_logging.py b/tests/echo/test_playback_logging.py similarity index 94% rename from tests/parrot/test_playback_logging.py rename to tests/echo/test_playback_logging.py index 0c90fd3..6b63601 100644 --- a/tests/parrot/test_playback_logging.py +++ b/tests/echo/test_playback_logging.py @@ -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 # @@ -35,7 +35,7 @@ from adn_server.domain import bytes_3, bytes_4 def test_start_playback_logs_duration_with_two_decimals(caplog) -> None: """PLAYBACK duration matches bridge-style %.2f (no float noise in logs).""" 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) recorded = [DeterministicScenario.voice_head_spec(base).data()] diff --git a/tests/parrot/test_playback_send_loop.py b/tests/echo/test_playback_send_loop.py similarity index 98% rename from tests/parrot/test_playback_send_loop.py rename to tests/echo/test_playback_send_loop.py index 1f5cc9f..cc25cda 100644 --- a/tests/parrot/test_playback_send_loop.py +++ b/tests/echo/test_playback_send_loop.py @@ -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 # diff --git a/tests/parrot/test_recording_timers.py b/tests/echo/test_recording_timers.py similarity index 98% rename from tests/parrot/test_recording_timers.py rename to tests/echo/test_recording_timers.py index d2b82b6..24d97ce 100644 --- a/tests/parrot/test_recording_timers.py +++ b/tests/echo/test_recording_timers.py @@ -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 # diff --git a/tests/parrot/test_rekey_playback.py b/tests/echo/test_rekey_playback.py similarity index 99% rename from tests/parrot/test_rekey_playback.py rename to tests/echo/test_rekey_playback.py index 403cd08..84cfec2 100644 --- a/tests/parrot/test_rekey_playback.py +++ b/tests/echo/test_rekey_playback.py @@ -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 # diff --git a/tests/infrastructure/test_doctor.py b/tests/infrastructure/test_doctor.py index 3dbd0ac..65722c7 100644 --- a/tests/infrastructure/test_doctor.py +++ b/tests/infrastructure/test_doctor.py @@ -28,8 +28,8 @@ import textwrap from adn_server.infrastructure.doctor import collect_findings, run_doctor -def test_doctor_parrot_requires_peer_system(tmp_path) -> None: - cfg = tmp_path / "adn-parrot.yaml" +def test_doctor_echo_requires_peer_system(tmp_path) -> None: + cfg = tmp_path / "adn-echo.yaml" cfg.write_text( textwrap.dedent( """ @@ -45,7 +45,7 @@ def test_doctor_parrot_requires_peer_system(tmp_path) -> None: ), 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 diff --git a/tests/infrastructure/test_parrot_cli.py b/tests/infrastructure/test_echo_cli.py similarity index 92% rename from tests/infrastructure/test_parrot_cli.py rename to tests/infrastructure/test_echo_cli.py index 8d8efdc..3d0e3d9 100644 --- a/tests/infrastructure/test_parrot_cli.py +++ b/tests/infrastructure/test_echo_cli.py @@ -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 # @@ -18,7 +18,7 @@ # 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 @@ -30,15 +30,15 @@ import pytest 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 ( - 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.setup_logging") as mock_log, ): mock_loader_cls.return_value.load.return_value = {"GLOBAL": {"SERVER_ID": 9990}, "LOGGER": {}} 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() 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") 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(peer_mod, "ReportServerFactory", lambda *a, **k: _FakeReportFactory()) monkeypatch.setattr(peer_mod, "create_report_mqtt_publisher", lambda *_: None) diff --git a/tests/infrastructure/test_parrot_config.py b/tests/infrastructure/test_echo_config.py similarity index 74% rename from tests/infrastructure/test_parrot_config.py rename to tests/infrastructure/test_echo_config.py index b91249d..4c2233b 100644 --- a/tests/infrastructure/test_parrot_config.py +++ b/tests/infrastructure/test_echo_config.py @@ -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 # @@ -18,18 +18,18 @@ # 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 adn_server.infrastructure.config_validator import validate_config -MINIMAL_PARROT = { +MINIMAL_ECHO = { "GLOBAL": {"SERVER_ID": 9990}, - "LOGGER": {"LOG_FILE": "/var/log/adn-server/parrot.log"}, + "LOGGER": {"LOG_FILE": "/var/log/adn-server/echo.log"}, "SYSTEMS": { - "PARROT": { + "ECHO": { "MODE": "PEER", "IP": "127.0.0.1", "PORT": 54915, @@ -44,11 +44,11 @@ MINIMAL_PARROT = { } -def test_minimal_parrot_config_validates_without_proxy() -> None: - validate_config(MINIMAL_PARROT) +def test_minimal_echo_config_validates_without_proxy() -> None: + validate_config(MINIMAL_ECHO) -def test_minimal_parrot_has_no_reports_or_aliases() -> None: - assert "REPORTS" not in MINIMAL_PARROT - assert "ALIASES" not in MINIMAL_PARROT - assert "PROXY" not in MINIMAL_PARROT +def test_minimal_echo_has_no_reports_or_aliases() -> None: + assert "REPORTS" not in MINIMAL_ECHO + assert "ALIASES" not in MINIMAL_ECHO + assert "PROXY" not in MINIMAL_ECHO diff --git a/tests/infrastructure/test_hbp_repeat_options_filter.py b/tests/infrastructure/test_hbp_repeat_options_filter.py index 5397299..a5cd291 100644 --- a/tests/infrastructure/test_hbp_repeat_options_filter.py +++ b/tests/infrastructure/test_hbp_repeat_options_filter.py @@ -85,8 +85,8 @@ def test_bridge_downlink_send_peers_filters_by_options() -> None: assert len(tx_pkts) == 1 -def test_parrot_tg_9990_reaches_caller_without_9990_in_options() -> None: - """Echo/parrot downlink must not be blocked by static OPTIONS (TG 9990).""" +def test_echo_tg_9990_reaches_caller_without_9990_in_options() -> None: + """Echo/echo downlink must not be blocked by static OPTIONS (TG 9990).""" stack = _inject_proxy_stack() stack.register_peer( bytes_4(730039101), diff --git a/tests/infrastructure/test_proxy_config.py b/tests/infrastructure/test_proxy_config.py index 925fa39..b34c1c5 100644 --- a/tests/infrastructure/test_proxy_config.py +++ b/tests/infrastructure/test_proxy_config.py @@ -47,10 +47,10 @@ def test_proxy_section_required_when_master_present() -> None: 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({ "GLOBAL": {"SERVER_ID": 9990}, - "SYSTEMS": {"PARROT": {"MODE": "PEER", "ENABLED": True, "PORT": 54915}}, + "SYSTEMS": {"ECHO": {"MODE": "PEER", "ENABLED": True, "PORT": 54915}}, }) diff --git a/tests/routing/test_parrot_bootstrap_seed.py b/tests/routing/test_echo_bootstrap_seed.py similarity index 92% rename from tests/routing/test_parrot_bootstrap_seed.py rename to tests/routing/test_echo_bootstrap_seed.py index a74a44b..7b18198 100644 --- a/tests/routing/test_parrot_bootstrap_seed.py +++ b/tests/routing/test_echo_bootstrap_seed.py @@ -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 # @@ -36,7 +36,7 @@ def _prod_like_config() -> dict: 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.routing.apply_startup_subscriptions() @@ -46,7 +46,7 @@ def test_parrot_routing_missing_without_echo_store_seed() -> None: 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() scenario = DeterministicScenario(config=config, routing_table=_seed_echo_routing_table(config)) scenario.routing.apply_startup_subscriptions() diff --git a/tests/routing/test_echo_subscription_reset.py b/tests/routing/test_echo_subscription_reset.py index 2ae583e..0760472 100644 --- a/tests/routing/test_echo_subscription_reset.py +++ b/tests/routing/test_echo_subscription_reset.py @@ -18,7 +18,7 @@ # 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