diff --git a/adn-server.example.yaml b/adn-server.example.yaml index cb46178..56d5584 100644 --- a/adn-server.example.yaml +++ b/adn-server.example.yaml @@ -17,12 +17,14 @@ GLOBAL: PASS_SECURITY: "" USERS_PASS: user_passwords.json HASH_ENCRYPT: encryption_key.secret - # Talker Alias (HBP DMRA): see docs/en/server/user-guide/talker-alias.md + # Talker Alias (embedded LC + optional HBP DMRA): see docs/en/server/user-guide/talker-alias.md TALKER_ALIAS: false TALKER_ALIAS_MODE: both TALKER_ALIAS_FORMAT: "{callsign} {fname}" # utf8 (Motorola), iso8 (Hytera), 7bit; both vendors by default TALKER_ALIAS_TEXT_FORMAT: "utf8,iso8" + # Standalone DMRA UDP (default false — legacy-like; radios use embedded LC in DMRD) + TALKER_ALIAS_SEND_DMRA: false # UDP SO_RCVBUF for voice listeners (bytes); default 4 MB avoids kernel RcvbufErrors under load # UDP_RCVBUF: 4194304 diff --git a/docs/en/server/user-guide/configuration.md b/docs/en/server/user-guide/configuration.md index 4788510..cb724f3 100644 --- a/docs/en/server/user-guide/configuration.md +++ b/docs/en/server/user-guide/configuration.md @@ -84,6 +84,7 @@ Optional DMR Talker Alias on HBP (`DMRA` packets). Full guide: [Talker Alias](ta | **TALKER_ALIAS** | Enable server TA inject/passthrough (`false` default). | | **TALKER_ALIAS_MODE** | `both` (default), `passthrough`, or `inject`. | | **TALKER_ALIAS_FORMAT** | Template, e.g. `{callsign} {fname}`. Max **29** chars (protocol limit, not YAML). | +| **TALKER_ALIAS_SEND_DMRA** | Send standalone HBP `DMRA` UDP (`false` default; radios use embedded LC). | | **TALKER_ALIAS_TEXT_FORMAT** | `utf8`, `iso8`, `7bit`, or comma list (e.g. `utf8,iso8` for Motorola + Hytera). Default `utf8`. | --- diff --git a/docs/en/server/user-guide/talker-alias.md b/docs/en/server/user-guide/talker-alias.md index 31ea226..b0a3add 100644 --- a/docs/en/server/user-guide/talker-alias.md +++ b/docs/en/server/user-guide/talker-alias.md @@ -18,7 +18,7 @@ When enabled on a **MASTER** system, the server can: | **`passthrough`** | Relay the source's TA only (buffered `DMRA` and the source embedded LC, both untouched). Never inject the template. | | **`inject`** | Always build TA from the configured template and alias data, overwriting the embedded LC and sending `DMRA` packets. | -On **bridge forward** at voice header (`VHEAD`), the server sends four `DMRA` packets to each HBP target (**MASTER** peers or **PEER** upstream) once per stream, then forwards `DMRD` as usual. +On **bridge forward** at voice header (`VHEAD`), when **`TALKER_ALIAS_SEND_DMRA`** is `true`, the server may send up to four `DMRA` packets to each HBP target (**MASTER** peers or **PEER** upstream) once per stream, then forwards `DMRD` as usual. With the default (`false`), only embedded LC is used — closer to legacy ADN masters that often show **no** `DMRA` on the wire. **MMDVMHost / DMRGateway (Pi-Star, WPSD):** stock MMDVMHost does **not** consume standalone downlink `DMRA` UDP; it decodes Talker Alias from **embedded LC inside `DMRD` voice** (FLCO 4–7). @@ -43,6 +43,7 @@ GLOBAL: TALKER_ALIAS_MODE: both TALKER_ALIAS_FORMAT: "TA {callsign} {fname}" TALKER_ALIAS_TEXT_FORMAT: "utf8,iso8" + TALKER_ALIAS_SEND_DMRA: false ``` | Key | Meaning | @@ -51,6 +52,7 @@ GLOBAL: | **TALKER_ALIAS_MODE** | `both`, `passthrough`, or `inject`. Default **`both`** if omitted. | | **TALKER_ALIAS_FORMAT** | Python format string; fields: `{callsign}`, `{fname}`, `{surname}`, `{id}`. | | **TALKER_ALIAS_TEXT_FORMAT** | TA payload encoding: `utf8` (Motorola / MMDVMHost default), `iso8` (ISO-8859-1, many Hytera models), or `7bit` (oldest radios). Comma-separated list (e.g. `utf8,iso8`) emits **both** encodings back-to-back in embedded LC so each vendor can pick the format it displays; standalone `DMRA` UDP uses the **first** format only. Default **`utf8`**. | +| **TALKER_ALIAS_SEND_DMRA** | Send standalone HBP `DMRA` UDP on VHEAD (`false` by default). Radios / MMDVMHost use **embedded LC** in `DMRD`; leaving this off matches typical legacy wire (no `DMRA` toward the peer). Set `true` only if a client needs the UDP packets. | Maximum string length is **29 characters** (ETSI / MMDVMHost). This limit is fixed in code and is **not** configurable, to avoid incompatible payloads on radios and hotspots. diff --git a/docs/es/server/user-guide/configuration.md b/docs/es/server/user-guide/configuration.md index 9e87ec5..be05eff 100644 --- a/docs/es/server/user-guide/configuration.md +++ b/docs/es/server/user-guide/configuration.md @@ -84,6 +84,7 @@ Talker Alias DMR opcional en HBP (paquetes `DMRA`). Guía completa: [Talker Alia | **TALKER_ALIAS** | Activa inyección/passthrough de TA (`false` por defecto). | | **TALKER_ALIAS_MODE** | `both` (por defecto), `passthrough` o `inject`. | | **TALKER_ALIAS_FORMAT** | Plantilla, p. ej. `{callsign} {fname}`. Máx. **29** caracteres (límite de protocolo, no YAML). | +| **TALKER_ALIAS_SEND_DMRA** | Enviar `DMRA` UDP autónomo (`false` por defecto; las radios usan LC embebida). | | **TALKER_ALIAS_TEXT_FORMAT** | `utf8`, `iso8`, `7bit` o lista con comas (p. ej. `utf8,iso8`). Por defecto `utf8`. | --- diff --git a/docs/es/server/user-guide/talker-alias.md b/docs/es/server/user-guide/talker-alias.md index 00277bb..3368e1f 100644 --- a/docs/es/server/user-guide/talker-alias.md +++ b/docs/es/server/user-guide/talker-alias.md @@ -18,9 +18,9 @@ Con la función activada en un sistema **MASTER**, el servidor puede: | **`passthrough`** | Solo reenvía `DMRA` bufferizado. | | **`inject`** | Siempre genera TA desde la plantilla y datos de alias. | -En el **reenvío por bridge** en cabecera de voz (`VHEAD`), el servidor envía cuatro paquetes `DMRA` a cada destino HBP (**MASTER** peers o **PEER** upstream) una vez por stream, y después reenvía `DMRD` como siempre. +En el **reenvío por bridge** en cabecera de voz (`VHEAD`), si **`TALKER_ALIAS_SEND_DMRA`** es `true`, el servidor puede enviar hasta cuatro paquetes `DMRA` a cada destino HBP una vez por stream. Con el valor por defecto (`false`) solo se usa la LC embebida — más parecido a masters legacy que a menudo no muestran `DMRA` en el cable. -**MMDVMHost / DMRGateway (Pi-Star, WPSD):** el MMDVMHost estándar **no** procesa `DMRA` UDP independiente en downlink; decodifica Talker Alias desde la **LC embebida en voz `DMRD`** (FLCO 4–7). Con TA activado, ADN inyecta el TA en la LC embebida de los bursts **B–E** (dtype 1–4) en el **reenvío por bridge** hacia destinos HBP, alternando LC de grupo y bloques TA durante todo el stream, además de los paquetes `DMRA` opcionales para clientes que los soporten. +**MMDVMHost / DMRGateway (Pi-Star, WPSD):** el MMDVMHost estándar **no** procesa `DMRA` UDP independiente en downlink; decodifica Talker Alias desde la **LC embebida en voz `DMRD`** (FLCO 4–7). Con TA activado, ADN inyecta el TA en la LC embebida de los bursts **B–E** (dtype 1–4) en el **reenvío por bridge** hacia destinos HBP, alternando LC de grupo y bloques TA durante todo el stream; los paquetes `DMRA` son opcionales (`TALKER_ALIAS_SEND_DMRA`). En el **mismo MASTER**, cuando **`REPEAT`** copia voz de grupo a otros hotspots registrados, el servidor también envía esos cuatro `DMRA` en `VHEAD` (excluyendo el peer que transmite). Si el bridge apunta al mismo system, comparten la misma deduplicación por stream y el TA no se envía dos veces. @@ -38,6 +38,7 @@ GLOBAL: TALKER_ALIAS_MODE: both TALKER_ALIAS_FORMAT: "{callsign} {fname}" TALKER_ALIAS_TEXT_FORMAT: "utf8,iso8" + TALKER_ALIAS_SEND_DMRA: false ``` | Clave | Significado | @@ -46,6 +47,7 @@ GLOBAL: | **TALKER_ALIAS_MODE** | `both`, `passthrough` o `inject`. Por defecto **`both`** si se omite. | | **TALKER_ALIAS_FORMAT** | Plantilla tipo Python; campos: `{callsign}`, `{fname}`, `{surname}`, `{id}`. | | **TALKER_ALIAS_TEXT_FORMAT** | Codificación del TA: `utf8` (Motorola), `iso8` (Hytera), `7bit`. Lista separada por comas (p. ej. `utf8,iso8`) emite ambas en LC embebido; `DMRA` UDP usa solo el **primer** formato. Por defecto **`utf8`**. | +| **TALKER_ALIAS_SEND_DMRA** | Enviar `DMRA` UDP autónomo en VHEAD (`false` por defecto). Las radios / MMDVMHost usan la **LC embebida** en `DMRD`; dejarlo en off se acerca al cable legacy (sin `DMRA` hacia el peer). Pon `true` solo si un cliente necesita esos paquetes UDP. | La longitud máxima es **29 caracteres** (ETSI / MMDVMHost). Este límite está fijado en código y **no** es configurable, para evitar payloads incompatibles en radios y hotspots. diff --git a/src/adn_server/application/routing/lc_ta.py b/src/adn_server/application/routing/lc_ta.py index df79dbe..428fe4d 100644 --- a/src/adn_server/application/routing/lc_ta.py +++ b/src/adn_server/application/routing/lc_ta.py @@ -326,6 +326,11 @@ class LcTaMixin: """Emit DMRA to an HBP target on VHEAD (once per target stream).""" if not self._send_dmra_to_system: return + settings = talker_alias_settings(self._config, source_system) + if not settings["enabled"] or not settings["send_dmra"]: + # Embedded TA in DMRD B–E still runs via _rewrite_embed_lc; this gate is + # standalone DMRA UDP only (legacy parity default: off). + return tgt_mode = self._config.get("SYSTEMS", {}).get(target_system, {}).get("MODE") if tgt_mode not in ("MASTER", "PEER"): return diff --git a/src/adn_server/application/talker_alias_use_cases.py b/src/adn_server/application/talker_alias_use_cases.py index 5e401ab..220dc26 100644 --- a/src/adn_server/application/talker_alias_use_cases.py +++ b/src/adn_server/application/talker_alias_use_cases.py @@ -65,11 +65,16 @@ def talker_alias_settings(config: dict[str, Any], system_name: str | None = None tfmt = sys_cfg.get("TALKER_ALIAS_TEXT_FORMAT") if tfmt is None: tfmt = global_cfg.get("TALKER_ALIAS_TEXT_FORMAT", "utf8") + send_dmra = sys_cfg.get("TALKER_ALIAS_SEND_DMRA") + if send_dmra is None: + send_dmra = global_cfg.get("TALKER_ALIAS_SEND_DMRA", False) return { "enabled": bool(enabled), "mode": mode, "format": str(fmt), "text_formats": parse_ta_text_formats(tfmt), + # Standalone HBP DMRA UDP (legacy wire often omits this; radios use embedded LC). + "send_dmra": bool(send_dmra), } diff --git a/src/adn_server/infrastructure/config_normalizer.py b/src/adn_server/infrastructure/config_normalizer.py index d81ade8..ab145d8 100644 --- a/src/adn_server/infrastructure/config_normalizer.py +++ b/src/adn_server/infrastructure/config_normalizer.py @@ -94,6 +94,8 @@ def apply_talker_alias_defaults(config: dict) -> None: g.setdefault("TALKER_ALIAS_MODE", "both") g.setdefault("TALKER_ALIAS_FORMAT", "{callsign} {fname}") g.setdefault("TALKER_ALIAS_TEXT_FORMAT", "utf8") + # Standalone DMRA UDP off by default (radios use embedded LC; matches legacy wire). + g.setdefault("TALKER_ALIAS_SEND_DMRA", False) def normalize_peer_config(config: dict) -> None: diff --git a/src/adn_server/infrastructure/config_validator.py b/src/adn_server/infrastructure/config_validator.py index f10b021..d87bc69 100644 --- a/src/adn_server/infrastructure/config_validator.py +++ b/src/adn_server/infrastructure/config_validator.py @@ -145,6 +145,7 @@ def _validate_global(global_cfg: dict[str, Any], errors: list[str]) -> None: "DEBUG_BRIDGES", "ENABLE_API", "TALKER_ALIAS", + "TALKER_ALIAS_SEND_DMRA", ): if key in global_cfg: _expect_bool(f"GLOBAL.{key}", global_cfg[key], errors) diff --git a/tests/harness/scenarios.py b/tests/harness/scenarios.py index b3214e8..a5fd774 100644 --- a/tests/harness/scenarios.py +++ b/tests/harness/scenarios.py @@ -49,6 +49,8 @@ def talker_alias_config() -> dict[str, Any]: config["GLOBAL"]["TALKER_ALIAS"] = True config["GLOBAL"]["TALKER_ALIAS_MODE"] = "inject" config["GLOBAL"]["TALKER_ALIAS_FORMAT"] = "{callsign} {fname}" + # Tests that assert dmra_capture need explicit opt-in (production default is false). + config["GLOBAL"]["TALKER_ALIAS_SEND_DMRA"] = True rid = 3120001 config["_SUB_PROFILES"] = { rid: {"callsign": "CE5RPY", "fname": "Rodrigo", "surname": "Perez"}, diff --git a/tests/talker_alias/test_routing_inject.py b/tests/talker_alias/test_routing_inject.py index b410db7..3ea3c98 100644 --- a/tests/talker_alias/test_routing_inject.py +++ b/tests/talker_alias/test_routing_inject.py @@ -50,6 +50,21 @@ def test_talker_alias_inject_dmra_on_bridge_vhead() -> None: assert b"CE5RPY" in payload +def test_talker_alias_send_dmra_false_skips_standalone_dmra() -> None: + """TALKER_ALIAS_SEND_DMRA false: no DMRA UDP; embedded TA path still armed.""" + config = talker_alias_config() + config["GLOBAL"]["TALKER_ALIAS_SEND_DMRA"] = False + bridges = active_routing_table(91, (("MASTER-A", 2), ("MASTER-B", 2))) + scenario = DeterministicScenario(config=config, routing_table=bridges) + base = PacketSpec(dst_id=91, rf_src=3120001, stream_id=0x90909091) + + scenario.inject_hbp("MASTER-A", DeterministicScenario.voice_head_spec(base)) + + assert scenario.dmra_capture == [] + ts_st = scenario.protocols["MASTER-B"].STATUS[2] + assert ts_st.get("TX_TA_EMB") is not None + + def test_talker_alias_vhead_sent_once_per_target_stream() -> None: bridges = active_routing_table(91, (("MASTER-A", 2), ("MASTER-B", 2))) scenario = DeterministicScenario(config=talker_alias_config(), routing_table=bridges)