mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-24 19:32:42 +02:00
fix(sigint): decouple APRS lifecycle from Meshtastic
This commit is contained in:
@@ -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"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user