Add Airframes ACARS datalink on plane dossiers and Meshtastic planet scan.

Bulk-ingest Airframes messages on a rate-limited staggered queue with instant cache lookups and priority per-aircraft refresh when opening a dossier; add Meshtastic manual SCAN PLANET control in the SIGINT panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-24 22:15:50 -06:00
parent a0c79c2044
commit 9ad0a5ffce
13 changed files with 1211 additions and 27 deletions
+63 -27
View File
@@ -12,9 +12,11 @@ Polling interval deliberately kept low (4h) to be respectful to the service.
import json
import logging
import threading
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any
import requests
@@ -30,9 +32,6 @@ _MAX_AGE_HOURS = 24 # discard nodes not seen within this window
# one-person hobby service, so we prefer stale data over hammering it.
_CACHE_TRUST_HOURS = 20
# Track when we last fetched so the frontend can show staleness
_last_fetch_ts: float = 0.0
def _parse_node(node: dict) -> dict | None:
"""Convert an API node into a slim signal-like dict."""
@@ -132,7 +131,43 @@ def _save_cache(nodes: list[dict], fetch_ts: float):
logger.warning(f"Failed to save meshtastic cache: {e}")
def fetch_meshtastic_nodes():
# Track when we last fetched so the frontend can show staleness
_last_fetch_ts: float = 0.0
_scan_lock = threading.Lock()
_scan_in_progress = False
def get_meshtastic_map_status() -> dict[str, Any]:
from services.fetchers._store import get_latest_data_subset_refs
snap = get_latest_data_subset_refs("meshtastic_map_nodes", "meshtastic_map_fetched_at")
nodes = snap.get("meshtastic_map_nodes") or []
fetched_at = snap.get("meshtastic_map_fetched_at")
return {
"node_count": len(nodes) if isinstance(nodes, list) else 0,
"fetched_at": fetched_at,
"scan_in_progress": _scan_in_progress,
}
def start_meshtastic_planet_scan() -> dict[str, Any]:
if not _scan_lock.acquire(blocking=False):
return {"ok": False, "status": "scan already in progress"}
def _run() -> None:
global _scan_in_progress
try:
_scan_in_progress = True
fetch_meshtastic_nodes(force=True)
finally:
_scan_in_progress = False
_scan_lock.release()
threading.Thread(target=_run, daemon=True, name="meshtastic-planet-scan").start()
return {"ok": True, "status": "scanning"}
def fetch_meshtastic_nodes(*, force: bool = False):
"""Fetch global Meshtastic node positions from Liam Cottle's map API.
Stores processed nodes in latest_data["meshtastic_map_nodes"].
@@ -140,39 +175,40 @@ def fetch_meshtastic_nodes():
"""
from services.fetchers._store import is_any_active
if not is_any_active("sigint_meshtastic"):
if not force and not is_any_active("sigint_meshtastic"):
return
global _last_fetch_ts
# Trust a recent cache on disk — avoids hammering the upstream HTTP API
# when every install polls on roughly the same cadence.
try:
if _CACHE_FILE.exists():
mtime = _CACHE_FILE.stat().st_mtime
if time.time() - mtime < _CACHE_TRUST_HOURS * 3600:
# If memory is empty (cold start), hydrate from cache and skip fetch.
with _data_lock:
has_memory = bool(latest_data.get("meshtastic_map_nodes"))
if not has_memory:
cached = _load_cache()
if cached:
with _data_lock:
latest_data["meshtastic_map_nodes"] = cached
latest_data["meshtastic_map_fetched_at"] = mtime
_mark_fresh("meshtastic_map")
if not force:
try:
if _CACHE_FILE.exists():
mtime = _CACHE_FILE.stat().st_mtime
if time.time() - mtime < _CACHE_TRUST_HOURS * 3600:
# If memory is empty (cold start), hydrate from cache and skip fetch.
with _data_lock:
has_memory = bool(latest_data.get("meshtastic_map_nodes"))
if not has_memory:
cached = _load_cache()
if cached:
with _data_lock:
latest_data["meshtastic_map_nodes"] = cached
latest_data["meshtastic_map_fetched_at"] = mtime
_mark_fresh("meshtastic_map")
logger.info(
"Meshtastic map: cache fresh (<%.0fh), skipping network fetch",
_CACHE_TRUST_HOURS,
)
return
else:
logger.info(
"Meshtastic map: cache fresh (<%.0fh), skipping network fetch",
_CACHE_TRUST_HOURS,
)
return
else:
logger.info(
"Meshtastic map: cache fresh (<%.0fh), skipping network fetch",
_CACHE_TRUST_HOURS,
)
return
except Exception as e:
logger.debug(f"Meshtastic cache freshness check failed: {e}")
except Exception as e:
logger.debug(f"Meshtastic cache freshness check failed: {e}")
# Build a polite User-Agent. Historically this included the operator
# callsign so meshtastic.org could rate-limit per-install; that's still