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.
ADN-DMR-Peer-Server/tests/application/test_peer_single_downlink.py

611 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# ADN DMR Peer Server - tests application peer single downlink
#
# 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
###############################################################################
"""Per-peer SINGLE=1 downlink exclusivity (inject-only multi-hotspot)."""
from __future__ import annotations
from adn_server.application.routing.helpers import (
clear_peer_rx_status_slots,
clear_peer_ua_sessions,
peer_options_static_tg_slot,
peer_receives_group_tgid,
peer_should_receive_group_voice,
peer_single_blocks_group_voice,
peer_single_exclusive_tgid,
register_peer_ua_multi_tg,
register_peer_ua_session,
seed_peer_ua_session_from_status,
tg4000_reset_on_vhead,
)
from adn_server.domain import HBPF_DATA_SYNC, HBPF_SLT_VHEAD, HBPF_SLT_VTERM, bytes_3, bytes_4, int_id
def _sys_cfg() -> dict:
return {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 10}
def _peer_id() -> bytes:
return bytes_4(730039101)
def test_static_tg_on_opposite_slot_receives_group_voice() -> None:
"""Hotspot lists TG on TS1 but network voice arrives on TS2 (legacy REPEAT parity)."""
peer = {"OPTIONS": b"TS1=730170;"}
assert peer_receives_group_tgid(peer, 2, 730170)
assert peer_options_static_tg_slot(peer, 730170) == 1
assert peer_should_receive_group_voice(peer, 2, 730170, connected_count=8)
def test_single_blocks_other_static_tg_while_session_on_static() -> None:
"""Indigo on 7305 blocks RX on 730 even when both are in OPTIONS."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
def test_single_session_survives_on_sys_cfg_store() -> None:
"""Session lives on ``sys_cfg['_PEER_UA_SESSIONS']``, not only the peer dict."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
peer.pop("_UA_SESSION", None)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
def test_single_uses_peer_options_timer_not_yaml_default() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 60}
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
still_locked = now + 5 * 60 - 1
expired = now + 5 * 60 + 1
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=still_locked
)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=expired
)
def test_single_allows_other_tg_after_timer_expires() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
expired = now + 5 * 60 + 1
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=expired
)
def test_single_zero_allows_multiple_static_tgs() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=0;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
def test_tg4000_reset_only_on_voice_header() -> None:
assert tg4000_reset_on_vhead(4000, HBPF_DATA_SYNC, HBPF_SLT_VHEAD)
assert not tg4000_reset_on_vhead(4000, HBPF_DATA_SYNC, 2)
assert not tg4000_reset_on_vhead(730, HBPF_DATA_SYNC, HBPF_SLT_VHEAD)
def test_tg4000_clears_single_session() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
clear_peer_ua_sessions(peer, sys_cfg, peer_id, slot=2)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_tg4000_clear_rx_status_blocks_rpto_reseed() -> None:
"""After 4000, stale STATUS must not re-seed UA session on RPTO."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;", "CONNECTED": 999_000.0}
peer_id = _peer_id()
sys_cfg = _sys_cfg()
status = {2: {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 999_500.0}}
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
clear_peer_ua_sessions(peer, sys_cfg, peer_id, slot=2)
clear_peer_rx_status_slots(status, peer_id, slot=2)
seed_peer_ua_session_from_status(peer, peer_id, 2, status[2], sys_cfg, now=now + 10)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 20
)
def test_seed_session_from_status_after_options() -> None:
"""TX on 7305 before RPTO SINGLE=1 — OPTIONS must seed the lock."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;", "CONNECTED": 999_000.0}
peer_id = _peer_id()
sys_cfg = _sys_cfg()
status = {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 999_500.0}
now = 1_000_000.0
seed_peer_ua_session_from_status(
peer, peer_id, 2, status, sys_cfg, now=now,
)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_seed_ignores_stale_status_from_before_reconnect() -> None:
"""OPTIONS after reconnect must not restore indigo from pre-login STATUS."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;", "CONNECTED": 2_000_000.0}
peer_id = _peer_id()
sys_cfg = _sys_cfg()
status = {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 1_000_000.0}
now = 2_000_100.0
seed_peer_ua_session_from_status(
peer, peer_id, 2, status, sys_cfg, now=now,
)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_disconnect_clears_session_on_peer_and_sys_cfg() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
clear_peer_ua_sessions(peer, sys_cfg, peer_id)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_disconnect_clears_stale_rx_status() -> None:
peer_id = _peer_id()
status = {
2: {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 1_000_000.0},
}
clear_peer_rx_status_slots(status, peer_id)
assert status[2]["RX_PEER"] == b"\x00"
assert int_id(status[2]["RX_TGID"]) == 0
def test_single_never_blocks_uplink_tx_switches_indigo() -> None:
"""SINGLE=1 blocks RX on other TGs, not local TX; new TX replaces the session."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_single_blocks_group_voice(peer, 2, 730, sys_cfg, peer_id=peer_id, now=now + 10)
register_peer_ua_session(peer, peer_id, 2, 730, sys_cfg, now=now + 120)
assert not peer_single_blocks_group_voice(peer, 2, 730, sys_cfg, peer_id=peer_id, now=now + 121)
assert peer_single_blocks_group_voice(peer, 2, 7305, sys_cfg, peer_id=peer_id, now=now + 121)
def test_single_still_blocks_non_static_dynamic() -> None:
"""Dynamic UA not in OPTIONS stays exclusive to the session owner."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert not peer_should_receive_group_voice(
peer, 2, 730444, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_cross_peer_static_tx_does_not_reach_locked_peer() -> None:
"""730039101 indigo 7305 must not hear 730 when 730039210 keys up on 730."""
peer_a = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
peer_b = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
id_a = bytes_4(730039101)
id_b = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer_a, id_a, 2, 7305, sys_cfg, now=now)
register_peer_ua_session(peer_b, id_b, 2, 730, sys_cfg, now=now + 5)
assert not peer_should_receive_group_voice(
peer_a, 2, 730, peer_id=id_a, connected_count=3, sys_cfg=sys_cfg, now=now + 10
)
def test_active_routing_table_does_not_fan_out_without_static_tg() -> None:
"""Regression: ACTIVE BRIDGES leg must not deliver to peers missing TG in OPTIONS."""
peer_without = {"OPTIONS": b"TS2=91;"}
peer_with = {"OPTIONS": b"TS2=7305;"}
bridges = {
"7305": [
{
"SYSTEM": "SYSTEM",
"TS": 2,
"ACTIVE": True,
"TO_TYPE": "ON",
}
]
}
assert not peer_should_receive_group_voice(
peer_without,
2,
7305,
system="SYSTEM",
bridges=bridges,
connected_count=8,
sys_cfg=_sys_cfg(),
)
assert peer_should_receive_group_voice(
peer_with,
2,
7305,
system="SYSTEM",
bridges=bridges,
connected_count=8,
sys_cfg=_sys_cfg(),
)
def test_dynamic_ua_delivers_only_to_session_owner() -> None:
"""UA TG not in static OPTIONS — only the activating SINGLE peer receives it."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
other = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
other_id = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 730444, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 730444, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
assert not peer_should_receive_group_voice(
other, 2, 730444, peer_id=other_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_peer_without_static_tgs_receives_nothing_until_dynamic() -> None:
"""No OPTIONS static list → no downlink; dynamic UA session is the only path."""
peer = {"OPTIONS": b"Type=HBlink;SINGLE=1;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
assert not peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=1, sys_cfg=sys_cfg, now=now + 10
)
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=1, sys_cfg=sys_cfg, now=now + 10
)
def test_single_zero_dynamic_heard_when_both_peers_keyed() -> None:
"""SINGLE=0: HS1 and HS2 both keyed 7304 → each hears the other's TX on 7304."""
peer_a = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
peer_b = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
sys_cfg = _sys_cfg()
id_a = bytes_4(730039101)
id_b = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer_a, id_a, 2, 7304, sys_cfg, now=now)
register_peer_ua_session(peer_b, id_b, 2, 7304, sys_cfg, now=now + 5)
assert peer_should_receive_group_voice(
peer_a, 2, 7304, peer_id=id_a, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
assert peer_should_receive_group_voice(
peer_b, 2, 7304, peer_id=id_b, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
def test_single_zero_dynamic_not_heard_without_local_key() -> None:
"""SINGLE=0: dynamic 7304 only reaches peers that keyed it (not the other HS)."""
peer_a = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
peer_b = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
sys_cfg = _sys_cfg()
id_a = bytes_4(730039101)
id_b = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer_a, id_a, 2, 7304, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer_a, 2, 7304, peer_id=id_a, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
assert not peer_should_receive_group_voice(
peer_b, 2, 7304, peer_id=id_b, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
def test_single_zero_tg4000_clears_multi_dynamic() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_multi_tg(peer, peer_id, 2, 7304, sys_cfg)
clear_peer_ua_sessions(peer, sys_cfg, peer_id, slot=2)
assert not peer_should_receive_group_voice(
peer, 2, 7304, peer_id=peer_id, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
def test_register_peer_ua_session_ignores_4000() -> None:
"""TG 4000 resets dynamics; it must never be stored as a UA session."""
peer_id = _peer_id()
sys_cfg = _sys_cfg()
now = 1_000_000.0
peer_multi = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
register_peer_ua_session(peer_multi, peer_id, 2, 4000, sys_cfg, now=now)
pk = peer_id
assert sys_cfg.get("_PEER_UA_MULTI_TGS", {}).get(pk, {}).get(2, set()) == set()
peer_single = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
register_peer_ua_session(peer_single, peer_id, 2, 4000, sys_cfg, now=now)
assert peer_single_exclusive_tgid(peer_single, 2, sys_cfg, peer_id=peer_id, now=now) is None
def test_register_peer_ua_session_ignores_echo_9990() -> None:
"""Echo TG 9990 must not create SINGLE=1 exclusive listen lock."""
peer_id = _peer_id()
sys_cfg = _sys_cfg()
now = 1_000_000.0
peer = {"OPTIONS": b"TS2=730444;SINGLE=1;TIMER=5;"}
register_peer_ua_session(peer, peer_id, 2, 730444, sys_cfg, now=now)
register_peer_ua_session(peer, peer_id, 2, 9990, sys_cfg, now=now)
assert peer_single_exclusive_tgid(peer, 2, sys_cfg, peer_id=peer_id, now=now) == 730444
def test_register_peer_ua_session_ignores_service_999x() -> None:
"""On-demand / service 9991–9999 are not UA sessions (same class as echo 9990)."""
peer_id = _peer_id()
sys_cfg = _sys_cfg()
now = 1_000_000.0
peer = {"OPTIONS": b"TS2=730444;SINGLE=1;TIMER=5;"}
register_peer_ua_session(peer, peer_id, 2, 9999, sys_cfg, now=now)
assert peer_single_exclusive_tgid(peer, 2, sys_cfg, peer_id=peer_id, now=now) is None
def test_new_tx_replaces_single_session_tg() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
register_peer_ua_session(peer, peer_id, 2, 730, sys_cfg, now=now + 120)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 121
)
assert not peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 121
)
def test_downlink_listen_registers_single_session() -> None:
"""SINGLE=1: first downlink VHEAD on a static TG locks out other TGs."""
from adn_server.application.routing.downlink import DownlinkContext, track_peer_group_dmrd
sys_cfg = {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 10, "MODE": "MASTER", "MAX_PEERS": 8}
config = {"PROXY": {"TARGET_SYSTEM": "MASTER-A"}, "SYSTEMS": {"MASTER-A": sys_cfg}}
peer_id = _peer_id()
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
ctx = DownlinkContext(
config=config,
system_name="MASTER-A",
sys_cfg=sys_cfg,
peers={peer_id: peer},
status={1: {}, 2: {}},
connected_count=3,
)
now = 1_000_000.0
vhead_7305 = b"".join([
b"DMRD", b"\x00", bytes_3(100), bytes_3(7305), b"\x00\x00\x00\x00",
bytes([0x80 | (HBPF_DATA_SYNC << 4) | HBPF_SLT_VHEAD]), bytes_4(0x11111111),
] + [b"\x00"] * 33)
track_peer_group_dmrd(ctx, peer_id, vhead_7305, peer, pkt_time=now)
assert peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 1,
)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 1,
)
def test_downlink_listen_allows_voice_frames_after_vhead() -> None:
"""Listen lock on VHEAD must not block voice frames of the same downlink stream."""
from adn_server.application.routing.downlink import (
DownlinkContext,
peer_accepts_group_dmrd_packet,
track_peer_group_dmrd,
)
from adn_server.domain import HBPF_DATA_SYNC, HBPF_SLT_VHEAD, bytes_3, bytes_4
from adn_server.domain.hbp_protocol import HBPF_VOICE
sys_cfg = {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 10, "MODE": "MASTER", "MAX_PEERS": 8}
config = {"PROXY": {"TARGET_SYSTEM": "MASTER-A"}, "SYSTEMS": {"MASTER-A": sys_cfg}}
peer_id = _peer_id()
peer = {"OPTIONS": b"TS2=730502;SINGLE=1;TIMER=60;"}
ctx = DownlinkContext(
config=config,
system_name="MASTER-A",
sys_cfg=sys_cfg,
peers={peer_id: peer},
status={1: {}, 2: {}},
connected_count=3,
)
now = 1_000_000.0
stream = bytes_4(0x11111111)
vhead = b"".join([
b"DMRD", b"\x00", bytes_3(100), bytes_3(730502), b"\x00\x00\x00\x00",
bytes([0x80 | (HBPF_DATA_SYNC << 4) | HBPF_SLT_VHEAD]), stream,
] + [b"\x00"] * 33)
voice = b"".join([
b"DMRD", b"\x00", bytes_3(100), bytes_3(730502), b"\x00\x00\x00\x00",
bytes([0x80 | (HBPF_VOICE << 4)]), stream,
] + [b"\x00"] * 33)
track_peer_group_dmrd(ctx, peer_id, vhead, peer, pkt_time=now)
assert peer_accepts_group_dmrd_packet(ctx, peer_id, peer, voice)
def test_downlink_vterm_clears_listen_session_not_local_tx() -> None:
"""Listen lock from downlink ends on VTERM; local TX session survives."""
from adn_server.application.routing.downlink import (
DownlinkContext,
touch_peer_voice_slot,
track_peer_group_dmrd,
)
sys_cfg = {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 10, "MODE": "MASTER", "MAX_PEERS": 8}
config = {"PROXY": {"TARGET_SYSTEM": "MASTER-A"}, "SYSTEMS": {"MASTER-A": sys_cfg}}
peer_id = _peer_id()
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
ctx = DownlinkContext(
config=config,
system_name="MASTER-A",
sys_cfg=sys_cfg,
peers={peer_id: peer},
status={1: {}, 2: {}},
connected_count=3,
)
now = 1_000_000.0
stream = bytes_4(0x11111111)
vhead = b"".join([
b"DMRD", b"\x00", bytes_3(100), bytes_3(7305), b"\x00\x00\x00\x00",
bytes([0x80 | (HBPF_DATA_SYNC << 4) | HBPF_SLT_VHEAD]), stream,
] + [b"\x00"] * 33)
vterm = b"".join([
b"DMRD", b"\x00", bytes_3(100), bytes_3(7305), b"\x00\x00\x00\x00",
bytes([0x80 | (HBPF_DATA_SYNC << 4) | HBPF_SLT_VTERM]), stream,
] + [b"\x00"] * 33)
track_peer_group_dmrd(ctx, peer_id, vhead, peer, pkt_time=now)
track_peer_group_dmrd(ctx, peer_id, vterm, peer, pkt_time=now + 8)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 9,
)
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now + 20)
touch_peer_voice_slot(
ctx, peer_id, 2, bytes_4(0x22222222), bytes_3(7305), pkt_time=now + 21, ingress=True,
)
track_peer_group_dmrd(ctx, peer_id, vterm, peer, pkt_time=now + 30, from_ingress=False)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 31,
)
def test_single_lock_persists_after_vterm_until_timer_expires() -> None:
"""SINGLE=1: ingress VTERM does not clear TIMER session; other TGs blocked until expiry."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=1;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 30,
)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 61,
)
def test_single_allows_dynamic_same_tg_while_local_ua() -> None:
"""SINGLE=1 UA on a dynamic TG: downlink on the same TG must pass.
A local PTT that activates a dynamic TG (not in OPTIONS) proves the peer
wants to listen to that TG. Blocking it would make the peer deaf to the
TG it just activated — the OBP/HBP round-trip regression.
"""
from adn_server.application.routing.helpers import peer_single_blocks_foreign_same_tg_downlink
peer = {"OPTIONS": b"TS2=730500,730508;SINGLE=1;TIMER=60;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 730502, sys_cfg, now=now)
assert not peer_single_blocks_foreign_same_tg_downlink(
peer, peer_id, 2, bytes_3(730502), None, sys_cfg, now=now + 10,
)
def test_single_allows_static_same_tg_cross_rx_after_local_ptt() -> None:
"""Static TG in OPTIONS: network downlink on same TG after local PTT (cross-rx)."""
from adn_server.application.routing.helpers import peer_single_blocks_foreign_same_tg_downlink
peer = {"OPTIONS": b"TS1=730500;TS2=730501,730502,730503;SINGLE=1;TIMER=60;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 1, 730500, sys_cfg, now=now)
assert not peer_single_blocks_foreign_same_tg_downlink(
peer, peer_id, 1, bytes_3(730500), None, sys_cfg, now=now + 10,
)
def test_downlink_vterm_does_not_clear_local_ua_session() -> None:
"""Network VTERM on session TG must not wipe a local PTT lock (TIMER session)."""
from adn_server.application.routing.downlink import DownlinkContext, track_peer_group_dmrd
sys_cfg = {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 10, "MODE": "MASTER", "MAX_PEERS": 8}
config = {"PROXY": {"TARGET_SYSTEM": "MASTER-A"}, "SYSTEMS": {"MASTER-A": sys_cfg}}
peer_id = _peer_id()
peer = {"OPTIONS": b"TS2=730500,730508;SINGLE=1;TIMER=60;"}
ctx = DownlinkContext(
config=config,
system_name="MASTER-A",
sys_cfg=sys_cfg,
peers={peer_id: peer},
status={1: {}, 2: {}},
connected_count=3,
)
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 730502, sys_cfg, now=now, source="local")
stream = bytes_4(0x11111111)
vterm = b"".join([
b"DMRD", b"\x00", bytes_3(100), bytes_3(730502), b"\x00\x00\x00\x00",
bytes([0x80 | (HBPF_DATA_SYNC << 4) | HBPF_SLT_VTERM]), stream,
] + [b"\x00"] * 33)
track_peer_group_dmrd(ctx, peer_id, vterm, peer, pkt_time=now + 8)
assert not peer_should_receive_group_voice(
peer, 2, 730500, peer_id=peer_id, connected_count=3, sys_cfg=sys_cfg, now=now + 9,
)

Powered by TurnKey Linux.