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.
428 lines
18 KiB
428 lines
18 KiB
# ADN DMR Peer Server - tests routing unit data routing
|
|
#
|
|
# 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
|
|
###############################################################################
|
|
|
|
"""Unit data routing: SUB_MAP, hotspot, gateway, OBP fanout."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from tests.harness.assertions import assert_forwarded, assert_not_forwarded
|
|
from tests.harness.deterministic import (
|
|
DeterministicScenario,
|
|
PacketSpec,
|
|
add_openbridge_system,
|
|
minimal_config,
|
|
parse_dmr_fields,
|
|
patch_routing_wall_time,
|
|
FakeClock,
|
|
)
|
|
from tests.routing.unit_data_helpers import idle_hbp_slot
|
|
|
|
from adn_server.domain import bytes_3, bytes_4
|
|
from adn_server.domain.hbp_protocol import HBPF_SLT_VHEAD, HBPF_SLT_VTERM
|
|
|
|
DAPRS_GATEWAY_ID = 900999
|
|
HOTSPOT_SUB_ID = 7300392
|
|
_HARNESS_T0 = FakeClock().time()
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_hbp_forward_wire_format_matches_legacy_send_data_to_hbp() -> None:
|
|
"""Server is a transparent bridge: only TS bit may change; DMR payload [20:53] is untouched."""
|
|
config = minimal_config(("SYSTEM", "D-APRS"))
|
|
config["SYSTEMS"]["D-APRS"]["PEERS"] = {
|
|
DAPRS_GATEWAY_ID: {"CALLSIGN": "D-APRS", "IP": "127.0.0.1", "PORT": 52555},
|
|
}
|
|
config["SYSTEMS"]["D-APRS"]["GROUP_HANGTIME"] = 0
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["D-APRS"].STATUS[2] = idle_hbp_slot()
|
|
payload = bytes(range(33)) # distinct pattern for dmrpkt [20:53]
|
|
base = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=HOTSPOT_SUB_ID,
|
|
dst_id=DAPRS_GATEWAY_ID,
|
|
stream_id=0xDEADBEEF,
|
|
slot=1,
|
|
dtype_vseq=6,
|
|
payload=payload,
|
|
)
|
|
spec = DeterministicScenario.unit_data_header_spec(base)
|
|
ingress = spec.data()
|
|
scenario.inject_unit("SYSTEM", spec)
|
|
forwarded = scenario.capture.for_system("D-APRS")
|
|
assert len(forwarded) == 1
|
|
out = forwarded[0].packet
|
|
assert out[:15] == ingress[:15]
|
|
assert out[16:20] == ingress[16:20]
|
|
assert out[20:53] == ingress[20:53]
|
|
assert out[8:11] == ingress[8:11]
|
|
assert out[5:8] == ingress[5:8]
|
|
assert (out[15] ^ ingress[15]) == 0x80
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_sub_map_forwards_to_idle_hbp() -> None:
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
# 6-digit dst: only unit_data SUB_MAP path (pvt_call runs for 7-digit only, legacy ~3253).
|
|
dst_sub = 712345
|
|
config["_SUB_MAP"] = {bytes_3(dst_sub): ("MASTER-B", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["MASTER-B"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(call_type="unit", dst_id=dst_sub, stream_id=0x52525252, slot=2)
|
|
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert_forwarded(scenario, "MASTER-B", count=1, call_type="unit")
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_7digit_sub_map_idle_forwards_via_both_legacy_paths() -> None:
|
|
"""Legacy parity: dtype 6/7/8 to 7-digit dst runs unit_data + pvt_call_received."""
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
dst_sub = 7123456
|
|
config["_SUB_MAP"] = {bytes_3(dst_sub): ("MASTER-B", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["MASTER-B"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(call_type="unit", dst_id=dst_sub, stream_id=0x52525252, slot=2)
|
|
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert_forwarded(scenario, "MASTER-B", count=2, call_type="unit")
|
|
|
|
|
|
def test_unit_data_sub_map_skips_busy_hbp_target() -> None:
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
# 6-digit dst isolates SUB_MAP busy check (no pvt_call fallback).
|
|
dst_sub = 712345
|
|
config["_SUB_MAP"] = {bytes_3(dst_sub): ("MASTER-B", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["MASTER-B"].STATUS[2] = {
|
|
"RX_TYPE": HBPF_SLT_VHEAD,
|
|
"TX_TYPE": HBPF_SLT_VTERM,
|
|
"TX_TIME": scenario.clock.time(),
|
|
}
|
|
base = PacketSpec(call_type="unit", dst_id=dst_sub, stream_id=0x53535353, slot=2)
|
|
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert_not_forwarded(scenario, "MASTER-B")
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_hotspot_peer_match_forwards_to_idle_hbp() -> None:
|
|
"""Regression: hotspot peer ID match forwards unit data to idle MASTER slot."""
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
config["SYSTEMS"]["MASTER-B"]["PEERS"] = {
|
|
1234567890: {"CALLSIGN": "HS1", "IP": "127.0.0.1", "PORT": 62040},
|
|
}
|
|
config["SYSTEMS"]["MASTER-B"]["GROUP_HANGTIME"] = 0
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["MASTER-B"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(call_type="unit", dst_id=123456, stream_id=0x54545454, slot=2)
|
|
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert_forwarded(scenario, "MASTER-B", count=1, call_type="unit", dst_id=123456)
|
|
|
|
|
|
def test_unit_data_forwards_to_data_gateway_when_enabled() -> None:
|
|
config = minimal_config(("MASTER-A",))
|
|
config["GLOBAL"]["DATA_GATEWAY"] = True
|
|
add_openbridge_system(config, "DATA-GATEWAY")
|
|
config["SYSTEMS"]["DATA-GATEWAY"]["ENABLED"] = True
|
|
scenario = DeterministicScenario(config=config)
|
|
base = PacketSpec(call_type="unit", dst_id=5005, stream_id=0x65656565, slot=2)
|
|
|
|
with patch_routing_wall_time(scenario.clock):
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert len(scenario.capture.for_system("DATA-GATEWAY")) == 1
|
|
|
|
|
|
def test_unit_data_fanout_to_other_obp_with_ver_gt_1() -> None:
|
|
config = minimal_config(("MASTER-A",))
|
|
add_openbridge_system(config, "OBP-FAN")
|
|
scenario = DeterministicScenario(config=config)
|
|
base = PacketSpec(call_type="unit", dst_id=1000001, stream_id=0x66666666, slot=2)
|
|
|
|
with patch_routing_wall_time(scenario.clock):
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert len(scenario.capture.for_system("OBP-FAN")) == 1
|
|
assert parse_dmr_fields(scenario.capture.for_system("OBP-FAN")[0].packet)["call_type"] == "unit"
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_to_fresh_local_sub_is_not_fanned_out_to_obp() -> None:
|
|
"""Unit data to a subscriber recently seen on a local system goes only via SUB_MAP."""
|
|
config = minimal_config(("D-APRS", "SYSTEM"))
|
|
add_openbridge_system(config, "OBP-FAN")
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, _HARNESS_T0 - 60)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["SYSTEM"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(call_type="unit", rf_src=DAPRS_GATEWAY_ID, dst_id=HOTSPOT_SUB_ID, stream_id=0x67676767, slot=2)
|
|
|
|
with patch_routing_wall_time(scenario.clock):
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert len(scenario.capture.for_system("OBP-FAN")) == 0
|
|
assert len(scenario.capture.for_system("SYSTEM")) >= 1
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_to_stale_local_sub_is_still_fanned_out_to_obp() -> None:
|
|
config = minimal_config(("D-APRS", "SYSTEM"))
|
|
add_openbridge_system(config, "OBP-FAN")
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, _HARNESS_T0 - 3600)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["SYSTEM"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(call_type="unit", rf_src=DAPRS_GATEWAY_ID, dst_id=HOTSPOT_SUB_ID, stream_id=0x68686868, slot=2)
|
|
|
|
with patch_routing_wall_time(scenario.clock):
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert len(scenario.capture.for_system("OBP-FAN")) == 1
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_to_sub_learned_via_obp_is_still_fanned_out_to_obp() -> None:
|
|
config = minimal_config(("D-APRS",))
|
|
add_openbridge_system(config, "OBP-FAN")
|
|
add_openbridge_system(config, "OBP-OTHER")
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("OBP-OTHER", 1, _HARNESS_T0 - 60)}
|
|
scenario = DeterministicScenario(config=config)
|
|
base = PacketSpec(call_type="unit", rf_src=DAPRS_GATEWAY_ID, dst_id=HOTSPOT_SUB_ID, stream_id=0x69696969, slot=2)
|
|
|
|
with patch_routing_wall_time(scenario.clock):
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert len(scenario.capture.for_system("OBP-FAN")) == 1
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_daprs_peer_id_match_forwards() -> None:
|
|
"""Legacy parity: unit data routes when dst matches a PEER on D-APRS (runtime or static)."""
|
|
for gateway_id in (DAPRS_GATEWAY_ID, 730999):
|
|
config = minimal_config(("SYSTEM", "D-APRS"))
|
|
config["SYSTEMS"]["D-APRS"]["PEERS"] = {
|
|
gateway_id: {"CALLSIGN": "D-APRS", "IP": "127.0.0.1", "PORT": 52555},
|
|
}
|
|
config["SYSTEMS"]["D-APRS"]["GROUP_HANGTIME"] = 0
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["D-APRS"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=HOTSPOT_SUB_ID,
|
|
dst_id=gateway_id,
|
|
stream_id=0xAABBCCDD,
|
|
slot=2,
|
|
)
|
|
scenario.inject_unit("SYSTEM", DeterministicScenario.unit_data_header_spec(base))
|
|
assert_forwarded(scenario, "D-APRS", count=1, call_type="unit", dst_id=gateway_id)
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_daprs_uplink_then_downlink_while_hotspot_transmitting() -> None:
|
|
"""D-APRS: uplink unit data must not mark SYSTEM slot busy; reply via SUB_MAP forwards."""
|
|
config = minimal_config(("SYSTEM", "D-APRS"))
|
|
config["SYSTEMS"]["D-APRS"]["PEERS"] = {
|
|
DAPRS_GATEWAY_ID: {"CALLSIGN": "D-APRS", "IP": "127.0.0.1", "PORT": 52555},
|
|
}
|
|
config["SYSTEMS"]["D-APRS"]["GROUP_HANGTIME"] = 0
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["SYSTEM"].STATUS[2] = idle_hbp_slot()
|
|
scenario.protocols["D-APRS"].STATUS[2] = idle_hbp_slot()
|
|
|
|
uplink = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=HOTSPOT_SUB_ID,
|
|
dst_id=DAPRS_GATEWAY_ID,
|
|
stream_id=0x41381248,
|
|
slot=2,
|
|
)
|
|
scenario.inject_unit("SYSTEM", DeterministicScenario.unit_data_header_spec(uplink))
|
|
assert scenario.protocols["SYSTEM"].STATUS[2]["RX_TYPE"] == HBPF_SLT_VTERM
|
|
assert_forwarded(scenario, "D-APRS", count=1, call_type="unit")
|
|
|
|
scenario.capture.packets.clear()
|
|
downlink = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=DAPRS_GATEWAY_ID,
|
|
dst_id=HOTSPOT_SUB_ID,
|
|
stream_id=0x33416424,
|
|
slot=2,
|
|
)
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(downlink))
|
|
|
|
# Idle SYSTEM slot: legacy sends via sendDataToHBP and pvt_call send_system (7-digit dst).
|
|
assert_forwarded(scenario, "SYSTEM", count=2, call_type="unit", dst_id=HOTSPOT_SUB_ID)
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_7digit_dst_uses_pvt_call_when_sub_map_idle_check_fails() -> None:
|
|
"""Legacy parity: dtype 6/7/8 to 7-digit dst also runs pvt_call_received (routerHBP ~3253)."""
|
|
from adn_server.domain.hbp_protocol import HBPF_SLT_VHEAD
|
|
|
|
config = minimal_config(("D-APRS", "SYSTEM"))
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config)
|
|
# SUB_MAP idle check fails (strict RX/TX VTERM), but pvt_call uses STREAM_TO contention.
|
|
scenario.protocols["SYSTEM"].STATUS[2] = {
|
|
"RX_TYPE": HBPF_SLT_VHEAD,
|
|
"TX_TYPE": HBPF_SLT_VHEAD,
|
|
"RX_TGID": bytes_3(900999),
|
|
"TX_TGID": bytes_3(0),
|
|
"RX_TIME": scenario.clock.time(),
|
|
"TX_TIME": 0.0,
|
|
"RX_STREAM_ID": bytes_3(0),
|
|
}
|
|
base = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=900999,
|
|
dst_id=HOTSPOT_SUB_ID,
|
|
stream_id=0x86673819,
|
|
slot=2,
|
|
)
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert_forwarded(scenario, "SYSTEM", count=1, call_type="unit", dst_id=HOTSPOT_SUB_ID)
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_7digit_multi_frame_pierces_rx_tgid_match() -> None:
|
|
"""Unit data downlink must forward all frames when target RX_TGID equals private dst."""
|
|
from adn_server.domain.hbp_protocol import HBPF_DATA_SYNC, HBPF_SLT_VHEAD
|
|
|
|
config = minimal_config(("D-APRS", "SYSTEM"))
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["SYSTEM"].STATUS[2] = {
|
|
"RX_TYPE": HBPF_SLT_VHEAD,
|
|
"TX_TYPE": HBPF_SLT_VHEAD,
|
|
"RX_TGID": bytes_3(HOTSPOT_SUB_ID),
|
|
"TX_TGID": bytes_3(0),
|
|
"RX_TIME": scenario.clock.time(),
|
|
"TX_TIME": 0.0,
|
|
"RX_STREAM_ID": bytes_3(0),
|
|
}
|
|
stream = 0xAABBCCDD
|
|
for i, dtype in enumerate([3, 6, 8, 8, 8]):
|
|
spec = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=900999,
|
|
dst_id=HOTSPOT_SUB_ID,
|
|
stream_id=stream,
|
|
slot=2,
|
|
dtype_vseq=dtype,
|
|
frame_type=HBPF_DATA_SYNC,
|
|
payload=bytes([dtype]) * 33,
|
|
seq=i,
|
|
)
|
|
scenario.inject_unit("D-APRS", spec)
|
|
assert_forwarded(scenario, "SYSTEM", count=5, call_type="unit", dst_id=HOTSPOT_SUB_ID)
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_pvt_call_does_not_emit_private_voice_monitor_events() -> None:
|
|
"""Unit data downlink must not light monitor TS chips (no PRIVATE VOICE START/END)."""
|
|
config = minimal_config(("D-APRS", "SYSTEM"))
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, 1000.0)}
|
|
scenario = DeterministicScenario(config=config, enable_reporting=True)
|
|
scenario.protocols["SYSTEM"].STATUS[2] = idle_hbp_slot()
|
|
base = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=900999,
|
|
dst_id=HOTSPOT_SUB_ID,
|
|
stream_id=0x11223344,
|
|
slot=2,
|
|
)
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert_forwarded(scenario, "SYSTEM", count=2, call_type="unit", dst_id=HOTSPOT_SUB_ID)
|
|
assert scenario.report_factory is not None
|
|
private_voice = [e for e in scenario.report_factory.events if e.startswith("PRIVATE VOICE")]
|
|
assert private_voice == []
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_sub_map_with_peer_id_targets_only_that_peer() -> None:
|
|
"""Regression: cross-talk -- unit DATA to a known subscriber must reach only the
|
|
one hotspot it was last heard on, not every peer of the destination system
|
|
(send_to_system/send_peers broadcasts to all peers when no peer is targeted)."""
|
|
dst_sub = 712345
|
|
dst_peer = bytes_4(730039101)
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
config["_SUB_MAP"] = {bytes_3(dst_sub): ("MASTER-B", 2, 1000.0, dst_peer)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["MASTER-B"].STATUS[2] = idle_hbp_slot()
|
|
scenario.protocols["MASTER-B"]._peers[dst_peer] = {}
|
|
base = PacketSpec(call_type="unit", dst_id=dst_sub, stream_id=0x52525253, slot=2)
|
|
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert scenario.protocols["MASTER-B"].sent_to_peer, "must deliver directly to the known peer"
|
|
assert scenario.protocols["MASTER-B"].sent_to_peer[0][0] == dst_peer
|
|
assert_not_forwarded(scenario, "MASTER-B")
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_unit_data_sub_map_peer_not_connected_falls_back_to_broadcast() -> None:
|
|
"""When the known peer isn't (or is no longer) connected, fall back to the old
|
|
broadcast-to-system behavior rather than silently dropping the packet."""
|
|
dst_sub = 712345
|
|
dst_peer = bytes_4(730039101)
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
config["_SUB_MAP"] = {bytes_3(dst_sub): ("MASTER-B", 2, 1000.0, dst_peer)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["MASTER-B"].STATUS[2] = idle_hbp_slot()
|
|
# dst_peer intentionally NOT registered in scenario.protocols["MASTER-B"]._peers.
|
|
base = PacketSpec(call_type="unit", dst_id=dst_sub, stream_id=0x52525254, slot=2)
|
|
|
|
scenario.inject_unit("MASTER-A", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert not scenario.protocols["MASTER-B"].sent_to_peer
|
|
assert_forwarded(scenario, "MASTER-B", count=1, call_type="unit")
|
|
|
|
|
|
@pytest.mark.behavior
|
|
def test_pvt_call_sub_map_with_peer_id_targets_only_that_peer() -> None:
|
|
"""Same cross-talk fix for the 7-digit pvt_call_received cross-system path (private
|
|
voice and ARS/LRRP downlink use this, independent of the unit-data SUB_MAP branch)."""
|
|
dst_peer = bytes_4(730039101)
|
|
config = minimal_config(("D-APRS", "SYSTEM"))
|
|
config["_SUB_MAP"] = {bytes_3(HOTSPOT_SUB_ID): ("SYSTEM", 2, 1000.0, dst_peer)}
|
|
scenario = DeterministicScenario(config=config)
|
|
scenario.protocols["SYSTEM"].STATUS[2] = idle_hbp_slot()
|
|
scenario.protocols["SYSTEM"]._peers[dst_peer] = {}
|
|
base = PacketSpec(
|
|
call_type="unit",
|
|
rf_src=DAPRS_GATEWAY_ID,
|
|
dst_id=HOTSPOT_SUB_ID,
|
|
stream_id=0x33416425,
|
|
slot=2,
|
|
)
|
|
|
|
scenario.inject_unit("D-APRS", DeterministicScenario.unit_data_header_spec(base))
|
|
|
|
assert scenario.protocols["SYSTEM"].sent_to_peer, "must deliver directly to the known peer"
|
|
assert scenario.protocols["SYSTEM"].sent_to_peer[0][0] == dst_peer
|
|
assert_not_forwarded(scenario, "SYSTEM")
|