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/src/adn_server/application/routing_use_cases.py

1162 lines
61 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 - routing use cases
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
#
# Derived from ADN DMR Server / FreeDMR / HBlink. Original license:
###############################################################################
# Copyright (C) 2026 Joaquin Madrid Belando, EA5GVK <ea5gvk@gmail.com>
# Copyright (C) 2020 Simon Adlem, G7RZU <g7rzu@gb7fr.org.uk>
# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# 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
###############################################################################
"""Routing facade: dmrd_received and unit/private paths.
Helpers live in application/routing/ (timers, OBP/HBP forward, LC/TA, subscription table).
"""
from __future__ import annotations
import logging
import time
from time import perf_counter
from typing import Any
from .reporting_use_cases import ReportingUseCases
from bitarray import bitarray
from ..domain.dmr import bptc
from ..domain import HBPF_DATA_SYNC, HBPF_SLT_VHEAD, HBPF_SLT_VTERM, STREAM_TO, bytes_3, bytes_4, int_id
from .ports import AclRouter, DmrEmbeddedLcEncoder, SubscriptionStore, TalkerAliasEmblcEncoder
from .talker_alias_use_cases import TalkerAliasUseCases
from .routing.helpers import (
is_private_subscriber_dst,
is_unit_data_ingress,
obp_target_bcsq_quenches_stream,
resolve_voice_peer_id,
unit_data_hbp_target_idle,
)
from .routing.timers import RoutingTimerMixin
from .routing.obp_forward import ObpForwardMixin
from .routing.hbp_forward import HbpForwardMixin
from .routing.lc_ta import LcTaMixin
from .routing.subscription_table import SubscriptionTableMixin
from .routing.store_authority_mixin import StoreAuthorityMixin
from .routing.voice_subscription import VoiceSubscriptionMixin
logger = logging.getLogger(__name__)
class RoutingUseCases(
RoutingTimerMixin,
ObpForwardMixin,
HbpForwardMixin,
LcTaMixin,
SubscriptionTableMixin,
StoreAuthorityMixin,
VoiceSubscriptionMixin,
):
"""Use cases for subscription-based voice routing."""
def __init__(
self,
acl_router: AclRouter,
config: dict[str, Any],
subscription_store: SubscriptionStore,
send_to_system: Any = None,
get_protocols: Any = None,
reporting: ReportingUseCases | None = None,
on_relay_deactivated: Any = None,
send_bcsq: Any = None,
send_dmra_to_system: Any = None,
get_dmra_blocks: Any = None,
call_later: Any = None,
encode_emblc: DmrEmbeddedLcEncoder | None = None,
ta_emblc_encoder: TalkerAliasEmblcEncoder | None = None,
) -> None:
self._acl_router = acl_router
self._config = config
self._subscription_store = subscription_store
self._subscription_router = None
self._routing_table_legacy_view = None
self._send_to_system = send_to_system # (system_name, packet, **kwargs) -> None
self._get_protocols = get_protocols # () -> dict[str, protocol]
self._reporting = reporting
self._on_relay_deactivated = on_relay_deactivated # (system_name: str) -> None; legacy disconnectedVoice
self._send_bcsq = send_bcsq # (system_name, tgid, stream_id) -> None; legacy OBP send_bcsq from router
self._send_dmra_to_system = send_dmra_to_system
self._get_dmra_blocks = get_dmra_blocks
self._call_later = call_later
if encode_emblc is None or ta_emblc_encoder is None:
raise TypeError("encode_emblc and ta_emblc_encoder are required (wire from main.py)")
self._encode_emblc = encode_emblc
self._talker_alias = TalkerAliasUseCases(config, ta_emblc_encoder=ta_emblc_encoder)
# (source_system, stream_id) -> {rf_src, peer, targets, timer}
self._both_ta_wait: dict[tuple[str, bytes], dict[str, Any]] = {}
# Passthrough DMRA/embed relay already applied for this source stream.
self._passthrough_relayed: set[tuple[str, bytes]] = set()
def _send_routing_event(self, event: str | bytes) -> bool:
"""Send BRDG_EVENT via ReportingUseCases when REPORT is enabled."""
if not self._config.get("REPORTS", {}).get("REPORT", True):
return False
if not self._reporting:
return False
if isinstance(event, bytes):
event = event.decode("utf-8", "ignore")
try:
self._reporting.send_routing_event(event)
return True
except Exception:
return False
def _send_routing_table_snapshot(self, *, incremental: bool = True) -> None:
"""Push BRIDGES / routing_table to report clients (monitor SINGLE_TS chips)."""
if not self._config.get("REPORTS", {}).get("REPORT", True):
return
if not self._reporting:
return
try:
self._reporting.send_routing_table(self._routing_table_for_report(), incremental=incremental)
except Exception:
pass
def routing_table_for_report(self) -> dict[str, list[dict[str, Any]]]:
"""Return BRIDGES export for monitor/report only (routing uses ``SubscriptionStore``)."""
return self._routing_table_for_report()
def acl_check(self, id_val: bytes | int, acl: tuple[bool, list[tuple[int, int]]]) -> bool:
"""Check ID against ACL. Legacy acl_check."""
return self._acl_router.acl_check(id_val, acl)
def dmrd_received(
self,
system_name: str,
peer_id: bytes,
rf_src: bytes,
dst_id: bytes,
seq: int,
slot: int,
call_type: str,
frame_type: int,
dtype_vseq: int,
stream_id: bytes,
data: bytes,
*,
ingress_pkt_time: float | None = None,
obp_use_parsed: bool = False,
obp_hops: bytes = b"",
obp_source_server: bytes | None = None,
obp_ber: bytes = b"\x00",
obp_rssi: bytes = b"\x00",
obp_source_rptr: bytes = b"\x00\x00\x00\x00",
) -> bool:
"""Called by UDP when DMRD is received. Forward to other systems in same bridge (to_target).
Returns True if the packet was accepted (passed ingress controls), False/None if dropped.
Legacy `hblink.dmrd_received` passes `_hash,_hops,_source_server,_ber,_rssi,_source_rptr` after
parsing OPENBRIDGE DMRD v1 / DMRE (`hblink.py` ~309–416, ~592–596). When `obp_use_parsed` is True,
the OBP path uses those values (1:1 with `bridge.py` `routerOBP.dmrd_received` → `send_system`).
HBP sources should pass ``ingress_pkt_time`` from UDP receive (legacy single ``pkt_time`` at router entry).
"""
if not self._send_to_system:
return
# Legacy routerHBP.dmrd_received ~3015-3020: log once on first packet after _reset.
# Only applies to HBP sources (routerOBP has no _reset gate).
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
if sys_cfg.get("MODE") != "OPENBRIDGE" and sys_cfg.get("_reset") and not sys_cfg.get("_resetlog"):
logger.info("(%s) disallow transmission until reset cycle is complete", system_name)
sys_cfg["_resetlog"] = True
return
# Legacy bridge_master 3080–3085: private call to ID 4000 only disconnects dynamics; do not route as PC.
if call_type == "unit" and int_id(dst_id) == 4000:
return
if call_type == "unit":
_int_dst = int_id(dst_id)
if dtype_vseq in (6, 7, 8) or (dtype_vseq == 3 and not self._is_stream_known(system_name, stream_id, slot)):
self._unit_data_received(
system_name, peer_id, rf_src, dst_id, seq, slot,
frame_type, dtype_vseq, stream_id, data,
obp_use_parsed=obp_use_parsed, obp_hops=obp_hops,
obp_source_server=obp_source_server,
obp_ber=obp_ber, obp_rssi=obp_rssi, obp_source_rptr=obp_source_rptr,
)
# Legacy routerHBP ~3252-3254: 7-digit routing runs after unit data, not
# instead of it. D-APRS ARS/LRRP downlink (dst 7300392) uses pvt_call_received
# when SUB_MAP sendDataToHBP is blocked by the strict idle check.
if len(str(_int_dst)) == 7:
self._pvt_call_received(
system_name, peer_id, rf_src, dst_id, seq, slot,
frame_type, dtype_vseq, stream_id, data,
)
return True
systems_cfg = self._config.get("SYSTEMS", {})
source_is_obp = systems_cfg.get(system_name, {}).get("MODE") == "OPENBRIDGE"
pkt_time = ingress_pkt_time if ingress_pkt_time is not None else time.time()
if not source_is_obp and call_type in ("group", "vcsbk"):
if not self._hbp_group_voice_ingress_controls(
system_name, peer_id, rf_src, dst_id, seq, slot, stream_id, data, pkt_time,
):
return
# Arm ON in-band rules on VHEAD (echo 9990 and UA bridges); VTERM handled in udp_hbp too.
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
self.apply_in_band_signalling(system_name, slot, dst_id, pkt_time)
from .subscription.subscription_queries import store_has_table
relay_table_key = str(int_id(dst_id))
dst_int = int_id(dst_id)
# Legacy bridge_master to_target: OpenBridge clears TS bit — "all OpenBridge streams are
# effectively on TS1". DMRD v1 rejects slot != 1; DMRE v5 can still set slot 2 from bits.
# BRIDGES entries for OBP use TS:1 (ensure_dynamic_relay / ensure_stat_relay). Match that.
bridge_match_slot = 1 if source_is_obp else slot
if not store_has_table(self._subscription_store, relay_table_key):
if dst_int < 5 or dst_int == 9 or dst_int == 4000 or dst_int == 5000:
logger.debug(
"(ROUTER) No bridge for TG %s (excluded TGID), not creating",
dst_int,
)
elif source_is_obp and self._config.get("GLOBAL", {}).get("GEN_STAT_BRIDGES"):
logger.debug("(%s) Bridge for STAT TG %s does not exist. Creating", system_name, dst_int)
self.ensure_stat_relay(dst_id)
self.apply_static_tg_to_bridge(dst_int)
else:
tmout = self._ua_timer_minutes_for_peer(system_name, peer_id)
logger.info(
"(%s) Bridge for TG %s does not exist. Creating as User Activated. Timeout %s",
system_name, dst_int, tmout,
)
self.ensure_dynamic_relay(dst_id, system_name, slot, float(tmout))
self.apply_static_tg_to_bridge(dst_int)
self._send_routing_table_snapshot(incremental=True)
# Legacy bridge_master routerOBP ~2413-2418: scan every BRIDGES[_bridge] table; forward only within
# a table that contains a matching ACTIVE source row (not a flat merge of TG + #TG only).
dst_id_b = dst_id if isinstance(dst_id, bytes) and len(dst_id) >= 3 else bytes_3(dst_int)
if source_is_obp:
self._ensure_obp_source_for_tg(system_name, relay_table_key, dst_id_b, dst_int)
if source_is_obp and call_type in ("group", "vcsbk"):
if not self._obp_group_voice_router_obp(
system_name,
peer_id,
rf_src,
dst_id,
seq,
slot,
call_type,
frame_type,
dtype_vseq,
stream_id,
data,
obp_hops if obp_use_parsed else b"",
):
return
# SINGLE_MODE in-band VTERM on another TG (e.g. 9990 echo) deactivates static OFF
# legs; re-arm OPTIONS static destinations before forward (inject-only merged lists).
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
self.apply_static_tg_to_bridge(dst_int)
has_source = bool(
self._voice_relay_tables_with_active_source(system_name, bridge_match_slot, dst_int)
)
if not has_source and systems_cfg.get(system_name, {}).get("MODE") == "MASTER":
self.options_config_for_system(system_name)
has_source = bool(
self._voice_relay_tables_with_active_source(system_name, bridge_match_slot, dst_int)
)
# Do not call ensure_dynamic_relay here for 9990–9999 when BRIDGES["9990"] already exists:
# ensure_dynamic_relay replaces the whole table and only sets the source MASTER ACTIVE; every
# other system (including ECHO with TO_TYPE NONE) becomes ACTIVE False — to_target then has
# no active target for the echo path (legacy _seed_echo_routing_table / make_bridges keeps ECHO
# ACTIVE True; activation of the source row is via in-band ON on VTERM, bridge_master ~3465).
if not has_source:
logger.debug(
"(ROUTER) No matching source rule for TG %s from %s slot %s (ACTIVE), not forwarding",
relay_table_key, system_name, bridge_match_slot,
)
return True
# Legacy bridge.py: BRDG_EVENT (OBP group/vcsbk START/END handled in _obp_group_voice_router_obp / post-forward VTERM)
_obp_grp = source_is_obp and call_type in ("group", "vcsbk")
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
if not _obp_grp:
_rx_report_peer = peer_id
if not source_is_obp:
_rx_report_peer = resolve_voice_peer_id(
peer_id,
rf_src,
system_name,
systems_cfg,
)
self._send_routing_event(
"GROUP VOICE,START,RX,{},{},{},{},{},{}".format(
system_name,
int_id(stream_id),
int_id(_rx_report_peer),
int_id(rf_src),
slot,
int_id(dst_id),
)
)
elif frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VTERM:
if not _obp_grp:
duration = 0.0
protocols = self._get_protocols() if self._get_protocols else {}
src_proto = protocols.get(system_name) if protocols else None
if src_proto and getattr(src_proto, "STATUS", None):
st = src_proto.STATUS
ent = st.get(stream_id)
start = ent.get("START") if isinstance(ent, dict) else None
if start is None and slot in st:
start = st.get(slot, {}).get("RX_START")
if start is not None:
duration = pkt_time - start
_rx_report_peer = peer_id
if not source_is_obp:
_rx_report_peer = resolve_voice_peer_id(
peer_id,
rf_src,
system_name,
systems_cfg,
)
self._send_routing_event(
"GROUP VOICE,END,RX,{},{},{},{},{},{},{:.2f}".format(
system_name,
int_id(stream_id),
int_id(_rx_report_peer),
int_id(rf_src),
slot,
int_id(dst_id),
duration,
)
)
# ── Exact port of legacy bridge.py routerOBP/routerHBP forwarding to targets ──
pkt_time = time.time()
dmrpkt = data[20:53] if len(data) >= 53 else b""
_bits = data[15] if len(data) > 15 else 0
protocols = self._get_protocols() if self._get_protocols else {}
src_proto = protocols.get(system_name) if protocols else None
# Legacy `bridge.py` `routerOBP.dmrd_received` forwards `_hops,_ber,_rssi,_source_server,_source_rptr`
# exactly as received from `hblink` (`bridge.py` ~486). Values are set in `hblink`:
# - DMRD v1 OBP: SERVER_ID, zeros rptr, empty hops (`hblink.py` ~338–345)
# - DMRE v5: packet fields + incremented hops (`hblink.py` ~592–596)
# Legacy `bridge_master` `routerHBP.dmrd_received`: ber/rssi from payload, SERVER_ID + peer as rptr (~2936–2940).
if source_is_obp and obp_use_parsed:
_hops = obp_hops
_ber = obp_ber
_rssi = obp_rssi
_sid = self._config.get("GLOBAL", {}).get("SERVER_ID")
if obp_source_server is not None:
_source_server = obp_source_server
elif isinstance(_sid, bytes) and len(_sid) >= 4:
_source_server = _sid
elif _sid is not None:
_source_server = bytes_4(int(_sid) & 0xFFFFFFFF)
else:
_source_server = b"\x00\x00\x00\x00"
_source_rptr = obp_source_rptr
elif source_is_obp:
_ber = b"\x00"
_rssi = b"\x00"
_hops = b""
_sid = self._config.get("GLOBAL", {}).get("SERVER_ID")
if isinstance(_sid, bytes) and len(_sid) >= 4:
_source_server = _sid
elif _sid is not None:
_source_server = bytes_4(int(_sid) & 0xFFFFFFFF)
else:
_source_server = b"\x00\x00\x00\x00"
_source_rptr = b"\x00\x00\x00\x00"
else:
_ber = data[53:54] if len(data) > 53 else b"\x00"
_rssi = data[54:55] if len(data) > 54 else b"\x00"
_hops = b""
_sid = self._config.get("GLOBAL", {}).get("SERVER_ID")
if isinstance(_sid, bytes) and len(_sid) >= 4:
_source_server = _sid
elif _sid is not None:
_source_server = bytes_4(int(_sid) & 0xFFFFFFFF)
else:
_source_server = b"\x00\x00\x00\x00"
_source_rptr = peer_id
# Legacy: source LC — routerOBP uses self.STATUS[_stream_id]['LC'], routerHBP uses self.STATUS[_slot]['RX_LC']
source_lc = None
if src_proto and getattr(src_proto, "STATUS", None):
st = src_proto.STATUS
if source_is_obp:
ent = st.get(stream_id)
source_lc = ent.get("LC") if isinstance(ent, dict) else None
else:
source_lc = st.get(slot, {}).get("RX_LC")
if not source_lc or len(source_lc) < 9:
source_lc = b"\x00\x00\x20" + dst_id_b + rf_src
# Legacy bridge_master routerOBP: _sysIgnore accumulates across each to_target(BRIDGES[_bridge])
# pass; dedupe (SYSTEM, TS) for OpenBridge targets so the same leg is not sent twice per packet.
# SubscriptionRouter.resolve() already applies OBP dedup on OpenBridge targets.
forward_tables, forward_legs = self._voice_forward_plan(
system_name=system_name,
peer_id=peer_id,
rf_src=rf_src,
dst_id=dst_id,
slot=slot,
call_type=call_type,
stream_id=stream_id,
source_is_obp=source_is_obp,
bridge_match_slot=bridge_match_slot,
dst_int=dst_int,
)
forwarded = []
_leg_iter: list[tuple[str, dict[str, Any]]] = [
(
forward_tables[0] if forward_tables else str(dst_int),
{
"SYSTEM": leg.target_system,
"TS": int(leg.slot),
"TGID": bytes_3(int(leg.target_tgid)),
"ACTIVE": True,
},
)
for leg in forward_legs
]
for _relay_table_key, entry in _leg_iter:
if not systems_cfg.get(entry["SYSTEM"], {}).get("ENABLED", True):
continue
_target_system = systems_cfg.get(entry["SYSTEM"], {})
target_mode = _target_system.get("MODE")
tgt_proto = protocols.get(entry["SYSTEM"]) if protocols else None
_target_status = getattr(tgt_proto, "STATUS", None) if tgt_proto else None
if target_mode == "OPENBRIDGE":
# ── bridge_master.routerOBP.to_target OPENBRIDGE branch (~1850-1959) ──
entry_ts_obp = entry.get("TS")
if entry_ts_obp is None:
entry_ts_obp = 1
target_tgid = entry.get("TGID")
if isinstance(target_tgid, int):
target_tgid = bytes_3(target_tgid)
# If target has quenched us, don't send (~1856-1859).
if obp_target_bcsq_quenches_stream(systems_cfg, entry["SYSTEM"], dst_id_b, stream_id):
continue
# If target has missed keepalives (ENHANCED_OBP), don't send (~1861-1863)
if _target_system.get("ENHANCED_OBP") and (
"_bcka" not in _target_system or _target_system["_bcka"] < pkt_time - 60
):
continue
# Talkgroup ACL (global + per-system TG1) (~1865-1873)
_global_cfg = self._config.get("GLOBAL", {})
if _global_cfg.get("USE_ACL"):
if not self.acl_check(target_tgid, _global_cfg.get("TG1_ACL", (True, []))):
continue
if not self.acl_check(target_tgid, _target_system.get("TG1_ACL", (True, []))):
continue
if _target_status is not None:
if stream_id not in _target_status:
_target_status[stream_id] = {
"START": pkt_time,
"CONTENTION": False,
"RFS": rf_src,
"TGID": dst_id_b,
"RX_PEER": peer_id,
"EMB_LC": {1: b"\x00", 2: b"\x00", 3: b"\x00", 4: b"\x00"},
"H_LC": b"\x00",
"T_LC": b"\x00",
}
try:
dst_lc = source_lc[0:3] + target_tgid + rf_src
except Exception:
logger.exception("(to_target) caught exception")
_target_status[stream_id]["LAST"] = pkt_time
return
_target_status[stream_id]["H_LC"] = bptc.encode_header_lc(dst_lc)
_target_status[stream_id]["T_LC"] = bptc.encode_terminator_lc(dst_lc)
_target_status[stream_id]["EMB_LC"] = self._encode_emblc(dst_lc)
self._init_talker_alias_embed(
_target_status[stream_id],
system_name,
entry["SYSTEM"],
rf_src,
stream_id,
)
logger.debug(
"(%s) Conference Bridge: %s, Call Bridged to OBP System: %s TS: %s, TGID: %s",
system_name, _relay_table_key, entry["SYSTEM"], entry.get("TS", 1), int_id(target_tgid),
)
self._send_routing_event(
"GROUP VOICE,START,TX,{},{},{},{},{},{}".format(
entry["SYSTEM"], int_id(stream_id), int_id(peer_id), int_id(rf_src), entry.get("TS", 1), int_id(target_tgid)
)
)
if "EMB_LC" not in _target_status[stream_id]:
try:
dst_lc = source_lc[0:3] + target_tgid + rf_src
_target_status[stream_id]["EMB_LC"] = self._encode_emblc(dst_lc)
except Exception:
logger.exception("(to_target) caught exception while creating EMB_LC")
return
if "H_LC" not in _target_status[stream_id]:
try:
dst_lc = source_lc[0:3] + target_tgid + rf_src
_target_status[stream_id]["H_LC"] = bptc.encode_header_lc(dst_lc)
except Exception:
logger.exception("(to_target) caught exception while creating H_LC")
return
if "T_LC" not in _target_status[stream_id]:
try:
dst_lc = source_lc[0:3] + target_tgid + rf_src
_target_status[stream_id]["T_LC"] = bptc.encode_terminator_lc(dst_lc)
except Exception:
logger.exception("(to_target) caught exception while creating T_LC")
return
_target_status[stream_id]["LAST"] = pkt_time
_tmp_bits = _bits & ~(1 << 7)
_tmp_data = b"".join([data[:8], target_tgid, data[11:15], bytes([_tmp_bits]), data[16:20]])
dmrbits = bitarray(endian="big")
dmrbits.frombytes(dmrpkt)
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
dmrbits = _target_status[stream_id]["H_LC"][0:98] + dmrbits[98:166] + _target_status[stream_id]["H_LC"][98:197]
elif frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VTERM:
dmrbits = _target_status[stream_id]["T_LC"][0:98] + dmrbits[98:166] + _target_status[stream_id]["T_LC"][98:197]
self._clear_talker_alias_embed(_target_status[stream_id])
call_duration = pkt_time - _target_status[stream_id].get("START", pkt_time)
self._send_routing_event(
"GROUP VOICE,END,TX,{},{},{},{},{},{},{:.2f}".format(
entry["SYSTEM"], int_id(stream_id), int_id(peer_id), int_id(rf_src), entry.get("TS", 1), int_id(target_tgid), call_duration
)
)
elif dtype_vseq in (1, 2, 3, 4):
self._rewrite_embed_lc(
dmrbits, _target_status[stream_id], dtype_vseq, "EMB_LC",
)
dmrpkt_out = dmrbits.tobytes()
_tmp_data = b"".join([_tmp_data, dmrpkt_out])
else:
_tmp_bits = _bits & ~(1 << 7)
_tmp_data = b"".join([data[:8], target_tgid, data[11:15], bytes([_tmp_bits]), data[16:20]])
_tmp_data = b"".join([_tmp_data, dmrpkt])
try:
self._send_to_system(
entry["SYSTEM"], _tmp_data,
_hops=_hops, _ber=_ber, _rssi=_rssi,
_source_server=_source_server, _source_rptr=_source_rptr,
)
forwarded.append(entry["SYSTEM"])
except Exception as e:
logger.warning("(ROUTER) send_to_system %s failed: %s", entry.get("SYSTEM"), e)
else:
# ── Exact port of legacy bridge.py HBP target (lines 403-486 / 720-803) ──
entry_ts = entry.get("TS")
if entry_ts is None:
continue
entry_tgid_b = entry.get("TGID")
if isinstance(entry_tgid_b, int):
entry_tgid_b = bytes_3(entry_tgid_b)
if _target_status is None or entry_ts not in _target_status:
continue
_ts_st = _target_status[entry_ts]
if source_is_obp:
_src_stream_st = getattr(src_proto, "STATUS", {}).get(stream_id, {}) if src_proto else {}
else:
_src_stream_st = getattr(src_proto, "STATUS", {}).get(slot, {}) if src_proto else {}
# Contention handling (exact port of legacy bridge_master.py ~2056-2075)
if (entry_tgid_b != _ts_st.get("RX_TGID", b"\x00\x00\x00")) and ((pkt_time - _ts_st.get("RX_TIME", 0)) < float(_target_system.get("GROUP_HANGTIME", 0))):
if not _src_stream_st.get("CONTENTION"):
_src_stream_st["CONTENTION"] = True
logger.info("(%s) Call not routed to TGID %s, target active or in group hangtime: HBSystem: %s, TS: %s, TGID: %s", system_name, int_id(entry_tgid_b), entry.get("SYSTEM"), entry_ts, int_id(_ts_st.get("RX_TGID", b"")))
continue
if (entry_tgid_b != _ts_st.get("TX_TGID", b"\x00\x00\x00")) and ((pkt_time - _ts_st.get("TX_TIME", 0)) < float(_target_system.get("GROUP_HANGTIME", 0))):
if not _src_stream_st.get("CONTENTION"):
_src_stream_st["CONTENTION"] = True
logger.info("(%s) Call not routed to TGID %s, target in group hangtime: HBSystem: %s, TS: %s, TGID: %s", system_name, int_id(entry_tgid_b), entry.get("SYSTEM"), entry_ts, int_id(_ts_st.get("TX_TGID", b"")))
continue
if (entry_tgid_b == _ts_st.get("RX_TGID", b"\x00\x00\x00")) and ((pkt_time - _ts_st.get("RX_TIME", 0)) < STREAM_TO):
if not _src_stream_st.get("CONTENTION"):
_src_stream_st["CONTENTION"] = True
logger.info("(%s) Call not routed to TGID %s, matching call already active on target: HBSystem: %s, TS: %s, TGID: %s", system_name, int_id(entry_tgid_b), entry.get("SYSTEM"), entry_ts, int_id(_ts_st.get("RX_TGID", b"")))
continue
if (entry_tgid_b == _ts_st.get("TX_TGID", b"\x00\x00\x00")) and (rf_src != _ts_st.get("TX_RFS", b"")) and ((pkt_time - _ts_st.get("TX_TIME", 0)) < STREAM_TO):
if not _src_stream_st.get("CONTENTION"):
_src_stream_st["CONTENTION"] = True
logger.info("(%s) Call not routed for subscriber %s, call route in progress on target: HBSystem: %s, TS: %s, TGID: %s, SUB: %s", system_name, int_id(rf_src), entry.get("SYSTEM"), entry_ts, int_id(_ts_st.get("TX_TGID", b"")), int_id(_ts_st.get("TX_RFS", b"")))
continue
# New stream detection — legacy OBP uses _target_status[TS]['TX_STREAM_ID'], legacy HBP uses self.STATUS[_slot]['RX_STREAM_ID']
if source_is_obp:
_is_new_stream = (_ts_st.get("TX_STREAM_ID") != stream_id)
else:
src_status = getattr(src_proto, "STATUS", None) if src_proto else None
_is_new_stream = (src_status.get(slot, {}).get("RX_STREAM_ID") if src_status else b"") != stream_id
if _is_new_stream:
_ts_st["TX_START"] = pkt_time
_ts_st["TX_TGID"] = entry_tgid_b
_ts_st["TX_STREAM_ID"] = stream_id
_ts_st["TX_RFS"] = rf_src
_ts_st["TX_PEER"] = peer_id
dst_lc = source_lc[0:3] + entry_tgid_b + rf_src
_ts_st["TX_H_LC"] = bptc.encode_header_lc(dst_lc)
_ts_st["TX_T_LC"] = bptc.encode_terminator_lc(dst_lc)
_ts_st["TX_EMB_LC"] = self._encode_emblc(dst_lc)
self._dispatch_talker_alias_on_bridge_open(
_ts_st,
system_name,
entry["SYSTEM"],
rf_src,
stream_id,
peer_id,
)
logger.info(
"(%s) Conference Bridge: %s, Call Bridged to HBP System: %s TS: %s, TGID: %s",
system_name, _relay_table_key, entry["SYSTEM"], entry_ts, int_id(entry_tgid_b),
)
self._send_routing_event(
"GROUP VOICE,START,TX,{},{},{},{},{},{}".format(
entry["SYSTEM"],
int_id(stream_id),
int_id(peer_id),
int_id(rf_src),
entry_ts,
int_id(entry_tgid_b),
)
)
_ts_st["TX_TIME"] = pkt_time
_ts_st["TX_TYPE"] = dtype_vseq
# Slot bit rewrite (legacy bridge.py 457-460 / 770-773)
_src_entry_ts = slot
if _src_entry_ts != entry_ts:
_tmp_bits = _bits ^ (1 << 7)
else:
_tmp_bits = _bits
_tmp_data = b"".join([data[:8], entry_tgid_b, data[11:15], bytes([_tmp_bits]), data[16:20]])
# LC rewrite (exact port of legacy bridge.py 468-482 / 781-799)
dmrbits = bitarray(endian="big")
dmrbits.frombytes(dmrpkt)
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
dmrbits = _ts_st["TX_H_LC"][0:98] + dmrbits[98:166] + _ts_st["TX_H_LC"][98:197]
elif frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VTERM:
dmrbits = _ts_st["TX_T_LC"][0:98] + dmrbits[98:166] + _ts_st["TX_T_LC"][98:197]
call_duration = pkt_time - _ts_st.get("TX_START", pkt_time)
_end_peer = _ts_st.get("TX_PEER", peer_id)
self._send_routing_event(
"GROUP VOICE,END,TX,{},{},{},{},{},{},{:.2f}".format(
entry["SYSTEM"],
int_id(stream_id),
int_id(_end_peer),
int_id(rf_src),
entry_ts,
int_id(entry_tgid_b),
call_duration,
)
)
elif dtype_vseq in (1, 2, 3, 4):
self._rewrite_embed_lc(
dmrbits, _ts_st, dtype_vseq, "TX_EMB_LC",
)
dmrpkt_out = dmrbits.tobytes()
# bridge_master.routerOBP.to_target HBP branch: _tmp_data + dmrpkt only (~2041-2042);
# HBP source adds BER/RSSI from payload (bridge.py routerHBP ~800).
if source_is_obp:
_tmp_data = b"".join([_tmp_data, dmrpkt_out])
else:
_tmp_data = b"".join([_tmp_data, dmrpkt_out, data[53:55]])
try:
self._send_to_system(
entry["SYSTEM"], _tmp_data,
_hops=_hops, _ber=_ber, _rssi=_rssi,
_source_server=_source_server, _source_rptr=_source_rptr,
)
forwarded.append(entry["SYSTEM"])
except Exception as e:
logger.warning("(ROUTER) send_to_system %s failed: %s", entry.get("SYSTEM"), e)
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VTERM:
self._clear_talker_alias_embed(_ts_st)
self._talker_alias.clear_stream(entry["SYSTEM"], stream_id)
# Legacy bridge_master routerOBP ~2420-2434: after to_target, VTERM — CALL END log, END RX report, _fin, lastSeq
if (
source_is_obp
and call_type in ("group", "vcsbk")
and frame_type == HBPF_DATA_SYNC
and dtype_vseq == HBPF_SLT_VTERM
):
_src_p = protocols.get(system_name) if protocols else None
# Legacy parity (bridge_master.py:1911): routerOBP.STATUS is the flat dict.
_obp_st = getattr(_src_p, "STATUS", None) if _src_p else None
_ost_candidate = _obp_st.get(stream_id) if isinstance(_obp_st, dict) else None
if isinstance(_ost_candidate, dict):
ost = _ost_candidate
_end_t = time.time()
call_duration = _end_t - ost.get("START", _end_t)
packet_rate = (ost.get("packets", 0) / call_duration) if call_duration else 0.0
loss_pct = ((ost.get("loss", 0) / ost["packets"]) * 100) if ost.get("packets") else 0.0
logger.info(
"(%s) *CALL END* STREAM ID: %s SUB: %s PEER: %s TGID %s, TS %s, Duration: %.2f, Packet rate: %.2f/s, Loss: %.2f%%",
system_name,
int_id(stream_id),
int_id(rf_src),
int_id(peer_id),
int_id(dst_id),
slot,
call_duration,
packet_rate,
loss_pct,
)
self._send_routing_event(
"GROUP VOICE,END,RX,{},{},{},{},{},{},{:.2f}".format(
system_name,
int_id(stream_id),
int_id(peer_id),
int_id(rf_src),
slot,
int_id(dst_id),
call_duration,
)
)
ost["_fin"] = True
self._obp_emit_end_tx_for_forward_legs(stream_id, system_name, _end_t)
ost["lastSeq"] = False
if forwarded:
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD:
logger.info(
"(ROUTER) Bridged TG %s from %s -> %s",
relay_table_key, system_name, ", ".join(forwarded),
)
return True
# ── Unit DATA path (SMS, GPS, CSBK) — legacy routerOBP/routerHBP unit data branch ──
def _log_unit_data_hbp_busy(
self,
system_name: str,
d_system: str,
d_slot: int,
int_dst_id: int,
dst_slot: dict,
d_sys_cfg: dict,
pkt_time: float,
) -> None:
"""Legacy logs busy at debug only; include slot state for D-APRS downlink triage."""
logger.info(
"(%s) UNIT Data not bridged to HBP - target busy: %s slot %s DST_ID: %s "
"(RX_TYPE=%s TX_TYPE=%s RX_TGID=%s TX_TGID=%s TX_age=%.2fs hangtime=%s)",
system_name,
d_system,
d_slot,
int_dst_id,
dst_slot.get("RX_TYPE"),
dst_slot.get("TX_TYPE"),
int_id(dst_slot.get("RX_TGID", b"\x00\x00\x00")),
int_id(dst_slot.get("TX_TGID", b"\x00\x00\x00")),
pkt_time - dst_slot.get("TX_TIME", 0),
d_sys_cfg.get("GROUP_HANGTIME", 5),
)
def _unit_data_received(
self,
system_name: str,
peer_id: bytes,
rf_src: bytes,
dst_id: bytes,
seq: int,
slot: int,
frame_type: int,
dtype_vseq: int,
stream_id: bytes,
data: bytes,
*,
obp_use_parsed: bool = False,
obp_hops: bytes = b"",
obp_source_server: bytes | None = None,
obp_ber: bytes = b"\x00",
obp_rssi: bytes = b"\x00",
obp_source_rptr: bytes = b"\x00\x00\x00\x00",
) -> None:
"""Legacy routerOBP/routerHBP unit data branch: DATA-GATEWAY + OBP fan-out + SUB_MAP/hotspot."""
pkt_time = time.time()
dmrpkt = data[20:53] if len(data) >= 53 else b""
_bits = data[15] if len(data) > 15 else 0
_int_dst_id = int_id(dst_id)
systems_cfg = self._config.get("SYSTEMS", {})
source_is_obp = systems_cfg.get(system_name, {}).get("MODE") == "OPENBRIDGE"
global_cfg = self._config.get("GLOBAL", {})
if source_is_obp and obp_source_server is not None:
_source_server = obp_source_server
else:
_sid = global_cfg.get("SERVER_ID", b"\x00\x00\x00\x00")
if isinstance(_sid, bytes) and len(_sid) >= 4:
_source_server = _sid
elif _sid is not None:
_source_server = bytes_4(int(_sid) & 0xFFFFFFFF)
else:
_source_server = b"\x00\x00\x00\x00"
_source_rptr = peer_id if not source_is_obp else obp_source_rptr
if source_is_obp:
_hops = obp_hops
_ber = obp_ber
_rssi = obp_rssi
else:
_hops = b""
_ber = data[53:54] if len(data) > 53 else b"\x00"
_rssi = data[54:55] if len(data) > 54 else b"\x00"
# OBP unit-data loop control (legacy routerOBP ~2201-2255)
if source_is_obp:
protocols = self._get_protocols() if self._get_protocols else {}
src_proto = protocols.get(system_name)
if src_proto:
status = getattr(src_proto, "STATUS", None)
if status is not None:
if stream_id not in status:
status[stream_id] = {
"START": pkt_time, "CONTENTION": False, "RFS": rf_src,
"TGID": dst_id, "1ST": perf_counter(), "lastSeq": False,
"lastData": False, "RX_PEER": peer_id, "packets": 0, "crcs": set(),
}
status[stream_id]["LAST"] = pkt_time
status[stream_id]["packets"] = status[stream_id].get("packets", 0) + 1
# HBP loop check: if any HBP already has this RX_STREAM_ID, drop
for other_name, proto in protocols.items():
omode = systems_cfg.get(other_name, {}).get("MODE")
if other_name != system_name and omode != "OPENBRIDGE":
ostatus = getattr(proto, "STATUS", None)
if not ostatus:
continue
for _sysslot in ostatus:
slot_st = ostatus.get(_sysslot)
if isinstance(slot_st, dict) and stream_id == slot_st.get("RX_STREAM_ID"):
if not status[stream_id].get("LOOPLOG"):
logger.debug(
"(%s) OBP UNIT *LoopControl* FIRST HBP: %s, STREAM ID: %s, TG: %s, TS: %s, IGNORE THIS SOURCE",
system_name, other_name, int_id(stream_id), int_id(dst_id), _sysslot,
)
status[stream_id]["LOOPLOG"] = True
return
# OBP earliest-wins: compare 1ST across OBP systems
hr_times: dict[str, float] = {}
for other_name, proto in protocols.items():
omode = systems_cfg.get(other_name, {}).get("MODE")
if omode == "OPENBRIDGE":
obp_st = getattr(proto, "STATUS", None)
if obp_st and stream_id in obp_st and "1ST" in obp_st[stream_id]:
if obp_st[stream_id].get("TGID") == dst_id:
hr_times[other_name] = obp_st[stream_id]["1ST"]
fi = min(hr_times, key=hr_times.get, default=False)
if not fi:
logger.warning(
"(%s) OBP UNIT *LoopControl* fi is empty: STREAM ID: %s, TG: %s",
system_name, int_id(stream_id), int_id(dst_id),
)
return
if system_name != fi:
if not status[stream_id].get("LOOPLOG"):
logger.debug(
"(%s) OBP UNIT *LoopControl* FIRST OBP %s, STREAM ID: %s, TG %s, IGNORE THIS SOURCE",
system_name, fi, int_id(stream_id), int_id(dst_id),
)
status[stream_id]["LOOPLOG"] = True
return
# Dtype-specific logging (legacy routerOBP ~2257-2280, routerHBP ~3052-3082)
if dtype_vseq == 3:
logger.info(
"(%s) *UNIT CSBK* STREAM ID: %s SUB: %s PEER: %s DST_ID %s TS %s",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), _int_dst_id, slot,
)
elif dtype_vseq == 6:
logger.info(
"(%s) *UNIT DATA HEADER* STREAM ID: %s SUB: %s PEER: %s DST_ID %s TS %s",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), _int_dst_id, slot,
)
elif dtype_vseq == 7:
logger.info(
"(%s) *UNIT VCSBK 1/2 DATA BLOCK* STREAM ID: %s SUB: %s PEER: %s TGID %s TS %s",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), _int_dst_id, slot,
)
elif dtype_vseq == 8:
logger.info(
"(%s) *UNIT VCSBK 3/4 DATA BLOCK* STREAM ID: %s SUB: %s PEER: %s TGID %s TS %s",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), _int_dst_id, slot,
)
else:
logger.info(
"(%s) *UNKNOWN DATA TYPE* STREAM ID: %s SUB: %s PEER: %s TGID %s TS %s",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), _int_dst_id, slot,
)
_dtype_labels = {3: "UNIT CSBK", 6: "UNIT DATA HEADER", 7: "UNIT VCSBK 1/2 DATA BLOCK", 8: "UNIT VCSBK 3/4 DATA BLOCK"}
_label = _dtype_labels.get(dtype_vseq, "UNIT DATA")
self._send_routing_event(
"{},DATA,RX,{},{},{},{},{},{}".format(
_label, system_name, int_id(stream_id), int_id(peer_id), int_id(rf_src), slot, _int_dst_id,
)
)
# DATA-GATEWAY forwarding (legacy ~2281-2284 / ~3083-3087)
if global_cfg.get("DATA_GATEWAY"):
dg_cfg = systems_cfg.get("DATA-GATEWAY", {})
if dg_cfg.get("MODE") == "OPENBRIDGE" and dg_cfg.get("ENABLED"):
logger.debug("(%s) DATA packet sent to DATA-GATEWAY", system_name)
self._send_data_to_obp(
system_name, "DATA-GATEWAY", data, dmrpkt, pkt_time, stream_id,
dst_id, peer_id, rf_src, _bits, slot,
hops=_hops, ber=_ber, rssi=_rssi,
source_server=_source_server, source_rptr=_source_rptr,
)
# Fan-out to all OBP systems with VER > 1 and dst_id >= 1000000 (legacy ~2286-2295 / ~3088-3097)
protocols = self._get_protocols() if self._get_protocols else {}
for sys_name, sys_cfg in systems_cfg.items():
if sys_name == system_name:
continue
if sys_name == "DATA-GATEWAY":
continue
if sys_cfg.get("MODE") == "OPENBRIDGE" and sys_cfg.get("VER", 1) > 1 and _int_dst_id >= 1000000:
self._send_data_to_obp(
system_name, sys_name, data, dmrpkt, pkt_time, stream_id,
dst_id, peer_id, rf_src, _bits, slot,
hops=_hops, ber=_ber, rssi=_rssi,
source_server=_source_server, source_rptr=_source_rptr,
)
# SUB_MAP lookup (legacy ~2297-2312 / ~3099-3114)
sub_map = self._config.get("_SUB_MAP", {})
if dst_id in sub_map:
_d_system, _d_slot, _d_time = sub_map[dst_id]
_d_proto = protocols.get(_d_system)
if _d_proto:
_dst_slot = getattr(_d_proto, "STATUS", {}).get(_d_slot, {})
logger.info("(%s) SUB_MAP matched, System: %s Slot: %s, Time: %s", system_name, _d_system, _d_slot, _d_time)
_d_sys_cfg = systems_cfg.get(_d_system, {})
_hangtime = _d_sys_cfg.get("GROUP_HANGTIME", 5)
if unit_data_hbp_target_idle(_dst_slot, pkt_time, _hangtime):
_tmp_bits = _bits ^ (1 << 7) if slot != _d_slot else _bits
self._send_data_to_hbp(system_name, _d_system, _d_slot, dst_id, _tmp_bits, data, dmrpkt, rf_src, stream_id, peer_id)
elif not is_private_subscriber_dst(dst_id):
self._log_unit_data_hbp_busy(
system_name, _d_system, _d_slot, _int_dst_id, _dst_slot, _d_sys_cfg, pkt_time,
)
else:
# Hotspot 6/7-digit peer ID match (legacy ~3131-3168 / ~2314-2345)
for _d_system, _d_sys_cfg in systems_cfg.items():
if _d_sys_cfg.get("MODE") != "MASTER":
continue
_d_proto = protocols.get(_d_system)
if not _d_proto:
continue
_peers = _d_sys_cfg.get("PEERS", {})
_matched = False
for _to_peer in _peers:
_int_to_peer = int_id(_to_peer)
_dst_str = str(_int_dst_id)
_to_str = str(_int_to_peer)
if len(_dst_str) == 6:
if _to_str[:6] == _dst_str:
_d_slot = 2
_dst_slot = getattr(_d_proto, "STATUS", {}).get(_d_slot, {})
logger.info("(%s) User Peer Hotspot ID (6-digit) matched, System: %s Slot: %s", system_name, _d_system, _d_slot)
_hangtime = _d_sys_cfg.get("GROUP_HANGTIME", 5)
if unit_data_hbp_target_idle(_dst_slot, pkt_time, _hangtime):
_tmp_bits = _bits ^ (1 << 7) if slot != 2 else _bits
self._send_data_to_hbp(system_name, _d_system, _d_slot, dst_id, _tmp_bits, data, dmrpkt, rf_src, stream_id, peer_id)
elif not is_private_subscriber_dst(dst_id):
self._log_unit_data_hbp_busy(
system_name, _d_system, _d_slot, _int_dst_id, _dst_slot, _d_sys_cfg, pkt_time,
)
_matched = True
break
elif len(_dst_str) >= 7:
if _to_str[:7] == _dst_str[:7]:
_d_slot = 2
_dst_slot = getattr(_d_proto, "STATUS", {}).get(_d_slot, {})
logger.info("(%s) User Peer Hotspot ID (7-digit) matched, System: %s Slot: %s", system_name, _d_system, _d_slot)
_hangtime = _d_sys_cfg.get("GROUP_HANGTIME", 5)
if unit_data_hbp_target_idle(_dst_slot, pkt_time, _hangtime):
_tmp_bits = _bits ^ (1 << 7) if slot != 2 else _bits
self._send_data_to_hbp(system_name, _d_system, _d_slot, dst_id, _tmp_bits, data, dmrpkt, rf_src, stream_id, peer_id)
elif not is_private_subscriber_dst(dst_id):
self._log_unit_data_hbp_busy(
system_name, _d_system, _d_slot, _int_dst_id, _dst_slot, _d_sys_cfg, pkt_time,
)
_matched = True
break
if _matched:
break
def _pvt_call_received(
self,
system_name: str,
peer_id: bytes,
rf_src: bytes,
dst_id: bytes,
seq: int,
slot: int,
frame_type: int,
dtype_vseq: int,
stream_id: bytes,
data: bytes,
) -> None:
"""Legacy pvt_call_received: route private (unit) calls via SUB_MAP lookup."""
pkt_time = time.time()
dmrpkt = data[20:53] if len(data) >= 53 else b""
_bits = data[15] if len(data) > 15 else 0
sub_map = self._config.get("_SUB_MAP", {})
if sub_map is not None:
sub_map[rf_src] = (system_name, slot, pkt_time)
systems_cfg = self._config.get("SYSTEMS", {})
protocols = self._get_protocols() if self._get_protocols else {}
source_proto = protocols.get(system_name)
source_status = getattr(source_proto, "STATUS", {}) if source_proto else {}
if source_proto:
source_status.setdefault(slot, {})
slot_st = source_status[slot] if source_proto and slot in source_status else {}
_unit_data = is_unit_data_ingress(
"unit", dtype_vseq, stream_id, slot_st.get("RX_STREAM_ID"),
)
if stream_id != slot_st.get("RX_STREAM_ID"):
if (slot_st.get("RX_TYPE") != HBPF_SLT_VTERM) and (pkt_time < (slot_st.get("RX_TIME", 0) + STREAM_TO)) and (rf_src != slot_st.get("RX_RFS")):
logger.warning(
"(%s) PRIVATE CALL Packet received with STREAM ID: %s <FROM> SUB: %s PEER: %s <TO> UNIT %s, SLOT %s collided with existing call",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), int_id(dst_id), slot,
)
return
slot_st["RX_START"] = pkt_time
if dst_id in sub_map:
if sub_map[dst_id][0] != system_name:
self._pvt_targets = [sub_map[dst_id][0]]
else:
self._pvt_targets = []
logger.error("PRIVATE call to a subscriber on the same system, send nothing")
else:
self._pvt_targets = []
logger.info(
"(%s) *PRIVATE CALL START* STREAM ID: %s SUB: %s PEER: %s DST: %s, TS: %s, FORWARD: %s",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), int_id(dst_id), slot, self._pvt_targets,
)
# Unit data (ARS/LRRP/SMS) has no VTERM; PRIVATE VOICE START without END leaves
# monitor timeslot chips stuck. UNIT DATA events cover lastheard instead.
if not _unit_data:
self._send_routing_event(
"PRIVATE VOICE,START,RX,{},{},{},{},{},{}".format(
system_name, int_id(stream_id), int_id(peer_id), int_id(rf_src), slot, int_id(dst_id)
)
)
for _target in getattr(self, "_pvt_targets", []):
target_proto = protocols.get(_target)
if not target_proto:
continue
_target_status = getattr(target_proto, "STATUS", {})
_target_system = systems_cfg.get(_target, {})
if _target_system.get("MODE") == "OPENBRIDGE":
if _target_system.get("ENHANCED_OBP") and "_bcka" in _target_system and _target_system["_bcka"] < pkt_time - 60:
continue
if stream_id not in _target_status:
_target_status[stream_id] = {
"START": pkt_time,
"CONTENTION": False,
"RFS": rf_src,
"TYPE": "UNIT",
"DST": dst_id,
"ACTIVE": True,
}
logger.info(
"(%s) PRIVATE call bridged to OBP System: %s TS: %s, UNIT: %s",
system_name, _target, slot if _target_system.get("BOTH_SLOTS") else 1, int_id(dst_id),
)
if not _unit_data:
self._send_routing_event(
"PRIVATE VOICE,START,TX,{},{},{},{},{},{}".format(
_target, int_id(stream_id), int_id(peer_id), int_id(rf_src), slot, int_id(dst_id),
).encode("utf-8", "ignore")
)
_target_status[stream_id]["LAST"] = pkt_time
if _target_system.get("BOTH_SLOTS"):
_tmp_bits = _bits
else:
_tmp_bits = _bits & ~(1 << 7)
_tmp_data = b"".join([data[:15], _tmp_bits.to_bytes(1, "big"), data[16:20]])
send_data = b"".join([_tmp_data, dmrpkt])
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VTERM:
_target_status[stream_id]["ACTIVE"] = False
else:
ts_st = _target_status.get(slot, {})
# Voice private-call contention only. Unit data (ARS/LRRP/SMS) must
# pierce group/voice RX on the target slot — legacy pvt_call still
# sends via send_system even when sendDataToHBP is busy.
if not _unit_data:
if (dst_id == ts_st.get("RX_TGID")) and ((pkt_time - ts_st.get("RX_TIME", 0)) < STREAM_TO):
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD and slot_st.get("RX_STREAM_ID") != stream_id:
logger.info(
"(%s) PRIVATE Call not routed to destination %s, matching call already active on target: HBSystem: %s, TS: %s, DEST: %s",
system_name, int_id(dst_id), _target, slot, int_id(ts_st.get("RX_TGID", b"")),
)
continue
if (dst_id == ts_st.get("TX_TGID")) and (rf_src != ts_st.get("TX_RFS")) and ((pkt_time - ts_st.get("TX_TIME", 0)) < STREAM_TO):
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VHEAD and slot_st.get("RX_STREAM_ID") != stream_id:
logger.info(
"(%s) PRIVATE Call not routed for subscriber %s, call route in progress on target: HBSystem: %s, TS: %s, DEST: %s, SUB: %s",
system_name, int_id(rf_src), _target, slot, int_id(ts_st.get("TX_TGID", b"")), int_id(ts_st.get("TX_RFS", b"")),
)
continue
if stream_id != slot_st.get("RX_STREAM_ID"):
ts_st["TX_START"] = pkt_time
ts_st["TX_TGID"] = dst_id
ts_st["TX_STREAM_ID"] = stream_id
ts_st["TX_RFS"] = rf_src
ts_st["TX_PEER"] = peer_id
logger.info("(%s) PRIVATE call bridged to HBP System: %s TS: %s, DST: %s", system_name, _target, slot, int_id(dst_id))
if not _unit_data:
self._send_routing_event(
"PRIVATE VOICE,START,TX,{},{},{},{},{},{}".format(
_target, int_id(stream_id), int_id(peer_id), int_id(rf_src), slot, int_id(dst_id),
).encode("utf-8", "ignore")
)
ts_st["TX_TIME"] = pkt_time
ts_st["TX_TYPE"] = dtype_vseq
send_data = data
try:
self._send_to_system(_target, send_data)
except Exception as e:
logger.warning("(ROUTER) send_to_system %s failed: %s", _target, e)
if frame_type == HBPF_DATA_SYNC and dtype_vseq == HBPF_SLT_VTERM and slot_st.get("RX_TYPE") != HBPF_SLT_VTERM:
self._pvt_targets = []
call_duration = pkt_time - slot_st.get("RX_START", pkt_time)
logger.info(
"(%s) *PRIVATE CALL END* STREAM ID: %s SUB: %s PEER: %s DST: %s, TS %s, Duration: %.2f",
system_name, int_id(stream_id), int_id(rf_src), int_id(peer_id), int_id(dst_id), slot, call_duration,
)
self._send_routing_event(
"PRIVATE VOICE,END,RX,{},{},{},{},{},{},{:.2f}".format(
system_name,
int_id(stream_id),
int_id(peer_id),
int_id(rf_src),
slot,
int_id(dst_id),
call_duration,
)
)
if slot_st:
if _unit_data:
# Keep stream continuity for multi-frame unit data without marking the
# slot voice-busy (parity with is_unit_data_ingress on HBP ingress).
slot_st["RX_STREAM_ID"] = stream_id
else:
slot_st["RX_PEER"] = peer_id
slot_st["RX_SEQ"] = seq
slot_st["RX_RFS"] = rf_src
slot_st["RX_TYPE"] = dtype_vseq
slot_st["RX_TGID"] = dst_id
slot_st["RX_TIME"] = pkt_time
slot_st["RX_STREAM_ID"] = stream_id

Powered by TurnKey Linux.