Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 49 additions & 47 deletions monitor/src/adn_monitor/application/rts_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ..domain.peer_rf import (
SIMPLEX_VOICE_SLOT,
normalize_ua_voice_slot,
peer_downlink_display_slot,
peer_downlink_display_slots,
)
from ..domain.value_objects import ServerMode
from .alias_service import AliasService
Expand Down Expand Up @@ -116,9 +116,9 @@ def _apply_voice_single_ts(
_apply_multi_mode_chips(state, system, peer_id, peer_row)


def _static_tg_slot_for_peer(peer_row: dict, destination: int, event_slot: int) -> int:
"""Map wire timeslot to the peer chip that lists this TG in OPTIONS (TS1/TS2)."""
return peer_downlink_display_slot(peer_row, destination, event_slot)
def _static_tg_slots_for_peer(peer_row: dict, destination: int, event_slot: int) -> list[int]:
"""Chip slot(s) to update -- both, when this peer has the TG on TS1+TS2."""
return peer_downlink_display_slots(peer_row, destination, event_slot)


def _is_master_peer_row(system: str) -> bool:
Expand Down Expand Up @@ -260,7 +260,7 @@ def _voice_event_target_peers(
return targets


def _peer_display_slot(
def _peer_display_slots(
peer_row: dict,
peer_key,
*,
Expand All @@ -269,20 +269,21 @@ def _peer_display_slot(
event_slot: int,
destination: int,
trx: str = "",
) -> int:
"""Timeslot index for CTABLE peer chips (1 or 2)."""
) -> list[int]:
"""Timeslot index(es) for CTABLE peer chips -- usually one, both when this
peer has the TG on both static OPTIONS slots (see peer_downlink_display_slots)."""
if _peer_keys_equal(source_peer, peer_key):
return event_slot
return [event_slot]
if (
call_type == "GROUP VOICE"
and trx == "TX"
and _is_echo_service_live_tgid(destination)
):
# Echo/service downlink (bridge TX leg): use wire slot, not OPTIONS static map.
return event_slot
return [event_slot]
if call_type == "GROUP VOICE":
return _static_tg_slot_for_peer(peer_row, destination, event_slot)
return event_slot
return _static_tg_slots_for_peer(peer_row, destination, event_slot)
return [event_slot]


def rts_update_impl(
Expand Down Expand Up @@ -375,7 +376,7 @@ def rts_update_impl(
destination=destination,
dest_peer_id=dest_peer_id,
):
display_slot = _peer_display_slot(
display_slots = _peer_display_slots(
peer_row,
peer,
call_type=call_type,
Expand All @@ -385,41 +386,42 @@ def rts_update_impl(
trx=trx,
)
crxstatus = "RX" if _peer_keys_equal(source_peer, peer) else "TX"
peer_ts = peer_row[display_slot]
if action == "START":
if (
peer_ts.get("TS")
and not _is_echo_service_live_tgid(destination)
and _peer_slot_busy_other_tg(peer_ts, destination)
):
continue
# Local PTT (TRX=RX / red): do not replace with another peer's downlink (TRX=TX / green).
if (
peer_ts.get("TS")
and peer_ts.get("TRX") == "RX"
and not _peer_keys_equal(source_peer, peer)
and not _is_echo_service_live_tgid(destination)
):
continue
peer_ts["TIMEOUT"] = timeout
peer_ts["TS"] = True
peer_ts["TYPE"] = call_type
peer_ts["SUB"] = f"{sub_short} ({source_sub})"
peer_ts["CALL"] = sub_call
peer_ts["SRC"] = peer
peer_ts["DEST"] = tg_dest
peer_ts["TG"] = tg_short
peer_ts["TRX"] = crxstatus
peer_ts["ANNOUNCEMENT"] = is_announcement
elif action == "END":
if (
trx == "TX"
and peer_ts.get("TS")
and not _is_echo_service_live_tgid(destination)
and _peer_slot_busy_other_tg(peer_ts, destination)
):
continue
clear_voice_ts_for_destination(peer_row, destination)
for display_slot in display_slots:
peer_ts = peer_row[display_slot]
if action == "START":
if (
peer_ts.get("TS")
and not _is_echo_service_live_tgid(destination)
and _peer_slot_busy_other_tg(peer_ts, destination)
):
continue
# Local PTT (TRX=RX / red): do not replace with another peer's downlink (TRX=TX / green).
if (
peer_ts.get("TS")
and peer_ts.get("TRX") == "RX"
and not _peer_keys_equal(source_peer, peer)
and not _is_echo_service_live_tgid(destination)
):
continue
peer_ts["TIMEOUT"] = timeout
peer_ts["TS"] = True
peer_ts["TYPE"] = call_type
peer_ts["SUB"] = f"{sub_short} ({source_sub})"
peer_ts["CALL"] = sub_call
peer_ts["SRC"] = peer
peer_ts["DEST"] = tg_dest
peer_ts["TG"] = tg_short
peer_ts["TRX"] = crxstatus
peer_ts["ANNOUNCEMENT"] = is_announcement
elif action == "END":
if (
trx == "TX"
and peer_ts.get("TS")
and not _is_echo_service_live_tgid(destination)
and _peer_slot_busy_other_tg(peer_ts, destination)
):
continue
clear_voice_ts_for_destination(peer_row, destination)

server_mode = getattr(state, "server_mode", ServerMode.LEGACY)
if system in ctable.get("OPENBRIDGES", {}):
Expand Down
31 changes: 29 additions & 2 deletions monitor/src/adn_monitor/domain/peer_rf.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,37 @@ def peer_downlink_display_slot(
ts2 = [str(x).strip() for x in (peer_row.get("TS2_STATIC") or []) if str(x).strip()]
in_ts1 = tg in ts1
in_ts2 = tg in ts2
if in_ts1 and in_ts2:
# TG duplicated on both static slots: each downlink event carries its
# own real wire slot (server now delivers both independently), so the
# chip must follow event_slot -- collapsing to SIMPLEX_VOICE_SLOT here
# would make the TS1 delivery overwrite the same chip as TS2.
return event_slot
if peer_is_simplex(peer_row) and (in_ts1 or in_ts2):
return SIMPLEX_VOICE_SLOT
if in_ts1 and not in_ts2:
if in_ts1:
return 1
if in_ts2 and not in_ts1:
if in_ts2:
return 2
return event_slot


def peer_downlink_display_slots(
peer_row: dict[str, Any],
destination: int,
event_slot: int,
) -> list[int]:
"""CTABLE chip slot(s) to update for one downlink event to this peer.

Normally a single slot (see ``peer_downlink_display_slot``). When the TG
is duplicated on both static OPTIONS slots, adn-server's
``iter_downlink_voice_slots`` delivers to this peer independently on
TS1 *and* TS2 from the one incoming event -- both chips must reflect
that, not just whichever slot the source happened to transmit on.
"""
tg = str(destination)
ts1 = [str(x).strip() for x in (peer_row.get("TS1_STATIC") or []) if str(x).strip()]
ts2 = [str(x).strip() for x in (peer_row.get("TS2_STATIC") or []) if str(x).strip()]
if tg in ts1 and tg in ts2:
return [1, 2]
return [peer_downlink_display_slot(peer_row, destination, event_slot)]
14 changes: 14 additions & 0 deletions monitor/tests/domain/test_peer_rf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,17 @@ def test_normalize_ua_voice_slot_simplex_uses_ts2():
def test_peer_downlink_display_slot_simplex_static_on_ts2():
peer = {"RF_MODE": "simplex", "TS1_STATIC": ["730"], "TS2_STATIC": []}
assert peer_downlink_display_slot(peer, 730, 1) == 2


def test_peer_downlink_display_slot_tg_on_both_static_slots_follows_event_slot():
"""Duplex-capable hotspot with the same TG on TS1+TS2: each downlink event
(server now delivers both independently) must chip its own real slot, not
collapse onto SIMPLEX_VOICE_SLOT -- else the TS2 delivery overwrites the
TS1 chip and the UI only ever shows one slot active."""
peer = {"RF_MODE": "duplex", "TS1_STATIC": ["730"], "TS2_STATIC": ["730"]}
assert peer_downlink_display_slot(peer, 730, 1) == 1
assert peer_downlink_display_slot(peer, 730, 2) == 2

peer_simplex = {"RF_MODE": "simplex", "TS1_STATIC": ["730"], "TS2_STATIC": ["730"]}
assert peer_downlink_display_slot(peer_simplex, 730, 1) == 1
assert peer_downlink_display_slot(peer_simplex, 730, 2) == 2
18 changes: 18 additions & 0 deletions monitor/tests/test_rts_display_slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,24 @@ def test_wire_ts2_colors_ts1_when_tg_only_in_ts1_static() -> None:
assert peer[2]["TS"] is False


def test_tg_on_both_static_slots_lights_up_both_chips() -> None:
"""Duplex hotspot with the TG on TS1+TS2: adn-server delivers to it on
both slots independently (e.g. an OBP-sourced call, always reported on
slot 1) -- both chips must light up, not just the event's own slot."""
state = _state_with_peer(peer_id=730001, ts1_static=["730"], ts2_static=["730"])
rts_update_impl(
"GROUP VOICE,START,TX,SYSTEM,1,730002,730002,1,730".split(","),
state,
_alias(),
lambda: "12:00",
)
peer = state.CTABLE["MASTERS"]["SYSTEM"]["PEERS"][730001]
assert peer[1]["TS"] is True
assert peer[1]["TRX"] == "TX"
assert peer[2]["TS"] is True
assert peer[2]["TRX"] == "TX"


def test_transmitter_rx_uses_wire_slot_not_options() -> None:
state = _state_with_peer(peer_id=730002, ts1_static=[], ts2_static=["73010"])
rts_update_impl(
Expand Down
Loading