diff --git a/src/adn_server/application/bridge_use_cases.py b/src/adn_server/application/bridge_use_cases.py index c68410b..80a4905 100644 --- a/src/adn_server/application/bridge_use_cases.py +++ b/src/adn_server/application/bridge_use_cases.py @@ -39,8 +39,7 @@ from dmr_utils3 import bptc from dmr_utils3 import decode from dmr_utils3.const import LC_OPT -from ..domain import int_id, bytes_3, bytes_4 -from ..infrastructure.hbp_constants import HBPF_DATA_SYNC, HBPF_SLT_VHEAD, HBPF_SLT_VTERM, STREAM_TO +from ..domain import int_id, bytes_3, bytes_4, HBPF_DATA_SYNC, HBPF_SLT_VHEAD, HBPF_SLT_VTERM, STREAM_TO from .ports import BridgeRouter logger = logging.getLogger(__name__) diff --git a/src/adn_server/application/ident_use_cases.py b/src/adn_server/application/ident_use_cases.py index 645439b..e80c75b 100644 --- a/src/adn_server/application/ident_use_cases.py +++ b/src/adn_server/application/ident_use_cases.py @@ -30,8 +30,7 @@ import re import time from typing import Any, Callable -from ..domain import bytes_3, int_id -from ..infrastructure.hbp_constants import HBPF_SLT_VTERM +from ..domain import bytes_3, int_id, HBPF_SLT_VTERM logger = logging.getLogger(__name__) diff --git a/src/adn_server/application/playback_use_cases.py b/src/adn_server/application/playback_use_cases.py index 1a1b8ab..7babd68 100644 --- a/src/adn_server/application/playback_use_cases.py +++ b/src/adn_server/application/playback_use_cases.py @@ -30,8 +30,7 @@ from random import randint from time import sleep, time from typing import Any -from ..domain import int_id, bytes_4 -from ..infrastructure.hbp_constants import HBPF_DATA_SYNC, HBPF_SLT_VTERM +from ..domain import int_id, bytes_4, HBPF_DATA_SYNC, HBPF_SLT_VTERM logger = logging.getLogger(__name__) diff --git a/src/adn_server/application/ports.py b/src/adn_server/application/ports.py index b583f66..dc03e69 100644 --- a/src/adn_server/application/ports.py +++ b/src/adn_server/application/ports.py @@ -139,7 +139,7 @@ class VoiceProvider(ABC): ... @abstractmethod - def ensure_tts_ambe(self, text: str, lang: str, out_path: str, config: dict[str, Any]) -> str | None: + def ensure_tts_ambe(self, config: dict[str, Any], item: dict[str, Any], audio_path: str) -> str | None: """TTS to AMBE file; return path or None. Legacy tts_engine.ensure_tts_ambe.""" ... diff --git a/src/adn_server/application/voice_use_cases.py b/src/adn_server/application/voice_use_cases.py index 8204db2..435b478 100644 --- a/src/adn_server/application/voice_use_cases.py +++ b/src/adn_server/application/voice_use_cases.py @@ -32,9 +32,7 @@ import time from datetime import datetime from typing import Any, Callable -from ..domain import bytes_3 -from ..infrastructure.hbp_constants import HBPF_SLT_VHEAD, HBPF_SLT_VTERM -from ..infrastructure.voice.tts_engine import ensure_tts_ambe as tts_ensure_tts_ambe +from ..domain import bytes_3, HBPF_SLT_VHEAD, HBPF_SLT_VTERM from .ports import VoiceProvider logger = logging.getLogger(__name__) @@ -448,12 +446,12 @@ class VoiceUseCases: self._tts_running[tts_idx] = True logger.info("(%s) Starting TTS conversion in background thread for %s", label, _file) if self._defer_to_thread: - d = self._defer_to_thread(tts_ensure_tts_ambe, self._config, item, self._audio_path) + d = self._defer_to_thread(self._voice.ensure_tts_ambe, self._config, item, self._audio_path) d.addCallback(self._tts_conversion_done, tts_idx, _file, _tg, _lang, mode, label) d.addErrback(self._tts_conversion_error, tts_idx, label) else: try: - ambe_path = tts_ensure_tts_ambe(self._config, item, self._audio_path) + ambe_path = self._voice.ensure_tts_ambe(self._config, item, self._audio_path) self._tts_conversion_done(ambe_path, tts_idx, _file, _tg, _lang, mode, label) except Exception as e: self._tts_conversion_error(e, tts_idx, label) diff --git a/src/adn_server/domain/__init__.py b/src/adn_server/domain/__init__.py index 1c2a6f1..3ef119d 100644 --- a/src/adn_server/domain/__init__.py +++ b/src/adn_server/domain/__init__.py @@ -25,6 +25,11 @@ from .entities import BridgeEntry, StreamState, SystemConfig from .value_objects import DmrId, TgId, Slot, CallType, bytes_3, bytes_4, int_id, ID_MIN, ID_MAX, PEER_MAX from .errors import DomainError, ConfigError, ACLError from .result import Result, Success, Fail, is_fail, is_ok, unwrap_or +from .hbp_protocol import ( + HBPF_VOICE, HBPF_VOICE_SYNC, HBPF_DATA_SYNC, + HBPF_SLT_VHEAD, HBPF_SLT_VTERM, + VER, PROTO_VER, STREAM_TO, +) __all__ = [ "BridgeEntry", @@ -49,4 +54,12 @@ __all__ = [ "is_fail", "is_ok", "unwrap_or", + "HBPF_VOICE", + "HBPF_VOICE_SYNC", + "HBPF_DATA_SYNC", + "HBPF_SLT_VHEAD", + "HBPF_SLT_VTERM", + "VER", + "PROTO_VER", + "STREAM_TO", ] diff --git a/src/adn_server/domain/hbp_protocol.py b/src/adn_server/domain/hbp_protocol.py new file mode 100644 index 0000000..f546d2d --- /dev/null +++ b/src/adn_server/domain/hbp_protocol.py @@ -0,0 +1,43 @@ +# ADN DMR Peer Server - HBP protocol constants (domain) +# Copyright (C) 2026 Rodrigo Pérez, CE5RPY +# +# Derived from ADN DMR Server / FreeDMR / HBlink. Original license: +############################################################################### +# Copyright (C) 2020 Simon Adlem, G7RZU +# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################### + +"""HBP frame types and protocol-level constants used across layers. + +These are pure protocol vocabulary (no Twisted, no I/O dependencies) and +belong in the domain layer so application use cases can reference them +without importing infrastructure. +""" + +# Frame types (bits) +HBPF_VOICE = 0x0 +HBPF_VOICE_SYNC = 0x1 +HBPF_DATA_SYNC = 0x2 +HBPF_SLT_VHEAD = 0x1 +HBPF_SLT_VTERM = 0x2 + +# Protocol version +VER = 5 +PROTO_VER = 5 + +# Stream timeout (seconds) for contention (legacy const.py) +STREAM_TO = 0.36 diff --git a/src/adn_server/infrastructure/hbp_constants.py b/src/adn_server/infrastructure/hbp_constants.py index 98fd47c..7767c3e 100644 --- a/src/adn_server/infrastructure/hbp_constants.py +++ b/src/adn_server/infrastructure/hbp_constants.py @@ -21,7 +21,23 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ############################################################################### -"""Homebrew protocol opcodes and frame types (legacy const.py).""" +"""Homebrew protocol opcodes and frame types (legacy const.py). + +Frame types and protocol-level constants live in domain.hbp_protocol (the +canonical source); they are re-exported here so existing infrastructure +imports keep working. +""" + +from ..domain.hbp_protocol import ( # noqa: F401 — re-export + HBPF_VOICE, + HBPF_VOICE_SYNC, + HBPF_DATA_SYNC, + HBPF_SLT_VHEAD, + HBPF_SLT_VTERM, + VER, + PROTO_VER, + STREAM_TO, +) # DMR DMR = b"DMR" @@ -58,16 +74,3 @@ BCVE = b"BCVE" # Proxy PRIN = b"PRIN" PRBL = b"PRBL" - -# Frame types (bits) -HBPF_VOICE = 0x0 -HBPF_VOICE_SYNC = 0x1 -HBPF_DATA_SYNC = 0x2 -HBPF_SLT_VHEAD = 0x1 -HBPF_SLT_VTERM = 0x2 - -VER = 5 -PROTO_VER = 5 - -# Legacy const.py: stream timeout (seconds) for contention -STREAM_TO = 0.36 diff --git a/src/adn_server/infrastructure/voice/ambe_reader.py b/src/adn_server/infrastructure/voice/ambe_reader.py index b0e1ac5..1e2ff77 100644 --- a/src/adn_server/infrastructure/voice/ambe_reader.py +++ b/src/adn_server/infrastructure/voice/ambe_reader.py @@ -36,6 +36,7 @@ from bitarray import bitarray from ...application.ports import VoiceProvider from .pkt_gen import pkt_gen as _pkt_gen +from .tts_engine import ensure_tts_ambe as _tts_ensure_tts_ambe from .voice_map import VOICE_MAP logger = logging.getLogger(__name__) @@ -178,11 +179,9 @@ class DefaultVoiceProvider(VoiceProvider): """Generate HBP voice packets for phrase. Legacy mk_voice.pkt_gen.""" return _pkt_gen(rf_src, dst_id, peer, slot, phrase) - def ensure_tts_ambe(self, text: str, lang: str, out_path: str, config: dict[str, Any]) -> str | None: - """Return out_path if .ambe file exists (cached). Full TTS conversion is in tts_engine.ensure_tts_ambe.""" - if out_path and os.path.isfile(out_path): - return out_path - return None + def ensure_tts_ambe(self, config: dict[str, Any], item: dict[str, Any], audio_path: str) -> str | None: + """Delegate to tts_engine.ensure_tts_ambe (full TTS pipeline).""" + return _tts_ensure_tts_ambe(config, item, audio_path) class StubVoiceProvider(VoiceProvider): @@ -196,7 +195,7 @@ class StubVoiceProvider(VoiceProvider): ) -> Iterator[bytes]: return iter([]) - def ensure_tts_ambe(self, text: str, lang: str, out_path: str, config: dict[str, Any]) -> str | None: + def ensure_tts_ambe(self, config: dict[str, Any], item: dict[str, Any], audio_path: str) -> str | None: return None def read_single_file(self, audio_path: str, lang: str, file_number: str) -> list: