@ -271,6 +271,31 @@ def _peer_status_rx_hangtime_blocks(
return ( pkt_time - rx_t ) < hang
return ( pkt_time - rx_t ) < hang
def _peer_single_locked_tgid (
peer : dict [ str , Any ] ,
sys_cfg : dict [ str , Any ] ,
* ,
peer_id : bytes | None = None ,
now : float | None = None ,
prefer_slot : int | None = None ,
) - > int | None :
""" First active SINGLE=1 session TG (``prefer_slot`` checked before the other TS). """
slots : list [ int ] = [ ]
if prefer_slot is not None :
slots . append ( int ( prefer_slot ) )
for voice_slot in ( 1 , 2 ) :
if prefer_slot is not None and voice_slot == int ( prefer_slot ) :
continue
slots . append ( voice_slot )
for voice_slot in slots :
locked = peer_single_exclusive_tgid (
peer , voice_slot , sys_cfg , peer_id = peer_id , now = now ,
)
if locked is not None :
return locked
return None
def peer_single_same_tg_foreign_tx_blocks (
def peer_single_same_tg_foreign_tx_blocks (
peer : dict [ str , Any ] ,
peer : dict [ str , Any ] ,
peer_id : bytes ,
peer_id : bytes ,
@ -285,13 +310,9 @@ def peer_single_same_tg_foreign_tx_blocks(
if not sys_cfg or not peer_single_mode ( peer , sys_cfg ) :
if not sys_cfg or not peer_single_mode ( peer , sys_cfg ) :
return False
return False
incoming = int_id ( incoming_tgid_b )
incoming = int_id ( incoming_tgid_b )
locked = None
locked = _peer_single_locked_tgid (
for voice_slot in ( 1 , 2 ) :
peer , sys_cfg , peer_id = peer_id , now = pkt_time ,
locked = peer_single_exclusive_tgid (
)
peer , voice_slot , sys_cfg , peer_id = peer_id , now = pkt_time ,
)
if locked is not None :
break
if locked is None or int ( locked ) != incoming :
if locked is None or int ( locked ) != incoming :
return False
return False
if not slot_has_active_voice ( slot_st , pkt_time ) :
if not slot_has_active_voice ( slot_st , pkt_time ) :
@ -339,10 +360,22 @@ def peer_hotspot_voice_slot_busy(
if isinstance ( active , dict ) :
if isinstance ( active , dict ) :
if active . get ( " ingress " ) :
if active . get ( " ingress " ) :
return True
return True
if peer_hotspot_hard_slot_one_qso ( peer ) :
active_stream = active . get ( " stream_id " )
active_stream = active . get ( " stream_id " )
incoming_tgid = int_id ( incoming_tgid_b )
if not ( stream_id and active_stream and active_stream == stream_id ) :
active_tgid = int ( active . get ( " tgid " , 0 ) or 0 )
return True
if stream_id and active_stream and active_stream == stream_id :
pass
elif (
stream_id
and active_stream
and active_stream != stream_id
and active_tgid
and active_tgid == incoming_tgid
and ( pkt_time - float ( active . get ( " time " , 0 ) or 0 ) ) > = STREAM_TO
) :
peer_slots . pop ( int ( voice_slot ) , None )
else :
return True
if isinstance ( peer , dict ) and peer_single_blocks_foreign_same_tg_downlink (
if isinstance ( peer , dict ) and peer_single_blocks_foreign_same_tg_downlink (
peer , pk , voice_slot , incoming_tgid_b , peer_slots , sys_cfg , now = pkt_time ,
peer , pk , voice_slot , incoming_tgid_b , peer_slots , sys_cfg , now = pkt_time ,
) :
) :
@ -1302,18 +1335,9 @@ def peer_single_blocks_foreign_same_tg_downlink(
if not sys_cfg or not peer_single_mode ( peer , sys_cfg ) :
if not sys_cfg or not peer_single_mode ( peer , sys_cfg ) :
return False
return False
incoming = int_id ( incoming_tgid_b )
incoming = int_id ( incoming_tgid_b )
locked = peer_single_exclusive _tgid(
locked = _peer_single_locked _tgid(
peer , voice_slot, sys_cfg, peer_id = peer_id , now = now ,
peer , sys_cfg, peer_id = peer_id , now = now , prefer_slot = voice_slot ,
)
)
if locked is None :
for alt_slot in ( 1 , 2 ) :
if alt_slot == voice_slot :
continue
locked = peer_single_exclusive_tgid (
peer , alt_slot , sys_cfg , peer_id = peer_id , now = now ,
)
if locked is not None :
break
if locked is None or int ( locked ) != incoming :
if locked is None or int ( locked ) != incoming :
return False
return False
active = ( peer_slots or { } ) . get ( int ( voice_slot ) )
active = ( peer_slots or { } ) . get ( int ( voice_slot ) )
@ -1340,13 +1364,6 @@ def peer_wants_downlink_single_listen_lock(peer: dict[str, Any], sys_cfg: dict[s
return peer_static_options_tg_count ( peer ) < = 6
return peer_static_options_tg_count ( peer ) < = 6
def peer_hotspot_hard_slot_one_qso ( peer : dict [ str , Any ] | None ) - > bool :
""" True when the one-QSO-per-RF-slot rule applies (normal hotspots, not lab witnesses). """
if not isinstance ( peer , dict ) :
return True
return peer_static_options_tg_count ( peer ) < = 6
def peer_receives_group_tgid ( peer : dict [ str , Any ] , slot : int , tgid : int ) - > bool :
def peer_receives_group_tgid ( peer : dict [ str , Any ] , slot : int , tgid : int ) - > bool :
""" True when peer RPTO OPTIONS list the group TG on TS1 or TS2 (legacy REPEAT parity).
""" True when peer RPTO OPTIONS list the group TG on TS1 or TS2 (legacy REPEAT parity).
@ -1378,17 +1395,36 @@ def peer_options_static_tg_slot(peer: dict[str, Any], tgid: int) -> int | None:
return None
return None
def synthetic_group_dmrd_ route _packet(
def synthetic_group_dmrd_ burst _packet(
slot : int ,
slot : int ,
tgid : int ,
tgid : int ,
stream_id : bytes | None = None ,
stream_id : bytes ,
* ,
frame_type : int = 0 ,
dtype_vseq : int = 0 ,
call_type : str = " group " ,
) - > bytes :
) - > bytes :
""" Minimal DMRD for downlink/monitor gate lookup (slot, TG, optional stream). """
""" Minimal DMRD with burst header fields for slot-tracking helpers ."""
bits = 0x80 if int ( slot ) == 2 else 0
bits = 0x80 if int ( slot ) == 2 else 0
if call_type == " vcsbk " :
bits | = 0x23
else :
bits | = ( int ( frame_type ) & 0x3 ) << 4
bits | = int ( dtype_vseq ) & 0xF
sid = bytes_4 ( int_id ( stream_id ) ) if stream_id else b " \x00 " * 4
sid = bytes_4 ( int_id ( stream_id ) ) if stream_id else b " \x00 " * 4
return b " DMRD " + b " \x00 " * 4 + bytes_3 ( tgid ) + b " \x00 " * 4 + bytes ( [ bits ] ) + sid + b " \x00 " * 34
return b " DMRD " + b " \x00 " * 4 + bytes_3 ( tgid ) + b " \x00 " * 4 + bytes ( [ bits ] ) + sid + b " \x00 " * 34
def synthetic_group_dmrd_route_packet (
slot : int ,
tgid : int ,
stream_id : bytes | None = None ,
) - > bytes :
""" Minimal DMRD for downlink/monitor gate lookup (slot, TG, optional stream). """
sid = stream_id if stream_id else b " \x00 " * 4
return synthetic_group_dmrd_burst_packet ( slot , tgid , sid )
def peer_downlink_voice_slot (
def peer_downlink_voice_slot (
peer : dict [ str , Any ] ,
peer : dict [ str , Any ] ,
wire_slot : int ,
wire_slot : int ,