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.
157 lines
5.6 KiB
157 lines
5.6 KiB
# ADN DMR Peer Server - OBP → HBP cross-slot downlink
|
|
#
|
|
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
|
|
|
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.routing.helpers import inject_only_defer_obp_hbp_slot_contention
|
|
from adn_server.domain import HBPF_SLT_VHEAD, bytes_3, bytes_4
|
|
|
|
|
|
def _two_peer_master(scenario: DeterministicScenario) -> None:
|
|
master = scenario.config["SYSTEMS"]["MASTER-A"]
|
|
master["TS2_STATIC"] = "730444,7144"
|
|
master["PEERS"] = {
|
|
bytes_4(730001): {"CONNECTION": "YES", "OPTIONS": b"TS2=7144;"},
|
|
bytes_4(730002): {"CONNECTION": "YES", "OPTIONS": b"TS1=730444;"},
|
|
}
|
|
|
|
|
|
def test_defer_helper_obp_to_master_defers_global_slot_contention() -> None:
|
|
sys_cfg = {"MODE": "MASTER", "PEERS": {bytes_4(1): {"CONNECTION": "YES"}}}
|
|
assert inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "MASTER-A", sys_cfg, source_is_obp=True,
|
|
)
|
|
assert inject_only_defer_obp_hbp_slot_contention(
|
|
{"PROXY": {"TARGET_SYSTEM": "MASTER-A"}}, "MASTER-A", sys_cfg, source_is_obp=True,
|
|
)
|
|
assert not inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "MASTER-A", sys_cfg, source_is_obp=False,
|
|
)
|
|
assert not inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "OPENBRIDGE", {"MODE": "OPENBRIDGE"}, source_is_obp=True,
|
|
)
|
|
|
|
|
|
def test_defer_helper_hbp_multi_peer_defers_global_slot_contention() -> None:
|
|
sys_cfg = {
|
|
"MODE": "MASTER",
|
|
"PEERS": {
|
|
bytes_4(1): {"CONNECTION": "YES"},
|
|
bytes_4(2): {"CONNECTION": "YES"},
|
|
},
|
|
}
|
|
assert inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "MASTER-A", sys_cfg, source_is_obp=False, source_is_hbp=True, connected_count=3,
|
|
)
|
|
assert not inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "MASTER-A", sys_cfg, source_is_obp=False, source_is_hbp=True, connected_count=1,
|
|
)
|
|
|
|
|
|
def test_defer_helper_single_peer_master_still_defers_to_send_peer() -> None:
|
|
sys_cfg = {
|
|
"MODE": "MASTER",
|
|
"PEERS": {
|
|
bytes_4(1): {"CONNECTION": "YES"},
|
|
bytes_4(2): {"CONNECTION": "YES"},
|
|
},
|
|
}
|
|
assert inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "MASTER-A", sys_cfg, source_is_obp=True, connected_count=2,
|
|
)
|
|
assert inject_only_defer_obp_hbp_slot_contention(
|
|
{}, "MASTER-A", sys_cfg, source_is_obp=True, connected_count=1,
|
|
)
|
|
|
|
|
|
def _seed_busy_slot_2(scenario: DeterministicScenario, peer_id: int) -> None:
|
|
proto = scenario.protocols["MASTER-A"]
|
|
t = scenario.clock.time()
|
|
proto.STATUS[2] = {
|
|
"RX_STREAM_ID": bytes_4(0xAAAAAAAA),
|
|
"RX_PEER": bytes_4(peer_id),
|
|
"RX_TGID": bytes_3(7144),
|
|
"RX_TIME": t,
|
|
"RX_TYPE": HBPF_SLT_VHEAD,
|
|
"TX_STREAM_ID": bytes_4(0xAAAAAAAA),
|
|
"TX_TGID": bytes_3(7144),
|
|
"TX_TIME": t,
|
|
"TX_TYPE": HBPF_SLT_VHEAD,
|
|
"TX_PEER": bytes_4(peer_id),
|
|
}
|
|
|
|
|
|
def test_obp_hbp_forwards_when_bridge_ts_busy_but_peer_listens_other_ts() -> None:
|
|
"""OBP bridge leg TS2 must not block downlink to a TS1-static peer (inject-only)."""
|
|
scenario = obp_bridge_scenario("OBP-CL", tg=730444)
|
|
scenario.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"}
|
|
_two_peer_master(scenario)
|
|
_seed_busy_slot_2(scenario, 730001)
|
|
base = PacketSpec(
|
|
peer_id=73010,
|
|
rf_src=3340062,
|
|
dst_id=730444,
|
|
slot=1,
|
|
stream_id=0xBBBBBBBB,
|
|
)
|
|
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
|
|
|
|
|
|
def test_obp_hbp_forwards_dynamic_ua_on_slot_1_when_bridge_ts2_busy() -> None:
|
|
"""Dynamic TG keyed on TS1 must receive OBP downlink even when TS2 is occupied."""
|
|
scenario = obp_bridge_scenario("OBP-CL", tg=730444)
|
|
scenario.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"}
|
|
master = scenario.config["SYSTEMS"]["MASTER-A"]
|
|
master["TS2_STATIC"] = "730444,7144"
|
|
p_busy = bytes_4(730001)
|
|
p_dyn = bytes_4(730002)
|
|
master["PEERS"] = {
|
|
p_busy: {"CONNECTION": "YES", "OPTIONS": b"TS2=7144;"},
|
|
p_dyn: {
|
|
"CONNECTION": "YES",
|
|
"OPTIONS": b"TS2=730;SINGLE=1;TIMER=300;",
|
|
"_UA_SESSION": {
|
|
1: {"tgid": 730444, "expires": scenario.clock.time() + 300.0},
|
|
},
|
|
},
|
|
}
|
|
_seed_busy_slot_2(scenario, 730001)
|
|
base = PacketSpec(
|
|
peer_id=73010,
|
|
rf_src=3340062,
|
|
dst_id=730444,
|
|
slot=1,
|
|
stream_id=0xDDDDDDDD,
|
|
)
|
|
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
|
|
|
|
|
|
def test_obp_hbp_single_peer_inject_only_defers_like_repeat() -> None:
|
|
"""Sole inject-only hotspot: OBP uses per-peer slot checks (REPEAT parity)."""
|
|
scenario = obp_bridge_scenario("OBP-CL", tg=730444)
|
|
scenario.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"}
|
|
master = scenario.config["SYSTEMS"]["MASTER-A"]
|
|
master["TS2_STATIC"] = "730444,7144"
|
|
master["PEERS"] = {
|
|
bytes_4(730002): {"CONNECTION": "YES", "OPTIONS": b"TS1=730444;"},
|
|
}
|
|
_seed_busy_slot_2(scenario, 730002)
|
|
base = PacketSpec(
|
|
peer_id=73010,
|
|
rf_src=3340062,
|
|
dst_id=730444,
|
|
slot=1,
|
|
stream_id=0xCCCCCCCC,
|
|
)
|
|
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
|