|
|
# ADN DMR Peer Server - bridge table management (V2-P0-004)
|
|
|
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
|
|
|
|
|
"""BRIDGES table lifecycle: create, static TG, OPTIONS refresh (no Twisted)."""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import logging
|
|
|
import re
|
|
|
import time
|
|
|
from collections import deque
|
|
|
from typing import Any
|
|
|
|
|
|
from ...domain import bytes_3, bytes_4, int_id
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
class BridgeTableMixin:
|
|
|
"""make_single_bridge, stat/static TG, options_config_loop."""
|
|
|
|
|
|
def make_single_bridge(
|
|
|
self,
|
|
|
_tgid: bytes | int,
|
|
|
_sourcesystem: str,
|
|
|
_slot: int,
|
|
|
_tmout: float,
|
|
|
) -> None:
|
|
|
"""Legacy make_single_bridge: create bridge for TG with entries per MASTER (source ACTIVE on its slot) and OBP."""
|
|
|
tgid_int = int_id(_tgid) if not isinstance(_tgid, int) else _tgid
|
|
|
_tgid_s = str(tgid_int)
|
|
|
_tgid_b = _tgid if isinstance(_tgid, bytes) and len(_tgid) >= 3 else bytes_3(tgid_int)
|
|
|
if _tgid_s in ("9990", "9991", "9992", "9993", "9994", "9995", "9996", "9997", "9998", "9999"):
|
|
|
_tmout = 1.0 / 6.0
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import make_single_bridge_store
|
|
|
from ..subscription.store_sync import replace_store_from_bridges
|
|
|
|
|
|
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
|
|
|
make_single_bridge_store(
|
|
|
self._subscription_store,
|
|
|
tgid_int,
|
|
|
_sourcesystem,
|
|
|
_slot,
|
|
|
float(_tmout),
|
|
|
self._config.get("SYSTEMS", {}),
|
|
|
time.time(),
|
|
|
)
|
|
|
self._export_store_to_router()
|
|
|
return
|
|
|
timeout_sec = _tmout * 60.0
|
|
|
now = time.time()
|
|
|
bridges = self._router.get_bridges()
|
|
|
bridges[_tgid_s] = []
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
for _system in systems_cfg:
|
|
|
sys_cfg = systems_cfg.get(_system, {})
|
|
|
if sys_cfg.get("MODE") == "OPENBRIDGE":
|
|
|
if 79 <= tgid_int < 9990 or tgid_int > 9999:
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid_b, "ACTIVE": True, "TIMEOUT": "", "TO_TYPE": "NONE", "OFF": [], "ON": [], "RESET": [], "TIMER": now})
|
|
|
else:
|
|
|
if _system == _sourcesystem:
|
|
|
if _slot == 1:
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid_b, "ACTIVE": True, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now + timeout_sec})
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 2, "TGID": _tgid_b, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now})
|
|
|
else:
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 2, "TGID": _tgid_b, "ACTIVE": True, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now + timeout_sec})
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid_b, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now})
|
|
|
else:
|
|
|
# Other MASTER/PEER: ACTIVE False (legacy). ACTIVE True only via make_static_tg when a peer has this TG static.
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid_b, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now})
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 2, "TGID": _tgid_b, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now})
|
|
|
self._finalize_bridges_state()
|
|
|
|
|
|
def make_single_reflector(self, _tgid: bytes | int, _tmout: float, _sourcesystem: str) -> None:
|
|
|
"""Legacy make_single_reflector: create reflector bridge #tgid with MASTERs and OBP."""
|
|
|
_tgid_s = str(int_id(_tgid) if not isinstance(_tgid, int) else _tgid)
|
|
|
_bridge = "#" + _tgid_s
|
|
|
_tgid_b = _tgid if isinstance(_tgid, bytes) and len(_tgid) >= 3 else bytes_3(int(_tgid_s))
|
|
|
if _tgid_s in ("9990", "9991", "9992", "9993", "9994", "9995", "9996", "9997", "9998", "9999"):
|
|
|
_tmout = 1.0 / 6.0
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import make_single_reflector_store
|
|
|
|
|
|
make_single_reflector_store(
|
|
|
self._subscription_store,
|
|
|
int(_tgid_s),
|
|
|
float(_tmout),
|
|
|
_sourcesystem,
|
|
|
self._config.get("SYSTEMS", {}),
|
|
|
time.time(),
|
|
|
)
|
|
|
return
|
|
|
now = time.time()
|
|
|
bridges = self._router.get_bridges()
|
|
|
bridges[_bridge] = []
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
for _system in systems_cfg:
|
|
|
if systems_cfg.get(_system, {}).get("MODE") == "MASTER":
|
|
|
def_ua = systems_cfg[_system].get("DEFAULT_UA_TIMER", 10) * 60.0
|
|
|
if _system == _sourcesystem:
|
|
|
bridges[_bridge].append({"SYSTEM": _system, "TS": 2, "TGID": bytes_3(9), "ACTIVE": True, "TIMEOUT": _tmout * 60.0, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now + _tmout * 60.0})
|
|
|
else:
|
|
|
bridges[_bridge].append({"SYSTEM": _system, "TS": 2, "TGID": bytes_3(9), "ACTIVE": False, "TIMEOUT": def_ua, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid_b], "RESET": [], "TIMER": now})
|
|
|
if systems_cfg.get(_system, {}).get("MODE") == "OPENBRIDGE" and (79 <= int(_tgid_s) < 9990 or int(_tgid_s) > 9999):
|
|
|
bridges[_bridge].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid_b, "ACTIVE": True, "TIMEOUT": "", "TO_TYPE": "NONE", "OFF": [], "ON": [], "RESET": [], "TIMER": now})
|
|
|
|
|
|
def make_default_reflector(self, reflector: int, _tmout: float, system: str) -> None:
|
|
|
"""Legacy make_default_reflector: ensure #reflector bridge exists and set system TS2 to ACTIVE/OFF."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import make_default_reflector_store
|
|
|
|
|
|
make_default_reflector_store(
|
|
|
self._subscription_store,
|
|
|
reflector,
|
|
|
float(_tmout),
|
|
|
system,
|
|
|
self._config.get("SYSTEMS", {}),
|
|
|
time.time(),
|
|
|
)
|
|
|
return
|
|
|
bridge = "#" + str(reflector)
|
|
|
bridges = self._router.get_bridges()
|
|
|
if bridge not in bridges:
|
|
|
bridges[bridge] = []
|
|
|
self.make_single_reflector(bytes_3(reflector), _tmout, system)
|
|
|
bridgetemp = deque()
|
|
|
for bridgesystem in bridges.get(bridge, []):
|
|
|
if bridgesystem.get("SYSTEM") == system and bridgesystem.get("TS") == 2:
|
|
|
bridgetemp.append({"SYSTEM": system, "TS": 2, "TGID": bytes_3(9), "ACTIVE": True, "TIMEOUT": _tmout * 60.0, "TO_TYPE": "OFF", "OFF": [], "ON": [bytes_3(reflector)], "RESET": [], "TIMER": time.time() + _tmout * 60.0})
|
|
|
else:
|
|
|
bridgetemp.append(bridgesystem)
|
|
|
bridges[bridge] = list(bridgetemp)
|
|
|
|
|
|
def _ensure_master_legs_in_tg_bridge(self, tg: int, system: str, _tmout: float) -> None:
|
|
|
"""If BRIDGES[tg] exists but this MASTER has no TS1/TS2 rows, append them.
|
|
|
|
|
|
Legacy parity: make_stat_bridge and OPTIONS UA re-add create TS1+TS2 per MASTER; if a bridge
|
|
|
was built earlier without this system, make_static_tg must insert missing legs before ACTIVE.
|
|
|
"""
|
|
|
sys_cfg = self._config.get("SYSTEMS", {}).get(system, {})
|
|
|
if sys_cfg.get("MODE") != "MASTER":
|
|
|
return
|
|
|
bridge_key = str(tg)
|
|
|
if bridge_key[:1] == "#":
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
entries = bridges.get(bridge_key)
|
|
|
if not entries:
|
|
|
return
|
|
|
tgid_b = bytes_3(tg)
|
|
|
if bridge_key in ("9990", "9991", "9992", "9993", "9994", "9995", "9996", "9997", "9998", "9999"):
|
|
|
tmout_eff = 1.0 / 6.0
|
|
|
else:
|
|
|
tmout_eff = float(_tmout)
|
|
|
if tmout_eff <= 0:
|
|
|
tmout_eff = 35791394.0
|
|
|
timeout_sec = tmout_eff * 60.0
|
|
|
now = time.time()
|
|
|
has_ts1 = any(e.get("SYSTEM") == system and e.get("TS") == 1 for e in entries)
|
|
|
has_ts2 = any(e.get("SYSTEM") == system and e.get("TS") == 2 for e in entries)
|
|
|
if not has_ts1:
|
|
|
entries.append(
|
|
|
{
|
|
|
"SYSTEM": system,
|
|
|
"TS": 1,
|
|
|
"TGID": tgid_b,
|
|
|
"ACTIVE": False,
|
|
|
"TIMEOUT": timeout_sec,
|
|
|
"TO_TYPE": "ON",
|
|
|
"OFF": [],
|
|
|
"ON": [tgid_b],
|
|
|
"RESET": [],
|
|
|
"TIMER": now + timeout_sec,
|
|
|
}
|
|
|
)
|
|
|
if not has_ts2:
|
|
|
entries.append(
|
|
|
{
|
|
|
"SYSTEM": system,
|
|
|
"TS": 2,
|
|
|
"TGID": tgid_b,
|
|
|
"ACTIVE": False,
|
|
|
"TIMEOUT": timeout_sec,
|
|
|
"TO_TYPE": "ON",
|
|
|
"OFF": [],
|
|
|
"ON": [tgid_b],
|
|
|
"RESET": [],
|
|
|
"TIMER": now + timeout_sec,
|
|
|
}
|
|
|
)
|
|
|
|
|
|
def make_static_tg(self, tg: int, ts: int, _tmout: float, system: str) -> None:
|
|
|
"""Legacy make_static_tg: ensure bridge for tg exists and set system/ts to ACTIVE/OFF."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import make_static_tg_store
|
|
|
|
|
|
single_mode = bool(
|
|
|
self._config.get("SYSTEMS", {}).get(system, {}).get("SINGLE_MODE", False)
|
|
|
)
|
|
|
make_static_tg_store(
|
|
|
self._subscription_store,
|
|
|
tg,
|
|
|
ts,
|
|
|
float(_tmout),
|
|
|
system,
|
|
|
self._config.get("SYSTEMS", {}),
|
|
|
time.time(),
|
|
|
single_mode=single_mode,
|
|
|
)
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
key = str(tg)
|
|
|
if key not in bridges or not bridges.get(key):
|
|
|
self.make_single_bridge(bytes_3(tg), system, ts, _tmout)
|
|
|
self._ensure_master_legs_in_tg_bridge(tg, system, _tmout)
|
|
|
bridges = self._router.get_bridges()
|
|
|
timeout_sec = _tmout * 60.0
|
|
|
now = time.time()
|
|
|
single_mode = bool(
|
|
|
self._config.get("SYSTEMS", {}).get(system, {}).get("SINGLE_MODE", False)
|
|
|
)
|
|
|
bridgetemp = deque()
|
|
|
for bridgesystem in bridges.get(key, []):
|
|
|
if bridgesystem.get("SYSTEM") == system and bridgesystem.get("TS") == ts:
|
|
|
active = True
|
|
|
timer = now + timeout_sec
|
|
|
if single_mode and bridgesystem.get("ACTIVE") is False:
|
|
|
# OPTIONS refresh must not undo in-band SINGLE deactivation ([5] on VTERM).
|
|
|
active = False
|
|
|
timer = float(bridgesystem.get("TIMER") or timer)
|
|
|
bridgetemp.append(
|
|
|
{
|
|
|
"SYSTEM": system,
|
|
|
"TS": ts,
|
|
|
"TGID": bytes_3(tg),
|
|
|
"ACTIVE": active,
|
|
|
"TIMEOUT": timeout_sec,
|
|
|
"TO_TYPE": "OFF",
|
|
|
"OFF": [],
|
|
|
"ON": [bytes_3(tg)],
|
|
|
"RESET": [],
|
|
|
"TIMER": timer,
|
|
|
}
|
|
|
)
|
|
|
else:
|
|
|
bridgetemp.append(bridgesystem)
|
|
|
bridges[key] = list(bridgetemp)
|
|
|
self._finalize_bridges_state()
|
|
|
|
|
|
def reset_static_tg(self, tg: int, ts: int, _tmout: float, system: str) -> None:
|
|
|
"""Legacy reset_static_tg: set system/ts entry to ACTIVE False, TO_TYPE ON."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import reset_static_tg_store
|
|
|
|
|
|
reset_static_tg_store(
|
|
|
self._subscription_store,
|
|
|
tg,
|
|
|
ts,
|
|
|
float(_tmout),
|
|
|
system,
|
|
|
time.time(),
|
|
|
)
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
key = str(tg)
|
|
|
if key not in bridges:
|
|
|
return
|
|
|
bridgetemp = deque()
|
|
|
for bridgesystem in bridges.get(key, []):
|
|
|
if bridgesystem.get("SYSTEM") == system and bridgesystem.get("TS") == ts:
|
|
|
bridgetemp.append({"SYSTEM": system, "TS": ts, "TGID": bytes_3(tg), "ACTIVE": False, "TIMEOUT": _tmout * 60.0, "TO_TYPE": "ON", "OFF": [], "ON": [bytes_3(tg)], "RESET": [], "TIMER": time.time() + _tmout * 60.0})
|
|
|
else:
|
|
|
bridgetemp.append(bridgesystem)
|
|
|
bridges[key] = list(bridgetemp)
|
|
|
|
|
|
def reset_all_reflector_system(self, _tmout: float, system: str) -> None:
|
|
|
"""Legacy reset_all_reflector_system: set system's TS2 entry to inactive in every # bridge."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import reset_all_reflector_system_store
|
|
|
|
|
|
reset_all_reflector_system_store(
|
|
|
self._subscription_store,
|
|
|
float(_tmout),
|
|
|
system,
|
|
|
time.time(),
|
|
|
)
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
timeout_sec = _tmout * 60.0
|
|
|
now = time.time()
|
|
|
for bridge in list(bridges.keys()):
|
|
|
if bridge not in bridges:
|
|
|
continue
|
|
|
if bridge[:1] != "#":
|
|
|
continue
|
|
|
bridgetemp = deque()
|
|
|
for bridgesystem in bridges.get(bridge, []):
|
|
|
if bridgesystem.get("SYSTEM") == system and bridgesystem.get("TS") == 2:
|
|
|
tgid = bridgesystem.get("TGID", bytes_3(9))
|
|
|
on_tgid = bytes_3(int(bridge[1:])) if bridge[1:] else bytes_3(9)
|
|
|
bridgetemp.append({"SYSTEM": system, "TS": 2, "TGID": tgid, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [on_tgid], "RESET": [], "TIMER": now + timeout_sec})
|
|
|
else:
|
|
|
bridgetemp.append(bridgesystem)
|
|
|
bridges[bridge] = list(bridgetemp)
|
|
|
|
|
|
def remove_bridge_system(self, system: str) -> None:
|
|
|
"""Legacy remove_bridge_system: set all entries for system to ACTIVE False, TO_TYPE ON."""
|
|
|
bridges = self._router.get_bridges()
|
|
|
for _bridge in list(bridges.keys()):
|
|
|
if _bridge not in bridges:
|
|
|
continue
|
|
|
bridgetemp = deque()
|
|
|
for bridgesystem in bridges.get(_bridge, []):
|
|
|
if bridgesystem.get("SYSTEM") != system:
|
|
|
bridgetemp.append(bridgesystem)
|
|
|
else:
|
|
|
t = bridgesystem.get("TIMEOUT") or 600.0
|
|
|
if isinstance(t, str):
|
|
|
t = 600.0
|
|
|
bridgetemp.append({"SYSTEM": system, "TS": bridgesystem.get("TS", 1), "TGID": bridgesystem.get("TGID", bytes_3(0)), "ACTIVE": False, "TIMEOUT": t, "TO_TYPE": "ON", "OFF": [], "ON": [bridgesystem.get("TGID", bytes_3(0))], "RESET": [], "TIMER": time.time() + t})
|
|
|
bridges[_bridge] = list(bridgetemp)
|
|
|
|
|
|
def make_stat_bridge(self, _tgid: bytes) -> None:
|
|
|
"""Legacy make_stat_bridge: on-the-fly relay bridges for OBP traffic when GEN_STAT_BRIDGES is True."""
|
|
|
_tgid_s = str(int_id(_tgid))
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import make_stat_bridge_store
|
|
|
from ..subscription.store_sync import replace_store_from_bridges
|
|
|
|
|
|
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
|
|
|
make_stat_bridge_store(
|
|
|
self._subscription_store,
|
|
|
_tgid,
|
|
|
self._config.get("SYSTEMS", {}),
|
|
|
time.time(),
|
|
|
)
|
|
|
self._export_store_to_router()
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
bridges[_tgid_s] = []
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
now = time.time()
|
|
|
for _system in systems_cfg:
|
|
|
sys_cfg = systems_cfg.get(_system, {})
|
|
|
if sys_cfg.get("MODE") != "OPENBRIDGE":
|
|
|
if sys_cfg.get("MODE") == "MASTER":
|
|
|
_tmout = float(sys_cfg.get("DEFAULT_UA_TIMER", 10))
|
|
|
timeout_sec = _tmout * 60.0
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid], "RESET": [], "TIMER": now})
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 2, "TGID": _tgid, "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [_tgid], "RESET": [], "TIMER": now})
|
|
|
else:
|
|
|
bridges[_tgid_s].append({"SYSTEM": _system, "TS": 1, "TGID": _tgid, "ACTIVE": True, "TIMEOUT": "", "TO_TYPE": "STAT", "OFF": [], "ON": [], "RESET": [], "TIMER": now})
|
|
|
|
|
|
def deactivate_all_dynamic_bridges(self, system_name: str) -> None:
|
|
|
"""Legacy deactivate_all_dynamic_bridges: deactivate all non-STAT, non-reflector bridges for a system (TG 4000)."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import deactivate_all_dynamic_bridges_store
|
|
|
from ..subscription.store_sync import replace_store_from_bridges
|
|
|
|
|
|
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
|
|
|
deactivate_all_dynamic_bridges_store(self._subscription_store, system_name)
|
|
|
self._export_store_to_router()
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
for _bridge in list(bridges):
|
|
|
if _bridge not in bridges:
|
|
|
continue
|
|
|
if _bridge[:1] == "#":
|
|
|
continue
|
|
|
for _sys_entry in bridges.get(_bridge, []):
|
|
|
if _sys_entry.get("SYSTEM") == system_name and _sys_entry.get("TO_TYPE") != "STAT":
|
|
|
if _sys_entry.get("ACTIVE"):
|
|
|
_sys_entry["ACTIVE"] = False
|
|
|
logger.info(
|
|
|
"(ROUTER) Deactivated dynamic bridge due to TG/ID 4000: System: %s, Bridge: %s, TS: %s, TGID: %s",
|
|
|
system_name, _bridge, _sys_entry.get("TS"), int_id(_sys_entry.get("TGID", b"\x00\x00\x00")),
|
|
|
)
|
|
|
self._finalize_bridges_state()
|
|
|
|
|
|
def _readd_system_after_ua_timer_change(self, system: str, _tmout: float) -> None:
|
|
|
"""After remove_bridge_system, re-add system to bridges that no longer have ts1/ts2 (legacy 1624-1639)."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.bridge_table_ops import readd_system_after_ua_timer_change_store
|
|
|
|
|
|
readd_system_after_ua_timer_change_store(
|
|
|
self._subscription_store,
|
|
|
system,
|
|
|
float(_tmout),
|
|
|
time.time(),
|
|
|
)
|
|
|
return
|
|
|
bridges = self._router.get_bridges()
|
|
|
timeout_sec = _tmout * 60.0
|
|
|
now = time.time()
|
|
|
for _bridge in list(bridges.keys()):
|
|
|
if _bridge not in bridges:
|
|
|
continue
|
|
|
has_ts1 = any(e.get("SYSTEM") == system and e.get("TS") == 1 for e in bridges.get(_bridge, []))
|
|
|
has_ts2 = any(e.get("SYSTEM") == system and e.get("TS") == 2 for e in bridges.get(_bridge, []))
|
|
|
if _bridge[:1] != "#":
|
|
|
if not has_ts1:
|
|
|
try:
|
|
|
bridges[_bridge].append({"SYSTEM": system, "TS": 1, "TGID": bytes_3(int(_bridge)), "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [bytes_3(int(_bridge))], "RESET": [], "TIMER": now + timeout_sec})
|
|
|
except ValueError:
|
|
|
pass
|
|
|
if not has_ts2:
|
|
|
try:
|
|
|
bridges[_bridge].append({"SYSTEM": system, "TS": 2, "TGID": bytes_3(int(_bridge)), "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [], "ON": [bytes_3(int(_bridge))], "RESET": [], "TIMER": now + timeout_sec})
|
|
|
except ValueError:
|
|
|
pass
|
|
|
else:
|
|
|
if not has_ts2:
|
|
|
try:
|
|
|
bridges[_bridge].append({"SYSTEM": system, "TS": 2, "TGID": bytes_3(9), "ACTIVE": False, "TIMEOUT": timeout_sec, "TO_TYPE": "ON", "OFF": [bytes_3(4000)], "ON": [], "RESET": [], "TIMER": now + timeout_sec})
|
|
|
except ValueError:
|
|
|
pass
|
|
|
|
|
|
def apply_startup_bridges(self) -> None:
|
|
|
"""Legacy startup: set default reflectors and static TGs for each MASTER system."""
|
|
|
if self._subscription_store is not None:
|
|
|
from ..subscription.store_sync import replace_store_from_bridges
|
|
|
|
|
|
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
|
|
|
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
|
|
|
logger.debug("(ROUTER) Setting default reflectors")
|
|
|
for system, sys_cfg in self._config.get("SYSTEMS", {}).items():
|
|
|
if sys_cfg.get("MODE") != "MASTER":
|
|
|
continue
|
|
|
default_ref = int(sys_cfg.get("DEFAULT_REFLECTOR", 0))
|
|
|
if default_ref not in prohibited_tgs:
|
|
|
self.make_default_reflector(default_ref, float(sys_cfg.get("DEFAULT_UA_TIMER", 10)), system)
|
|
|
logger.debug("(ROUTER) setting static TGs")
|
|
|
for system, sys_cfg in self._config.get("SYSTEMS", {}).items():
|
|
|
if sys_cfg.get("MODE") != "MASTER":
|
|
|
continue
|
|
|
tmout = float(sys_cfg.get("DEFAULT_UA_TIMER", 10))
|
|
|
ts1_raw = sys_cfg.get("TS1_STATIC") or ""
|
|
|
ts2_raw = sys_cfg.get("TS2_STATIC") or ""
|
|
|
ts1 = [s.strip() for s in ts1_raw.split(",") if s.strip()]
|
|
|
ts2 = [s.strip() for s in ts2_raw.split(",") if s.strip()]
|
|
|
for tg_s in ts1:
|
|
|
try:
|
|
|
tg = int(tg_s)
|
|
|
except ValueError:
|
|
|
continue
|
|
|
if tg in prohibited_tgs:
|
|
|
continue
|
|
|
self.make_static_tg(tg, 1, tmout, system)
|
|
|
for tg_s in ts2:
|
|
|
try:
|
|
|
tg = int(tg_s)
|
|
|
except ValueError:
|
|
|
continue
|
|
|
if tg in prohibited_tgs:
|
|
|
continue
|
|
|
self.make_static_tg(tg, 2, tmout, system)
|
|
|
self._sync_subscription_store()
|
|
|
|
|
|
def _parse_options_string(self, opt_str: bytes | str) -> dict[str, Any] | None:
|
|
|
"""Parse hotspot OPTIONS / RPTO payload into a normalized options dict."""
|
|
|
try:
|
|
|
if isinstance(opt_str, bytes):
|
|
|
opt_str = opt_str.decode("utf8", errors="replace")
|
|
|
opt_str = opt_str.rstrip("\x00").encode("ascii", "ignore").decode()
|
|
|
opt_str = re.sub(r"['\"]", "", opt_str)
|
|
|
_options: dict[str, Any] = {}
|
|
|
for x in opt_str.split(";"):
|
|
|
try:
|
|
|
k, v = x.split("=", 1)
|
|
|
_options[k.strip()] = v.strip()
|
|
|
except ValueError:
|
|
|
continue
|
|
|
for old_k, new_k in [
|
|
|
("DIAL", "DEFAULT_REFLECTOR"),
|
|
|
("TIMER", "DEFAULT_UA_TIMER"),
|
|
|
("TS1", "TS1_STATIC"),
|
|
|
("TS2", "TS2_STATIC"),
|
|
|
("IDENTTG", "OVERRIDE_IDENT_TG"),
|
|
|
("VOICETG", "OVERRIDE_IDENT_TG"),
|
|
|
("IDENT", "VOICE"),
|
|
|
]:
|
|
|
if old_k in _options:
|
|
|
_options[new_k] = _options.pop(old_k)
|
|
|
for old_k, new_k in [("StartRef", "DEFAULT_REFLECTOR"), ("RelinkTime", "DEFAULT_UA_TIMER")]:
|
|
|
if old_k in _options:
|
|
|
_options[new_k] = _options.pop(old_k)
|
|
|
if "TS1_1" in _options:
|
|
|
parts = [_options.pop("TS1_1", "")]
|
|
|
for i in range(2, 10):
|
|
|
p = _options.pop(f"TS1_{i}", None)
|
|
|
if p is not None:
|
|
|
parts.append(p)
|
|
|
_options["TS1_STATIC"] = ",".join(parts)
|
|
|
if "TS2_1" in _options:
|
|
|
parts = [_options.pop("TS2_1", "")]
|
|
|
for i in range(2, 10):
|
|
|
p = _options.pop(f"TS2_{i}", None)
|
|
|
if p is not None:
|
|
|
parts.append(p)
|
|
|
_options["TS2_STATIC"] = ",".join(parts)
|
|
|
return _options
|
|
|
except Exception:
|
|
|
return None
|
|
|
|
|
|
def _yaml_default_ua_timer(self, sys_cfg: dict[str, Any]) -> float:
|
|
|
tmout = float(sys_cfg.get("DEFAULT_UA_TIMER", 10))
|
|
|
return 35791394.0 if tmout <= 0 else tmout
|
|
|
|
|
|
def _peer_ua_timer_minutes(self, parsed: dict[str, Any], sys_cfg: dict[str, Any]) -> float:
|
|
|
try:
|
|
|
value = int(parsed.get("DEFAULT_UA_TIMER", sys_cfg.get("DEFAULT_UA_TIMER", 10)))
|
|
|
except (TypeError, ValueError):
|
|
|
return self._yaml_default_ua_timer(sys_cfg)
|
|
|
if value == 0:
|
|
|
return 35791394.0
|
|
|
return float(value)
|
|
|
|
|
|
def _ua_timer_minutes_for_peer(self, system_name: str, peer_id: bytes) -> float:
|
|
|
"""UA bridge timeout (minutes): transmitting peer OPTIONS TIMER, else YAML default."""
|
|
|
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
|
|
|
protocols = self._get_protocols() if self._get_protocols else {}
|
|
|
proto = protocols.get(system_name)
|
|
|
peers = getattr(proto, "_peers", {}) if proto is not None else {}
|
|
|
if isinstance(peers, dict):
|
|
|
peer_int = int_id(peer_id)
|
|
|
for pk, peer in peers.items():
|
|
|
if not isinstance(peer, dict) or peer.get("CONNECTION") != "YES":
|
|
|
continue
|
|
|
try:
|
|
|
pk_int = int_id(pk if isinstance(pk, bytes) else bytes_4(int(pk)))
|
|
|
except (TypeError, ValueError):
|
|
|
continue
|
|
|
if pk_int != peer_int:
|
|
|
continue
|
|
|
opt = peer.get("OPTIONS")
|
|
|
if opt is None:
|
|
|
break
|
|
|
parsed = self._parse_options_string(opt)
|
|
|
if parsed:
|
|
|
return self._peer_ua_timer_minutes(parsed, sys_cfg)
|
|
|
break
|
|
|
return self._yaml_default_ua_timer(sys_cfg)
|
|
|
|
|
|
def _connected_peer_options_strings(self, system_name: str) -> list[bytes | str]:
|
|
|
protocols = self._get_protocols() if self._get_protocols else {}
|
|
|
proto = protocols.get(system_name)
|
|
|
peers = getattr(proto, "_peers", {}) if proto is not None else {}
|
|
|
options: list[bytes | str] = []
|
|
|
if not isinstance(peers, dict):
|
|
|
return options
|
|
|
for peer in peers.values():
|
|
|
if not isinstance(peer, dict) or peer.get("CONNECTION") != "YES":
|
|
|
continue
|
|
|
opt = peer.get("OPTIONS")
|
|
|
if opt is not None:
|
|
|
options.append(opt)
|
|
|
return options
|
|
|
|
|
|
def _static_tg_timer_maps_for_master(
|
|
|
self,
|
|
|
system_name: str,
|
|
|
*,
|
|
|
peer_options: bytes | str | None = None,
|
|
|
) -> tuple[dict[int, float], dict[int, float]]:
|
|
|
"""Per-TG TIMER (minutes) from each peer OPTIONS; never merged with max() across peers."""
|
|
|
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
|
|
|
ts1_timers: dict[int, float] = {}
|
|
|
ts2_timers: dict[int, float] = {}
|
|
|
if peer_options is not None:
|
|
|
parsed = self._parse_options_static_tgs(peer_options, sys_cfg)
|
|
|
if parsed is not None:
|
|
|
peer_tmout, ts1_list, ts2_list = parsed
|
|
|
for tg in ts1_list:
|
|
|
ts1_timers[tg] = peer_tmout
|
|
|
for tg in ts2_list:
|
|
|
ts2_timers[tg] = peer_tmout
|
|
|
return ts1_timers, ts2_timers
|
|
|
for opt in self._connected_peer_options_strings(system_name):
|
|
|
parsed = self._parse_options_static_tgs(opt, sys_cfg)
|
|
|
if parsed is None:
|
|
|
continue
|
|
|
peer_tmout, ts1_list, ts2_list = parsed
|
|
|
for tg in ts1_list:
|
|
|
if tg not in ts1_timers:
|
|
|
ts1_timers[tg] = peer_tmout
|
|
|
for tg in ts2_list:
|
|
|
if tg not in ts2_timers:
|
|
|
ts2_timers[tg] = peer_tmout
|
|
|
return ts1_timers, ts2_timers
|
|
|
|
|
|
def _options_static_apply_fingerprint(self, system_name: str) -> str:
|
|
|
"""Fingerprint for duplicate RPTO short-circuit (includes runtime SINGLE_MODE)."""
|
|
|
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
|
|
|
merged = self._merged_static_tg_lists_for_master(system_name)
|
|
|
if merged is not None:
|
|
|
ts1_nums, ts2_nums = merged
|
|
|
new_ts1 = ",".join(str(x) for x in ts1_nums)
|
|
|
new_ts2 = ",".join(str(x) for x in ts2_nums)
|
|
|
else:
|
|
|
new_ts1 = str(sys_cfg.get("TS1_STATIC") or "").strip()
|
|
|
new_ts2 = str(sys_cfg.get("TS2_STATIC") or "").strip()
|
|
|
return (
|
|
|
f"{new_ts1}|{new_ts2}|"
|
|
|
f"{int(bool(sys_cfg.get('SINGLE_MODE', False)))}"
|
|
|
)
|
|
|
|
|
|
def _options_static_lists_valid(self, opt_str: bytes | str) -> bool:
|
|
|
"""Legacy: malformed TS1/TS2 in OPTIONS aborts static bridge refresh."""
|
|
|
parsed = self._parse_options_string(opt_str)
|
|
|
if not parsed:
|
|
|
return False
|
|
|
for key in ("TS1_STATIC", "TS2_STATIC"):
|
|
|
val = str(parsed.get(key) or "").strip()
|
|
|
if val and re.search(r"[^\d,]", val):
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
def _apply_master_runtime_options(self, system_name: str, _options: dict[str, Any]) -> None:
|
|
|
"""Apply SINGLE/TIMER/VOICE/LANG from peer OPTIONS over YAML defaults (legacy options_config).
|
|
|
|
|
|
BRIDGES mutates first; ``_finalize_bridges_state`` mirrors into the subscription store
|
|
|
and re-exports the legacy shim for report/debug.
|
|
|
"""
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
sys_cfg = systems_cfg.get(system_name, {})
|
|
|
if sys_cfg.get("MODE") != "MASTER":
|
|
|
return
|
|
|
if "VOICE" in _options and bool(_options["VOICE"]) and (
|
|
|
sys_cfg.get("VOICE_IDENT") != bool(int(_options["VOICE"]))
|
|
|
):
|
|
|
sys_cfg["VOICE_IDENT"] = bool(int(_options["VOICE"]))
|
|
|
logger.debug("(OPTIONS) %s - Setting voice ident to %s", system_name, sys_cfg["VOICE_IDENT"])
|
|
|
if "OVERRIDE_IDENT_TG" in _options and _options["OVERRIDE_IDENT_TG"] and (
|
|
|
sys_cfg.get("OVERRIDE_IDENT_TG") != int(_options["OVERRIDE_IDENT_TG"])
|
|
|
):
|
|
|
sys_cfg["OVERRIDE_IDENT_TG"] = int(_options["OVERRIDE_IDENT_TG"])
|
|
|
logger.debug(
|
|
|
"(OPTIONS) %s - Setting OVERRIDE_IDENT_TG to %s",
|
|
|
system_name,
|
|
|
sys_cfg["OVERRIDE_IDENT_TG"],
|
|
|
)
|
|
|
if "LANG" in _options and _options["LANG"] != sys_cfg.get("ANNOUNCEMENT_LANGUAGE"):
|
|
|
sys_cfg["ANNOUNCEMENT_LANGUAGE"] = _options["LANG"]
|
|
|
logger.debug("(OPTIONS) %s - Setting voice language to %s", system_name, sys_cfg["ANNOUNCEMENT_LANGUAGE"])
|
|
|
if "SINGLE" in _options and (sys_cfg.get("SINGLE_MODE") != bool(int(_options["SINGLE"]))):
|
|
|
sys_cfg["SINGLE_MODE"] = bool(int(_options["SINGLE"]))
|
|
|
logger.info("(OPTIONS) %s - Setting SINGLE_MODE to %s", system_name, sys_cfg["SINGLE_MODE"])
|
|
|
# TIMER is per-peer: applied via make_static_tg for that peer's static TGs only.
|
|
|
|
|
|
def options_config_for_system(
|
|
|
self,
|
|
|
system_name: str,
|
|
|
peer_options: bytes | str | None = None,
|
|
|
) -> None:
|
|
|
"""Update runtime flags and static TG bridges (RPTO or voice path).
|
|
|
|
|
|
``peer_options`` from RPTO overrides YAML (inject-only proxy: OPTIONS live on each peer).
|
|
|
"""
|
|
|
if self._subscription_store is not None and not getattr(self, "_options_store_batch", False):
|
|
|
from ..subscription.store_sync import replace_store_from_bridges
|
|
|
|
|
|
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
|
|
|
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
sys_cfg = systems_cfg.get(system_name, {})
|
|
|
if sys_cfg.get("MODE") != "MASTER":
|
|
|
return
|
|
|
try:
|
|
|
if peer_options is not None:
|
|
|
parsed_peer = self._parse_options_string(peer_options)
|
|
|
if parsed_peer:
|
|
|
self._apply_master_runtime_options(system_name, parsed_peer)
|
|
|
elif "OPTIONS" in sys_cfg:
|
|
|
parsed_sys = self._parse_options_string(sys_cfg["OPTIONS"])
|
|
|
if parsed_sys:
|
|
|
self._apply_master_runtime_options(system_name, parsed_sys)
|
|
|
|
|
|
source_opt: bytes | str | None = peer_options
|
|
|
if source_opt is None and "OPTIONS" in sys_cfg:
|
|
|
source_opt = sys_cfg["OPTIONS"]
|
|
|
if source_opt is not None and not self._options_static_lists_valid(source_opt):
|
|
|
self._sync_subscription_store()
|
|
|
return
|
|
|
|
|
|
merged = self._merged_static_tg_lists_for_master(system_name)
|
|
|
_fp = self._options_static_apply_fingerprint(system_name)
|
|
|
if sys_cfg.get("_options_static_apply_fp") == _fp:
|
|
|
if peer_options is not None:
|
|
|
ts1_timers, ts2_timers = self._static_tg_timer_maps_for_master(
|
|
|
system_name, peer_options=peer_options
|
|
|
)
|
|
|
for tg, tmout in ts1_timers.items():
|
|
|
self.make_static_tg(tg, 1, tmout, system_name)
|
|
|
for tg, tmout in ts2_timers.items():
|
|
|
self.make_static_tg(tg, 2, tmout, system_name)
|
|
|
self._restore_prohibited_static_bridge_legs(system_name)
|
|
|
self._sync_subscription_store()
|
|
|
return
|
|
|
if merged is None:
|
|
|
# Echo / parrot TGs (9990–9999) are excluded from merged lists but still need restore.
|
|
|
self._restore_prohibited_static_bridge_legs(system_name)
|
|
|
self._sync_subscription_store()
|
|
|
return
|
|
|
ts1_nums, ts2_nums = merged
|
|
|
yaml_tmout = self._yaml_default_ua_timer(sys_cfg)
|
|
|
if peer_options is not None:
|
|
|
ts1_timers, ts2_timers = self._static_tg_timer_maps_for_master(
|
|
|
system_name, peer_options=peer_options
|
|
|
)
|
|
|
else:
|
|
|
ts1_timers, ts2_timers = self._static_tg_timer_maps_for_master(system_name)
|
|
|
new_ts1 = ",".join(str(x) for x in ts1_nums)
|
|
|
new_ts2 = ",".join(str(x) for x in ts2_nums)
|
|
|
if re.search(r"[^\d,]", new_ts1) or re.search(r"[^\d,]", new_ts2):
|
|
|
return
|
|
|
# Legacy: reset TGs that were removed (bridge_master.py 1736-1767)
|
|
|
old_ts1 = str(sys_cfg.get("TS1_STATIC") or "").strip()
|
|
|
old_ts2 = str(sys_cfg.get("TS2_STATIC") or "").strip()
|
|
|
new_ts1_set: set[int] = set()
|
|
|
new_ts2_set: set[int] = set()
|
|
|
for x in new_ts1.split(","):
|
|
|
if x.strip():
|
|
|
try:
|
|
|
new_ts1_set.add(int(x))
|
|
|
except ValueError:
|
|
|
pass
|
|
|
for x in new_ts2.split(","):
|
|
|
if x.strip():
|
|
|
try:
|
|
|
t = int(x)
|
|
|
if t != 0 and t < 16777215:
|
|
|
new_ts2_set.add(t)
|
|
|
except ValueError:
|
|
|
pass
|
|
|
for tg_s in old_ts1.split(","):
|
|
|
if not tg_s.strip():
|
|
|
continue
|
|
|
try:
|
|
|
tg = int(tg_s)
|
|
|
if tg not in new_ts1_set:
|
|
|
self.reset_static_tg(tg, 1, ts1_timers.get(tg, yaml_tmout), system_name)
|
|
|
except ValueError:
|
|
|
pass
|
|
|
for tg_s in old_ts2.split(","):
|
|
|
if not tg_s.strip():
|
|
|
continue
|
|
|
try:
|
|
|
tg = int(tg_s)
|
|
|
if tg not in new_ts2_set and tg != 0 and tg < 16777215:
|
|
|
self.reset_static_tg(tg, 2, ts2_timers.get(tg, yaml_tmout), system_name)
|
|
|
except ValueError:
|
|
|
pass
|
|
|
peer_ts1_set = set(ts1_timers) if peer_options is not None else None
|
|
|
peer_ts2_set = set(ts2_timers) if peer_options is not None else None
|
|
|
for tg in ts1_nums:
|
|
|
if tg in prohibited_tgs:
|
|
|
continue
|
|
|
if peer_ts1_set is not None and tg not in peer_ts1_set:
|
|
|
continue
|
|
|
self.make_static_tg(tg, 1, ts1_timers.get(tg, yaml_tmout), system_name)
|
|
|
for tg in ts2_nums:
|
|
|
if tg == 0 or tg >= 16777215 or tg in prohibited_tgs:
|
|
|
continue
|
|
|
if peer_ts2_set is not None and tg not in peer_ts2_set:
|
|
|
continue
|
|
|
self.make_static_tg(tg, 2, ts2_timers.get(tg, yaml_tmout), system_name)
|
|
|
systems_cfg[system_name]["TS1_STATIC"] = new_ts1
|
|
|
systems_cfg[system_name]["TS2_STATIC"] = new_ts2
|
|
|
systems_cfg[system_name]["_options_static_apply_fp"] = self._options_static_apply_fingerprint(
|
|
|
system_name
|
|
|
)
|
|
|
if new_ts1 or new_ts2:
|
|
|
logger.info("(OPTIONS) %s static TGs applied: TS1=%s TS2=%s", system_name, new_ts1 or "-", new_ts2 or "-")
|
|
|
self._restore_prohibited_static_bridge_legs(system_name)
|
|
|
self._sync_subscription_store()
|
|
|
except Exception as e:
|
|
|
logger.debug("(OPTIONS) options_config_for_system %s: %s", system_name, e)
|
|
|
|
|
|
def _static_tg_lists_from_runtime_cfg(self, sys_cfg: dict[str, Any]) -> tuple[list[int], list[int]] | None:
|
|
|
"""Build static TG lists from TS1_STATIC / TS2_STATIC (updated when peers send RPTO)."""
|
|
|
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
|
|
|
ts1_list: list[int] = []
|
|
|
ts2_list: list[int] = []
|
|
|
for tg_s in str(sys_cfg.get("TS1_STATIC") or "").split(","):
|
|
|
if not tg_s.strip():
|
|
|
continue
|
|
|
try:
|
|
|
tg = int(tg_s.strip())
|
|
|
except ValueError:
|
|
|
continue
|
|
|
if tg in prohibited_tgs:
|
|
|
continue
|
|
|
ts1_list.append(tg)
|
|
|
for tg_s in str(sys_cfg.get("TS2_STATIC") or "").split(","):
|
|
|
if not tg_s.strip():
|
|
|
continue
|
|
|
try:
|
|
|
tg = int(tg_s.strip())
|
|
|
except ValueError:
|
|
|
continue
|
|
|
if tg in prohibited_tgs:
|
|
|
continue
|
|
|
if tg == 0 or tg >= 16777215:
|
|
|
continue
|
|
|
ts2_list.append(tg)
|
|
|
if not ts1_list and not ts2_list:
|
|
|
return None
|
|
|
return (ts1_list, ts2_list)
|
|
|
|
|
|
def _parse_options_static_tgs(self, opt_str: str, sys_cfg: dict) -> tuple[float, list[int], list[int]] | None:
|
|
|
"""Parse OPTIONS string; return (tmout, ts1_tg_list, ts2_tg_list) or None. Used to apply static TGs to an existing bridge."""
|
|
|
try:
|
|
|
if isinstance(opt_str, bytes):
|
|
|
opt_str = opt_str.decode("utf8", errors="replace")
|
|
|
opt_str = opt_str.rstrip("\x00").encode("ascii", "ignore").decode()
|
|
|
opt_str = re.sub(r"['\"]", "", opt_str)
|
|
|
_options: dict[str, Any] = {}
|
|
|
for x in opt_str.split(";"):
|
|
|
try:
|
|
|
k, v = x.split("=", 1)
|
|
|
_options[k.strip()] = v.strip()
|
|
|
except ValueError:
|
|
|
continue
|
|
|
for old_k, new_k in [("DIAL", "DEFAULT_REFLECTOR"), ("TIMER", "DEFAULT_UA_TIMER"), ("TS1", "TS1_STATIC"), ("TS2", "TS2_STATIC")]:
|
|
|
if old_k in _options:
|
|
|
_options[new_k] = _options.pop(old_k)
|
|
|
if "TS1_1" in _options:
|
|
|
parts = [_options.pop("TS1_1", "")]
|
|
|
for i in range(2, 10):
|
|
|
p = _options.pop(f"TS1_{i}", None)
|
|
|
if p is not None:
|
|
|
parts.append(p)
|
|
|
_options["TS1_STATIC"] = ",".join(parts)
|
|
|
if "TS2_1" in _options:
|
|
|
parts = [_options.pop("TS2_1", "")]
|
|
|
for i in range(2, 10):
|
|
|
p = _options.pop(f"TS2_{i}", None)
|
|
|
if p is not None:
|
|
|
parts.append(p)
|
|
|
_options["TS2_STATIC"] = ",".join(parts)
|
|
|
_tmout = float(int(_options.get("DEFAULT_UA_TIMER", sys_cfg.get("DEFAULT_UA_TIMER", 10))))
|
|
|
if _tmout <= 0:
|
|
|
_tmout = 35791394
|
|
|
ts1_list: list[int] = []
|
|
|
ts2_list: list[int] = []
|
|
|
ts1_raw = str(_options.get("TS1_STATIC") or "").strip()
|
|
|
ts2_raw = str(_options.get("TS2_STATIC") or "").strip()
|
|
|
if ts1_raw and not re.search(r"[^\d,]", ts1_raw):
|
|
|
for tg_s in ts1_raw.split(","):
|
|
|
try:
|
|
|
tg1 = int(tg_s.strip())
|
|
|
if tg1 not in (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999):
|
|
|
ts1_list.append(tg1)
|
|
|
except ValueError:
|
|
|
pass
|
|
|
if ts2_raw and not re.search(r"[^\d,]", ts2_raw):
|
|
|
for tg_s in ts2_raw.split(","):
|
|
|
try:
|
|
|
tg2 = int(tg_s.strip())
|
|
|
if 0 < tg2 < 16777215 and tg2 not in (9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999):
|
|
|
ts2_list.append(tg2)
|
|
|
except ValueError:
|
|
|
pass
|
|
|
return (_tmout, ts1_list, ts2_list)
|
|
|
except Exception:
|
|
|
return None
|
|
|
|
|
|
def _merged_static_tg_lists_for_master(
|
|
|
self, system_name: str
|
|
|
) -> tuple[list[int], list[int]] | None:
|
|
|
"""Union static TG ids from runtime YAML and every connected peer RPTO (inject proxy)."""
|
|
|
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
|
|
|
if sys_cfg.get("MODE") != "MASTER":
|
|
|
return None
|
|
|
ts1_set: set[int] = set()
|
|
|
ts2_set: set[int] = set()
|
|
|
runtime = self._static_tg_lists_from_runtime_cfg(sys_cfg)
|
|
|
if runtime is not None:
|
|
|
ts1_list, ts2_list = runtime
|
|
|
ts1_set.update(ts1_list)
|
|
|
ts2_set.update(ts2_list)
|
|
|
protocols = self._get_protocols() if self._get_protocols else {}
|
|
|
proto = protocols.get(system_name)
|
|
|
peers = getattr(proto, "_peers", {}) if proto is not None else {}
|
|
|
if isinstance(peers, dict):
|
|
|
for peer in peers.values():
|
|
|
if not isinstance(peer, dict) or peer.get("CONNECTION") != "YES":
|
|
|
continue
|
|
|
opt = peer.get("OPTIONS")
|
|
|
if opt is None:
|
|
|
continue
|
|
|
if isinstance(opt, bytes):
|
|
|
opt_str = opt.decode("utf8", errors="replace")
|
|
|
else:
|
|
|
opt_str = str(opt)
|
|
|
parsed = self._parse_options_static_tgs(opt_str, sys_cfg)
|
|
|
if parsed is None:
|
|
|
continue
|
|
|
_peer_tmout, ts1_list, ts2_list = parsed
|
|
|
ts1_set.update(ts1_list)
|
|
|
ts2_set.update(ts2_list)
|
|
|
if not ts1_set and not ts2_set and "OPTIONS" in sys_cfg:
|
|
|
parsed = self._parse_options_static_tgs(sys_cfg["OPTIONS"], sys_cfg)
|
|
|
if parsed is not None:
|
|
|
_peer_tmout, ts1_list, ts2_list = parsed
|
|
|
ts1_set.update(ts1_list)
|
|
|
ts2_set.update(ts2_list)
|
|
|
if not ts1_set and not ts2_set:
|
|
|
return None
|
|
|
return (sorted(ts1_set), sorted(ts2_set))
|
|
|
|
|
|
def apply_static_tg_to_bridge(self, tg_int: int) -> None:
|
|
|
"""When a bridge was just created from OBP, mark MASTER systems that have this TG in static TS1/TS2 (runtime lists or OPTIONS) ACTIVE so the first OBP traffic reaches them."""
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
for _system in systems_cfg:
|
|
|
if systems_cfg.get(_system, {}).get("MODE") != "MASTER":
|
|
|
continue
|
|
|
if not systems_cfg.get(_system, {}).get("ENABLED", True):
|
|
|
continue
|
|
|
parsed = self._merged_static_tg_lists_for_master(_system)
|
|
|
if not parsed:
|
|
|
continue
|
|
|
ts1_list, ts2_list = parsed
|
|
|
ts1_timers, ts2_timers = self._static_tg_timer_maps_for_master(_system)
|
|
|
yaml_tmout = self._yaml_default_ua_timer(systems_cfg.get(_system, {}))
|
|
|
if tg_int in ts1_list:
|
|
|
self.make_static_tg(tg_int, 1, ts1_timers.get(tg_int, yaml_tmout), _system)
|
|
|
if tg_int in ts2_list:
|
|
|
self.make_static_tg(tg_int, 2, ts2_timers.get(tg_int, yaml_tmout), _system)
|
|
|
|
|
|
def log_connected_systems_and_tgs(self) -> None:
|
|
|
"""Periodic debug: log each system, connection state, and static TGs (TS1/TS2). Only emits at DEBUG level."""
|
|
|
if not logger.isEnabledFor(logging.DEBUG):
|
|
|
return
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
protocols = self._get_protocols() if self._get_protocols else {}
|
|
|
lines: list[str] = ["(DEBUG) Systems and TGs:"]
|
|
|
for name in sorted(systems_cfg.keys()):
|
|
|
cfg = systems_cfg.get(name, {})
|
|
|
mode = cfg.get("MODE", "?")
|
|
|
enabled = cfg.get("ENABLED", True)
|
|
|
en = "enabled" if enabled else "disabled"
|
|
|
parts = [f" {name}: {mode} ({en})"]
|
|
|
if mode == "MASTER":
|
|
|
proto = protocols.get(name)
|
|
|
peers = getattr(proto, "_peers", {}) if proto else {}
|
|
|
connected = [p for p in peers.values() if p.get("CONNECTION") == "YES"]
|
|
|
parts.append(f"peers_connected={len(connected)}")
|
|
|
if connected:
|
|
|
def _cs(c):
|
|
|
v = c.get("CALLSIGN") or b""
|
|
|
if isinstance(v, bytes):
|
|
|
return v.decode("utf8", errors="replace").strip() or "?"
|
|
|
return str(v).strip() or "?"
|
|
|
parts.append(
|
|
|
"peers=[%s]"
|
|
|
% ", ".join("%s/%s" % (p.get("RADIO_ID", "?"), _cs(p)) for p in connected[:10])
|
|
|
)
|
|
|
if len(connected) > 10:
|
|
|
parts[-1] = parts[-1].rstrip("]") + f", +{len(connected) - 10} more]"
|
|
|
ts1 = (cfg.get("TS1_STATIC") or "").strip()
|
|
|
ts2 = (cfg.get("TS2_STATIC") or "").strip()
|
|
|
if ts1 or ts2:
|
|
|
parts.append("TS1=%s TS2=%s" % (ts1 or "-", ts2 or "-"))
|
|
|
elif mode == "PEER":
|
|
|
proto = protocols.get(name)
|
|
|
conn = getattr(proto, "_stats", {}).get("CONNECTION", "?") if proto else "?"
|
|
|
parts.append("connection=%s" % conn)
|
|
|
lines.append("".join(parts))
|
|
|
if len(lines) > 1:
|
|
|
logger.debug("\n".join(lines))
|
|
|
|
|
|
def options_config_loop(self) -> None:
|
|
|
"""Legacy options_config: parse OPTIONS from MASTER systems and update bridges (default reflector, static TGs)."""
|
|
|
batch_store = self._subscription_store is not None
|
|
|
if batch_store:
|
|
|
from ..subscription.store_sync import replace_store_from_bridges
|
|
|
|
|
|
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
|
|
|
self._options_store_batch = True
|
|
|
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
|
|
|
logger.debug("(OPTIONS) Running options parser")
|
|
|
systems_cfg = self._config.get("SYSTEMS", {})
|
|
|
try:
|
|
|
for _system in list(systems_cfg.keys()):
|
|
|
try:
|
|
|
if systems_cfg.get(_system, {}).get("MODE") != "MASTER":
|
|
|
continue
|
|
|
if not systems_cfg.get(_system, {}).get("ENABLED", True):
|
|
|
continue
|
|
|
opt_str: bytes | str | None = systems_cfg.get(_system, {}).get("OPTIONS")
|
|
|
if opt_str is None:
|
|
|
protocols = self._get_protocols() if self._get_protocols else {}
|
|
|
proto = protocols.get(_system)
|
|
|
peers = getattr(proto, "_peers", {}) if proto is not None else {}
|
|
|
if isinstance(peers, dict):
|
|
|
for peer in peers.values():
|
|
|
if isinstance(peer, dict) and peer.get("CONNECTION") == "YES" and peer.get("OPTIONS"):
|
|
|
opt_str = peer["OPTIONS"]
|
|
|
if opt_str is None:
|
|
|
continue
|
|
|
_options = self._parse_options_string(opt_str)
|
|
|
if not _options:
|
|
|
continue
|
|
|
logger.debug("(OPTIONS) Options found for %s", _system)
|
|
|
if "_opt_key" in systems_cfg[_system] and systems_cfg[_system].get("_opt_key"):
|
|
|
if "KEY" not in _options:
|
|
|
logger.debug("(OPTIONS) %s, options key set but no key in options string, skipping", _system)
|
|
|
continue
|
|
|
if systems_cfg[_system]["_opt_key"] != _options.get("KEY"):
|
|
|
logger.debug("(OPTIONS) %s, options key set but key sent does not match, skipping", _system)
|
|
|
continue
|
|
|
elif _options.get("KEY"):
|
|
|
systems_cfg[_system]["_opt_key"] = _options["KEY"]
|
|
|
logger.debug("(OPTIONS) %s, _opt_key not set but key sent. Setting to sent key", _system)
|
|
|
else:
|
|
|
systems_cfg[_system]["_opt_key"] = False
|
|
|
logger.debug("(OPTIONS) %s, _opt_key not set and no key sent. Set to false", _system)
|
|
|
self._apply_master_runtime_options(_system, _options)
|
|
|
_options.setdefault("TS1_STATIC", False)
|
|
|
_options.setdefault("TS2_STATIC", False)
|
|
|
_options.setdefault("DEFAULT_REFLECTOR", 0)
|
|
|
_options.setdefault("OVERRIDE_IDENT_TG", False)
|
|
|
_options.setdefault("DEFAULT_UA_TIMER", systems_cfg[_system].get("DEFAULT_UA_TIMER", 10))
|
|
|
if "TS1_STATIC" not in _options or "TS2_STATIC" not in _options or "DEFAULT_REFLECTOR" not in _options or "DEFAULT_UA_TIMER" not in _options:
|
|
|
logger.debug("(OPTIONS) %s - Required field missing, ignoring", _system)
|
|
|
continue
|
|
|
if _options["TS1_STATIC"] == "":
|
|
|
_options["TS1_STATIC"] = False
|
|
|
if _options["TS2_STATIC"] == "":
|
|
|
_options["TS2_STATIC"] = False
|
|
|
if _options.get("TS1_STATIC") and re.search(r"[^\d,]", str(_options["TS1_STATIC"])):
|
|
|
logger.debug("(OPTIONS) %s - TS1_STATIC contains characters other than numbers and comma, ignoring", _system)
|
|
|
continue
|
|
|
if _options.get("TS2_STATIC") and re.search(r"[^\d,]", str(_options["TS2_STATIC"])):
|
|
|
logger.debug("(OPTIONS) %s - TS2_STATIC contains characters other than numbers and comma, ignoring", _system)
|
|
|
continue
|
|
|
for key in ("DEFAULT_REFLECTOR", "OVERRIDE_IDENT_TG", "DEFAULT_UA_TIMER"):
|
|
|
if isinstance(_options.get(key), str) and not str(_options[key]).isdigit():
|
|
|
logger.debug("(OPTIONS) %s - %s is not an integer, ignoring", _system, key)
|
|
|
continue
|
|
|
if int(_options.get("DEFAULT_UA_TIMER", 0)) == 0:
|
|
|
_options["DEFAULT_UA_TIMER"] = 35791394
|
|
|
_tmout = float(int(_options["DEFAULT_UA_TIMER"]))
|
|
|
new_ref = int(_options.get("DEFAULT_REFLECTOR", 0))
|
|
|
cur_ref = int(systems_cfg[_system].get("DEFAULT_REFLECTOR", 0))
|
|
|
if new_ref != cur_ref:
|
|
|
if new_ref > 0:
|
|
|
logger.debug("(OPTIONS) %s default reflector changed, updating", _system)
|
|
|
self.reset_all_reflector_system(_tmout, _system)
|
|
|
self.make_default_reflector(new_ref, _tmout, _system)
|
|
|
elif new_ref in prohibited_tgs and not bool(new_ref):
|
|
|
logger.debug("(OPTIONS) %s default reflector is prohibited, ignoring change", _system)
|
|
|
else:
|
|
|
logger.debug("(OPTIONS) %s default reflector disabled, updating", _system)
|
|
|
self.reset_all_reflector_system(_tmout, _system)
|
|
|
self.options_config_for_system(_system)
|
|
|
except Exception as e:
|
|
|
logger.exception("(OPTIONS) caught exception: %s", e)
|
|
|
finally:
|
|
|
if batch_store:
|
|
|
self._options_store_batch = False
|
|
|
self._sync_subscription_store()
|
|
|
|