# ADN DMR Peer Server - OBP vs server announcement slot contention # # Copyright (C) 2026 Rodrigo Pérez, CE5RPY # ############################################################################### # 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 ############################################################################### """OBP ingress blocked while server announcement holds MASTER slot TX row.""" from __future__ import annotations from tests.harness.deterministic import DeterministicScenario, PacketSpec, patch_routing_wall_time from tests.harness.scenarios import obp_bridge_scenario from adn_server.application.server_voice import DEFAULT_SERVER_VOICE_ID from adn_server.domain import HBPF_SLT_VHEAD, HBPF_SLT_VTERM, bytes_3, bytes_4 def _seed_server_broadcast_slot(scenario: DeterministicScenario, *, ann_tg: int = 91) -> None: proto = scenario.protocols["MASTER-A"] t = scenario.clock.time() proto.STATUS[2] = { "RX_TYPE": HBPF_SLT_VTERM, "TX_TYPE": HBPF_SLT_VHEAD, "TX_TIME": t, "TX_RFS": bytes_3(DEFAULT_SERVER_VOICE_ID), "TX_TGID": bytes_3(ann_tg), "TX_STREAM_ID": bytes_4(0xA0A0A0A0), } def test_obp_blocked_while_server_broadcast_holds_master_ts2() -> None: """Legacy parity: OBP must not route onto TS2 while announcement holds TX row.""" scenario = obp_bridge_scenario("OBP-CL", tg=7144) scenario.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"} scenario.config["SYSTEMS"]["MASTER-A"]["PEERS"] = { bytes_4(730002): {"CONNECTION": "YES", "OPTIONS": b"TS2=7144;"}, } _seed_server_broadcast_slot(scenario, ann_tg=91) base = PacketSpec( peer_id=73010, rf_src=3340062, dst_id=7144, slot=1, stream_id=0x11111111, ) with patch_routing_wall_time(scenario.clock): scenario.inject_obp("OBP-CL", DeterministicScenario.voice_head_spec(base)) assert scenario.capture.for_system("MASTER-A") == [] def test_obp_still_forwards_when_no_server_broadcast_hold() -> None: """Inject-only defer remains when the slot is not held by server voice.""" scenario = obp_bridge_scenario("OBP-CL", tg=7144) scenario.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"} scenario.config["SYSTEMS"]["MASTER-A"]["PEERS"] = { bytes_4(730002): {"CONNECTION": "YES", "OPTIONS": b"TS2=7144;"}, } proto = scenario.protocols["MASTER-A"] t = scenario.clock.time() proto.STATUS[2] = { "RX_TYPE": HBPF_SLT_VTERM, "TX_TYPE": HBPF_SLT_VTERM, "TX_TIME": t - 60.0, "TX_RFS": bytes_3(3340062), "TX_TGID": bytes_3(7144), } base = PacketSpec( peer_id=73010, rf_src=3340062, dst_id=7144, slot=1, stream_id=0x22222222, ) with patch_routing_wall_time(scenario.clock): scenario.inject_obp("OBP-CL", DeterministicScenario.voice_head_spec(base)) assert len(scenario.capture.for_system("MASTER-A")) > 0