fix: remap REPEAT DMRD slot to peer OPTIONS for cross-slot downlink

Flip the DMRD timeslot bit per receiver so hotspots hear group voice on
the TS where the TG is configured (static OPTIONS or dynamic UA), and
emit inject-proxy BRDG field 7 on that same slot for monitor TE parity.
pull/17/head
Rodrigo Pérez 3 months ago
parent 2f13ab7c64
commit 1e423dff15

@ -31,7 +31,7 @@ from __future__ import annotations
import copy
from typing import Any
from adn_server.application.routing.helpers import is_special_tg, peer_should_receive_group_voice
from adn_server.application.routing.helpers import is_special_tg, peer_downlink_voice_slot, peer_should_receive_group_voice
from adn_server.application.proxy.deployment import is_proxy_inject_only, proxy_target_system
from adn_server.domain.value_objects import bytes_4, int_id
@ -262,6 +262,26 @@ def _echo_tx_target_peer(parts: list[str], peers: dict[Any, Any]) -> bytes | Non
return _peer_key_from_voice_csv(parts, peers)
def _voice_event_with_peer_display_slot(
parts: list[str],
*,
peer: dict[str, Any],
peer_key: bytes,
wire_slot: int,
tgid: int,
sys_cfg: dict[str, Any] | None,
) -> list[str]:
"""Set BRDG CSV field 7 to the receiver's configured listen TS (monitor TE chip)."""
out = list(parts)
if len(out) > 7:
out[7] = str(
peer_downlink_voice_slot(
peer, wire_slot, tgid, sys_cfg, peer_id=peer_key,
)
)
return out
def _remap_voice_event_to_slot(
parts: list[str],
*,
@ -361,13 +381,21 @@ def remap_inject_proxy_voice_events(
if not receivers:
return [event]
remapped: list[str] = []
for peer_key, _peer in receivers:
for peer_key, peer in receivers:
mapped_slot = slot_map.get(peer_key)
if mapped_slot is None:
continue
peer_parts = _voice_event_with_peer_display_slot(
parts,
peer=peer,
peer_key=_peer_key_from_int(peer_key),
wire_slot=voice_slot,
tgid=tgid,
sys_cfg=sys_cfg,
)
remapped.append(
_remap_voice_event_to_slot(
parts, target=target, slot=mapped_slot, peer_key=peer_key
peer_parts, target=target, slot=mapped_slot, peer_key=peer_key
)
)
return remapped if remapped else [event]
@ -390,7 +418,7 @@ def remap_inject_proxy_voice_events(
tx_parts = list(parts)
tx_parts[2] = "TX"
tx_parts[5] = str(int_id(peer_key))
for other_key, _peer in _peers_receiving_tgid(
for other_key, peer in _peers_receiving_tgid(
connected,
slot=voice_slot,
tgid=tgid,
@ -402,9 +430,17 @@ def remap_inject_proxy_voice_events(
other_slot = slot_map.get(other_key)
if other_slot is None:
continue
peer_parts = _voice_event_with_peer_display_slot(
tx_parts,
peer=peer,
peer_key=_peer_key_from_int(other_key),
wire_slot=voice_slot,
tgid=tgid,
sys_cfg=sys_cfg,
)
results.append(
_remap_voice_event_to_slot(
tx_parts,
peer_parts,
target=target,
slot=other_slot,
peer_key=other_key,

@ -638,6 +638,64 @@ def synthetic_group_dmrd_route_packet(slot: int, tgid: int) -> bytes:
return b"DMRD" + b"\x00" * 4 + bytes_3(tgid) + b"\x00" * 4 + bytes([bits]) + b"\x00" * 38
def peer_downlink_voice_slot(
peer: dict[str, Any],
wire_slot: int,
tgid: int,
sys_cfg: dict[str, Any] | None = None,
*,
peer_id: bytes | None = None,
) -> int:
"""Monitor/BRDG field 7: TS where this peer listens for ``tgid`` (OPTIONS or UA)."""
static = peer_options_static_tg_slot(peer, tgid)
if static is not None:
return static
if sys_cfg is not None and peer_id is not None:
tgid_i = int(tgid)
pk = bytes_4(int_id(peer_id))
for voice_slot in (1, 2):
locked = peer_single_exclusive_tgid(
peer, voice_slot, sys_cfg, peer_id=peer_id,
)
if locked is not None and int(locked) == tgid_i:
return voice_slot
store = sys_cfg.get("_PEER_UA_MULTI_TGS")
if isinstance(store, dict):
per_peer = store.get(pk)
if isinstance(per_peer, dict):
for voice_slot in (1, 2):
slot_set = per_peer.get(voice_slot)
if isinstance(slot_set, set) and tgid_i in slot_set:
return voice_slot
return int(wire_slot)
def remap_dmrd_to_peer_static_slot(
packet: bytes,
peer: dict[str, Any],
sys_cfg: dict[str, Any] | None = None,
*,
peer_id: bytes | None = None,
) -> bytes:
"""Flip DMRD slot bit so the hotspot RF TS matches OPTIONS/UA for this TG."""
parsed = parse_dmrd_route_fields(packet)
if parsed is None:
return packet
voice_slot, tgid, call_type = parsed
if call_type not in ("group", "vcsbk"):
return packet
if is_special_tg(str(tgid)):
return packet
cfg_slot = peer_downlink_voice_slot(
peer, voice_slot, tgid, sys_cfg, peer_id=peer_id,
)
if cfg_slot == voice_slot:
return packet
bits = packet[15]
new_bits = bits ^ (1 << 7)
return packet[:15] + bytes([new_bits]) + packet[16:]
def repeat_downlink_report_slot(
wire_slot: int,
tgid: int,
@ -656,27 +714,11 @@ def repeat_downlink_report_slot(
peer = peers.get(peer_id)
if not isinstance(peer, dict):
continue
static_slot = peer_options_static_tg_slot(peer, tgid_i)
if static_slot is not None:
display_slots.add(static_slot)
continue
if not sys_cfg:
continue
pk = bytes_4(int_id(peer_id))
for voice_slot in (1, 2):
locked = peer_single_exclusive_tgid(
peer, voice_slot, sys_cfg, peer_id=peer_id,
display_slots.add(
peer_downlink_voice_slot(
peer, wire_slot, tgid_i, sys_cfg, peer_id=peer_id,
)
if locked is not None and int(locked) == tgid_i:
display_slots.add(voice_slot)
store = sys_cfg.get("_PEER_UA_MULTI_TGS")
if isinstance(store, dict):
per_peer = store.get(pk)
if isinstance(per_peer, dict):
for voice_slot in (1, 2):
slot_set = per_peer.get(voice_slot)
if isinstance(slot_set, set) and tgid_i in slot_set:
display_slots.add(voice_slot)
)
if len(display_slots) == 1:
return display_slots.pop()
return int(wire_slot)

@ -50,6 +50,7 @@ from ...application.routing.helpers import (
peer_should_receive_group_voice,
peer_single_exclusive_tgid,
register_peer_ua_session,
remap_dmrd_to_peer_static_slot,
resolve_voice_peer_id,
repeat_downlink_report_slot,
seed_peer_ua_session_from_status,
@ -576,6 +577,11 @@ class HBPProtocol(DatagramProtocol):
if _packet[:4] == DMRD:
if not self._peer_should_receive_dmrd(_peer, _packet):
return
peer = self._peers.get(_peer)
if peer is not None:
_packet = remap_dmrd_to_peer_static_slot(
_packet, peer, self._config, peer_id=_peer,
)
_packet = b"".join([_packet[:11], _peer, _packet[15:]])
self.transport.write(_packet, self._peers[_peer]["SOCKADDR"])

@ -228,6 +228,27 @@ def test_local_hotspot_rx_fans_out_tx_only_to_peers_with_matching_tg() -> None:
assert "SYSTEM-4" not in by_system
def test_local_hotspot_rx_companion_tx_uses_receiver_options_slot() -> None:
"""HS2 TX slot 2 / TG 7144 → HS1 with TS1=7144 gets BRDG field 7 = 1 (monitor TE)."""
hs1 = bytes_4(730001)
hs2 = bytes_4(730002)
peers = {
hs1: _peer(options=b"TS1=7144;TS2=714,71442;"),
hs2: _peer(options=b"TS2=7144;"),
}
config = _proxy_config(peers)
peer_slots = {hs1: 0, hs2: 1}
raw = "GROUP VOICE,START,RX,SYSTEM,3262598598,730002,730002,2,7144"
events = remap_inject_proxy_voice_events(
raw, config, config["SYSTEMS"], peer_slots
)
tx_events = [e for e in events if e.split(",")[2] == "TX"]
assert len(tx_events) == 1
parts = tx_events[0].split(",")
assert parts[3] == "SYSTEM-0"
assert int(parts[7]) == 1
def test_obp_tx_single_hotspot_remaps_dynamic_tg_not_in_static() -> None:
"""One HS online: downlink/monitor must remap TX even when TG is UA-only (not in OPTIONS)."""
peer = bytes_4(730039101)

@ -30,14 +30,17 @@ from adn_server.application.routing.helpers import (
tg4000_reset_on_vhead,
clear_peer_rx_status_slots,
peer_options_static_tg_slot,
peer_downlink_voice_slot,
peer_receives_group_tgid,
peer_should_receive_group_voice,
remap_dmrd_to_peer_static_slot,
peer_single_blocks_group_voice,
peer_single_blocks_uplink,
peer_single_exclusive_tgid,
register_peer_ua_multi_tg,
register_peer_ua_session,
repeat_downlink_report_slot,
peer_downlink_voice_slot,
seed_peer_ua_session_from_status,
)
@ -79,12 +82,46 @@ def test_dynamic_tg_on_opposite_slot_receives_group_voice() -> None:
def test_repeat_downlink_report_slot_cross_slot_static() -> None:
peers = {
_peer_id(): {"OPTIONS": b"TS2=730444;"},
_peer_id(): {"OPTIONS": b"TS1=7144;"},
}
slot = repeat_downlink_report_slot(
1, 730444, peers, (_peer_id(),), _sys_cfg(),
2, 7144, peers, (_peer_id(),), _sys_cfg(),
)
assert slot == 2
assert slot == 1
def test_peer_downlink_voice_slot_cross_slot_static() -> None:
peer = {"OPTIONS": b"TS1=7144;TS2=714,71442;"}
assert peer_downlink_voice_slot(peer, 2, 7144) == 1
peer2 = {"OPTIONS": b"TS2=7144;"}
assert peer_downlink_voice_slot(peer2, 1, 7144) == 2
def test_remap_dmrd_flips_slot_to_options_static_ts() -> None:
from tests.harness.deterministic import DeterministicScenario, PacketSpec
peer = {"OPTIONS": b"TS1=7144;TS2=714,71442;"}
burst = DeterministicScenario.voice_burst_spec(
PacketSpec(dst_id=7144, slot=2, peer_id=730044402, rf_src=7300444),
seq=1,
dtype_vseq=1,
).data()
assert burst[15] & 0x80
remapped = remap_dmrd_to_peer_static_slot(burst, peer)
assert not (remapped[15] & 0x80)
def test_remap_dmrd_keeps_slot_when_options_match_voice_ts() -> None:
from tests.harness.deterministic import DeterministicScenario, PacketSpec
peer = {"OPTIONS": b"TS2=7144;"}
burst = DeterministicScenario.voice_burst_spec(
PacketSpec(dst_id=7144, slot=2, peer_id=730044402, rf_src=7300444),
seq=1,
dtype_vseq=1,
).data()
remapped = remap_dmrd_to_peer_static_slot(burst, peer)
assert remapped[15] == burst[15]
def test_repeat_downlink_report_slot_dynamic_on_ts2() -> None:

@ -72,6 +72,34 @@ def test_repeat_only_reaches_peers_with_matching_options() -> None:
assert other_pkts == []
def test_repeat_remaps_slot_to_peer_options_ts() -> None:
"""Simplex TS2 TX / TG 7144 → duplex TS1=7144 receives DMRD on TS1 (slot bit flip)."""
stack = build_hbp_repeat_stack(talker_alias=False, system_name="MASTER-A")
stack.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"}
stack.hbp._CONFIG = stack.config
simplex = bytes_4(730002)
duplex = bytes_4(730001)
addr_simplex = ("10.0.0.31", 62031)
addr_duplex = ("10.0.0.30", 62030)
stack.register_peer(simplex, addr_simplex, options="TS2=7144;")
stack.register_peer(duplex, addr_duplex, options="TS1=7144;TS2=714,71442;")
spec = PacketSpec(
peer_id=int.from_bytes(simplex, "big"),
rf_src=730002,
dst_id=7144,
slot=2,
stream_id=0x22334455,
payload=b"\x00" * 33,
)
burst = DeterministicScenario.voice_burst_spec(spec, seq=1, dtype_vseq=1).data()
stack.inject(burst, addr_simplex)
duplex_pkts = [p for p in stack.transport.for_addr(addr_duplex) if p[:4] == DMRD]
assert len(duplex_pkts) == 1
assert not (duplex_pkts[0][15] & 0x80)
def test_repeat_cross_slot_static_tg_downlink() -> None:
"""Voice on TS1 reaches hotspot that lists the TG only on TS2 (PR #2 parity)."""
stack = build_hbp_repeat_stack(talker_alias=False, system_name="MASTER-A")

Loading…
Cancel
Save

Powered by TurnKey Linux.