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_monitor_hangtime_gate.py

178 lines
6.0 KiB

# ADN DMR Peer Server - monitor gate parity with send_peer after GROUP_HANGTIME
#
# 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
###############################################################################
"""Monitor fan-out must use BRDG stream id — same gate as send_peer for new QSOs."""
from __future__ import annotations
import time
from adn_server.application.report.monitor_topology import remap_inject_proxy_voice_events
from adn_server.application.routing.downlink import (
DownlinkContext,
apply_hangtime_after_vterm,
peer_slot_blocks_downlink,
)
from adn_server.domain import HBPF_DATA_SYNC, HBPF_SLT_VHEAD, HBPF_SLT_VTERM, bytes_3, bytes_4
def _peer(*, options: bytes) -> dict:
return {
"CONNECTION": "YES",
"CONNECTED": 1_700_000_000,
"IP": "203.0.113.10",
"PORT": 62031,
"CALLSIGN": b"CE5RPY ",
"OPTIONS": options,
}
def _empty_slot() -> dict:
return {
"RX_TIME": 0.0,
"TX_TIME": 0.0,
"RX_TYPE": HBPF_SLT_VTERM,
"TX_TYPE": HBPF_SLT_VTERM,
"RX_TGID": b"\x00\x00\x00",
"TX_TGID": b"\x00\x00\x00",
"RX_STREAM_ID": b"",
"TX_STREAM_ID": b"",
"RX_PEER": b"\x00\x00\x00\x00",
"TX_PEER": b"\x00\x00\x00\x00",
}
def _vhead(slot: int, tgid: int, stream: int) -> bytes:
bits = 0x80 if slot == 2 else 0
bits |= HBPF_DATA_SYNC << 4
bits |= HBPF_SLT_VHEAD
return b"".join([
b"DMRD",
b"\x00",
bytes_3(100),
bytes_3(tgid),
bytes_4(7300392),
bytes([bits]),
bytes_4(stream),
] + [b"\x00"] * 33)
def _ctx(*, hangtime: float = 10.0) -> DownlinkContext:
peer_hs_a = bytes_4(730039101)
peer_hs_b = bytes_4(730039210)
peers = {
peer_hs_a: _peer(options=b"TS2=730,7305;SINGLE=0;"),
peer_hs_b: _peer(options=b"TS2=7305;"),
}
sys_cfg = {"GROUP_HANGTIME": hangtime, "MODE": "MASTER", "MAX_PEERS": 8}
config = {"PROXY": {"TARGET_SYSTEM": "SYSTEM"}, "SYSTEMS": {"SYSTEM": sys_cfg}}
sys_cfg["PEERS"] = peers
return DownlinkContext(
config=config,
system_name="SYSTEM",
sys_cfg=sys_cfg,
peers=peers,
status={1: _empty_slot(), 2: _empty_slot()},
connected_count=2,
)
def test_monitor_blocks_during_hangtime_allows_new_qso_with_stream_id() -> None:
"""New QSO after GROUP_HANGTIME: monitor fan-out matches send_peer when stream id is set."""
ctx = _ctx()
peer_hs_a = bytes_4(730039101)
peer_hs_b = bytes_4(730039210)
peer = ctx.peers[peer_hs_a]
now = time.time()
apply_hangtime_after_vterm(ctx, peer_hs_a, 2, 7306, pkt_time=now - 1)
new_stream = bytes_4(0xAABBCCDD)
ctx.status[2]["TX_STREAM_ID"] = new_stream
ctx.status[2]["TX_PEER"] = peer_hs_b
ctx.status[2]["TX_RFS"] = bytes_4(7300392)
burst = _vhead(2, 7305, 0xAABBCCDD)
assert peer_slot_blocks_downlink(ctx, peer_hs_a, peer, burst)
config = ctx.config
peer_slots = {peer_hs_a: 7, peer_hs_b: 8}
raw = "GROUP VOICE,START,TX,SYSTEM,{},73010,7300392,2,7305".format(
int.from_bytes(new_stream, "big"),
)
during = remap_inject_proxy_voice_events(
raw, config, config["SYSTEMS"], peer_slots, downlink_ctx=ctx,
)
assert {ev.split(",")[3] for ev in during} == {"SYSTEM-8"}
ctx.peer_voice_hangtime[peer_hs_a].pop(2, None)
assert not peer_slot_blocks_downlink(ctx, peer_hs_a, peer, burst)
after = remap_inject_proxy_voice_events(
raw, config, config["SYSTEMS"], peer_slots, downlink_ctx=ctx,
)
assert {ev.split(",")[3] for ev in after} == {"SYSTEM-7", "SYSTEM-8"}
def test_monitor_blocks_panama_end_tx_while_listening_chile() -> None:
"""END/TX for a foreign TG must not fan out when the hotspot is on another QSO."""
from adn_server.application.routing.downlink import touch_peer_voice_slot
peer_hs = bytes_4(714002301)
peers = {peer_hs: _peer(options=b"TS2=7141,71442;")}
sys_cfg = {"GROUP_HANGTIME": 5.0, "MODE": "MASTER", "MAX_PEERS": 8}
config = {"PROXY": {"TARGET_SYSTEM": "SYSTEM"}, "SYSTEMS": {"SYSTEM": sys_cfg}}
sys_cfg["PEERS"] = peers
now = time.time()
chile_stream = bytes_4(0x11111111)
panama_stream = bytes_4(0x22222222)
status = {
1: _empty_slot(),
2: {
**_empty_slot(),
"RX_PEER": peer_hs,
"RX_STREAM_ID": chile_stream,
"RX_TGID": bytes_3(7141),
"RX_TYPE": HBPF_SLT_VHEAD,
"RX_TIME": now,
"TX_PEER": bytes_4(73010),
"TX_STREAM_ID": panama_stream,
"TX_TGID": bytes_3(71442),
"TX_TYPE": HBPF_SLT_VHEAD,
"TX_TIME": now,
},
}
ctx = DownlinkContext(
config=config,
system_name="SYSTEM",
sys_cfg=sys_cfg,
peers=peers,
status=status,
connected_count=1,
)
touch_peer_voice_slot(ctx, peer_hs, 2, chile_stream, bytes_3(7141), pkt_time=now)
peer_slots = {peer_hs: 2}
end_raw = "GROUP VOICE,END,TX,SYSTEM,{},73010,7000002,2,71442,3.50".format(
int.from_bytes(panama_stream, "big"),
)
events = remap_inject_proxy_voice_events(
end_raw, config, config["SYSTEMS"], peer_slots, downlink_ctx=ctx,
)
assert events == []

Powered by TurnKey Linux.