You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
6.1 KiB
171 lines
6.1 KiB
# ADN DMR Peer Server - tests support hbp repeat stack
|
|
#
|
|
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
|
#
|
|
###############################################################################
|
|
# 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 MASTER + RoutingUseCases stack for REPEAT / talker-alias integration tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from adn_server.application.routing_use_cases import RoutingUseCases
|
|
from adn_server.application.reporting_use_cases import ReportingUseCases
|
|
from adn_server.domain.dmr.bptc import encode_emblc
|
|
from adn_server.infrastructure.acl_router import InMemoryAclRouter
|
|
from adn_server.infrastructure.subscription_store import InMemorySubscriptionStore
|
|
from adn_server.infrastructure.config_normalizer import (
|
|
apply_talker_alias_defaults,
|
|
ensure_system_runtime_config,
|
|
)
|
|
from adn_server.infrastructure.talker_alias_emblc import default_ta_emblc_encoder
|
|
from adn_server.infrastructure.twisted_adapters.udp_hbp import HBPProtocol
|
|
|
|
from tests.harness.deterministic import FakeReportFactory, FakeReportSender, PacketSpec
|
|
from tests.harness.scenarios import talker_alias_config
|
|
|
|
|
|
class RecordingTransport:
|
|
"""Capture MASTER downlink UDP writes (REPEAT and DMRA)."""
|
|
|
|
def __init__(self) -> None:
|
|
self.sent: list[tuple[bytes, tuple[str, int]]] = []
|
|
|
|
def write(self, data: bytes, addr: tuple[str, int]) -> None:
|
|
self.sent.append((data, addr))
|
|
|
|
def for_addr(self, addr: tuple[str, int]) -> list[bytes]:
|
|
return [pkt for pkt, target in self.sent if target == addr]
|
|
|
|
def clear(self) -> None:
|
|
self.sent.clear()
|
|
|
|
|
|
class _AclRouter:
|
|
def acl_check(self, _value: bytes, _acl: object) -> bool:
|
|
return True
|
|
|
|
|
|
@dataclass
|
|
class HbpRepeatStack:
|
|
system_name: str
|
|
config: dict[str, Any]
|
|
hbp: HBPProtocol
|
|
bridge: RoutingUseCases
|
|
transport: RecordingTransport
|
|
dmra_capture: list[tuple[list[bytes], bytes | None]] = field(default_factory=list)
|
|
|
|
def register_peer(
|
|
self,
|
|
peer_id: bytes,
|
|
sockaddr: tuple[str, int],
|
|
*,
|
|
options: str | bytes | None = None,
|
|
) -> None:
|
|
peer: dict[str, Any] = {
|
|
"CONNECTION": "YES",
|
|
"CONNECTED": 1_700_000_000.0,
|
|
"LAST_PING": 1_700_000_000.0,
|
|
"SOCKADDR": sockaddr,
|
|
"CALLSIGN": b"CE5RPY ",
|
|
"RADIO_ID": str(int.from_bytes(peer_id, "big")),
|
|
}
|
|
if options is not None:
|
|
peer["OPTIONS"] = options.encode("utf-8") if isinstance(options, str) else options
|
|
self.hbp._peers[peer_id] = peer
|
|
self.config["SYSTEMS"][self.system_name].setdefault("PEERS", {})[peer_id] = (
|
|
self.hbp._peers[peer_id]
|
|
)
|
|
self.hbp._refresh_connected_peer_count()
|
|
self.hbp._mark_downlink_index_dirty()
|
|
|
|
def inject(self, packet: bytes, sockaddr: tuple[str, int]) -> None:
|
|
self.hbp.datagramReceived(packet, sockaddr)
|
|
|
|
def inject_spec(self, spec: PacketSpec, sockaddr: tuple[str, int]) -> None:
|
|
self.inject(spec.data(), sockaddr)
|
|
|
|
|
|
def build_hbp_repeat_stack(
|
|
*,
|
|
talker_alias: bool = True,
|
|
system_name: str = "MASTER-A",
|
|
) -> HbpRepeatStack:
|
|
"""Real HBPProtocol with RoutingUseCases TA callbacks (not FakeHbpProtocol)."""
|
|
config = copy.deepcopy(talker_alias_config())
|
|
if not talker_alias:
|
|
config["GLOBAL"]["TALKER_ALIAS"] = False
|
|
apply_talker_alias_defaults(config)
|
|
ensure_system_runtime_config(config)
|
|
sys_cfg = config["SYSTEMS"][system_name]
|
|
sys_cfg["REPEAT"] = True
|
|
sys_cfg["MAX_PEERS"] = 8
|
|
sys_cfg["USE_ACL"] = False
|
|
|
|
transport = RecordingTransport()
|
|
hbp = HBPProtocol(system_name, config, router=_AclRouter())
|
|
hbp.transport = transport # type: ignore[assignment]
|
|
|
|
protocols: dict[str, Any] = {system_name: hbp}
|
|
dmra_capture: list[tuple[list[bytes], bytes | None]] = []
|
|
|
|
def _send_dmra(
|
|
target_system: str,
|
|
packets: list[bytes],
|
|
exclude_peer: bytes | None = None,
|
|
*,
|
|
slot: int | None = None,
|
|
tgid: int | None = None,
|
|
) -> int:
|
|
proto = protocols[target_system]
|
|
dmra_capture.append((list(packets), exclude_peer))
|
|
return proto.send_dmra_to_peers(
|
|
packets, exclude_peer=exclude_peer, slot=slot, tgid=tgid,
|
|
)
|
|
|
|
def _get_dmra_blocks(_system: str, stream_id: bytes) -> dict[int, bytes] | None:
|
|
return hbp.get_dmra_blocks(stream_id)
|
|
|
|
report_factory = FakeReportFactory()
|
|
bridge = RoutingUseCases(
|
|
InMemoryAclRouter(),
|
|
config,
|
|
InMemorySubscriptionStore(),
|
|
send_to_system=lambda *_a, **_k: None,
|
|
get_protocols=lambda: protocols,
|
|
reporting=ReportingUseCases(FakeReportSender(report_factory), config),
|
|
send_dmra_to_system=_send_dmra,
|
|
get_dmra_blocks=_get_dmra_blocks,
|
|
encode_emblc=encode_emblc,
|
|
ta_emblc_encoder=default_ta_emblc_encoder,
|
|
)
|
|
hbp._on_talker_alias_repeat_prepare = bridge.prepare_talker_alias_local_repeat
|
|
hbp._on_talker_alias_repeat_burst = bridge.rewrite_repeat_voice_burst
|
|
hbp._on_talker_alias_stream_end = bridge.clear_talker_alias_stream
|
|
|
|
return HbpRepeatStack(
|
|
system_name=system_name,
|
|
config=config,
|
|
hbp=hbp,
|
|
bridge=bridge,
|
|
transport=transport,
|
|
dmra_capture=dmra_capture,
|
|
)
|