From a263230130e7a91d4fb5283bb74dc46cd6d55f78 Mon Sep 17 00:00:00 2001 From: Shadowbroker <43977454+BigBodyCobain@users.noreply.github.com> Date: Sun, 23 Aug 2026 06:52:14 -0600 Subject: [PATCH] fix(sigint): decouple APRS lifecycle from Meshtastic --- backend/services/fetchers/sigint.py | 71 ++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/backend/services/fetchers/sigint.py b/backend/services/fetchers/sigint.py index 185cbe0..f1b3fd0 100644 --- a/backend/services/fetchers/sigint.py +++ b/backend/services/fetchers/sigint.py @@ -1,12 +1,15 @@ """SIGINT fetcher — pulls latest signals from the SIGINT Grid into latest_data. -Merges live MQTT signals with cached Meshtastic map API nodes. -Live MQTT signals always take priority (fresher) — API nodes fill in the gaps -for the thousands of nodes our MQTT listener hasn't heard yet. +Merges live APRS/MQTT/JS8Call signals with cached Meshtastic map API nodes. +Each external bridge is reconciled independently so enabling one transport does +not implicitly start another. """ import logging + +from services.aprs_is_bridge import aprs_is_bridge from services.fetchers._store import latest_data, _data_lock, _mark_fresh +from services.meshtastic_mqtt_settings import mqtt_bridge_enabled logger = logging.getLogger("services.data_fetcher") @@ -23,7 +26,7 @@ def _merge_sigint_snapshot( # Shallow-copy every entry so the published list owns its own dicts. The # inputs alias objects that other threads keep mutating in place: live - # signals are the SIGINT bridge's own dicts (updated as packets arrive), + # signals are the SIGINT bridges' own dicts (updated as packets arrive), # and api_nodes are the same objects published under latest_data # ["meshtastic_map_nodes"]. Publishing those references into # latest_data["sigint"] lets a concurrent mutation race the lock-free @@ -69,7 +72,11 @@ def build_sigint_snapshot() -> tuple[list[dict], dict[str, object], dict[str, in from services.sigint_bridge import sigint_grid + # The legacy APRS member in SIGINTGrid is intentionally never started by + # the production fetch path after #533. Safe APRS-IS receive traffic comes + # from aprs_is_bridge, which requires explicit bounded configuration. live_signals = sigint_grid.get_all_signals() + live_signals.extend(aprs_is_bridge.get_signals()) with _data_lock: api_nodes = list(latest_data.get("meshtastic_map_nodes", [])) merged = _merge_sigint_snapshot(live_signals, api_nodes) @@ -90,22 +97,54 @@ def refresh_sigint_snapshot() -> tuple[list[dict], dict[str, object], dict[str, return signals, channel_stats, totals -def fetch_sigint(): - """Fetch all signals from the SIGINT Grid, merge with Meshtastic map nodes.""" - from services.fetchers._store import is_any_active - - if not is_any_active("sigint_meshtastic", "sigint_aprs"): - return +def _reconcile_sigint_bridges(aprs_requested: bool, mesh_requested: bool) -> None: + """Start/stop each bridge independently from current operator state.""" from services.sigint_bridge import sigint_grid - # Start bridges on first call (idempotent) - sigint_grid.start() + # Defense-in-depth: the old SIGINTGrid APRS client used an effectively + # global range subscription. It is no longer part of the production fetch + # path; force it stopped even if another caller started it accidentally. + sigint_grid.aprs.stop() + + aprs_is_bridge.reconcile(aprs_requested) + + try: + mesh_network_enabled = mqtt_bridge_enabled() + except Exception: + mesh_network_enabled = False + if mesh_requested and mesh_network_enabled: + sigint_grid.mesh.start() + else: + sigint_grid.mesh.stop() + + # JS8Call is localhost-only and historically accompanies the SIGINT view. + # Keep that behavior without coupling it to either public network bridge. + if aprs_requested or mesh_requested: + sigint_grid.js8.start() + else: + sigint_grid.js8.stop() + + +def fetch_sigint(): + """Refresh SIGINT while matching bridge lifecycles to operator settings.""" + from services.fetchers._store import effective_layers + + layers = effective_layers() + aprs_requested = bool(layers.get("sigint_aprs", False)) + mesh_requested = bool(layers.get("sigint_meshtastic", False)) + _reconcile_sigint_bridges(aprs_requested, mesh_requested) + + if not aprs_requested and not mesh_requested: + return signals, channel_stats, totals = refresh_sigint_snapshot() + from services.sigint_bridge import sigint_grid - status = sigint_grid.status logger.info( - f"SIGINT: {len(signals)} signals " - f"(APRS:{status['aprs']} MESH:{status['meshtastic']} " - f"JS8:{status['js8call']} MAP:{totals['meshtastic_map']})" + "SIGINT: %d signals (APRS:%d MESH:%d JS8:%d MAP:%d)", + len(signals), + totals["aprs"], + totals["meshtastic_live"], + len(sigint_grid.js8.signals), + totals["meshtastic_map"], )