mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
fix(satpass): broadcast safety controls — opt-in filter, rate cap, dry-run, wire format
Incident response for 343 broadcasts in 126s (2026-06-12 22:10 UTC).
Five safety controls:
1. OPT-IN BIRD FILTER: norad_ids=[] now means "broadcast nothing"
(was "all birds"). Empty list logs once at INFO and suppresses all
broadcasts. The !satpass DM command remains ungated — it queries
any bird in the TLE cache using command_norad_ids as bare-command
default. Two paths, two rules.
2. RATE CAP: new satpass.max_broadcasts_per_hour (int, default 4).
Excess qualifying passes logged and suppressed. Broadcast path only.
3. DRY-RUN MODE: new satpass.dry_run (bool, default TRUE). Logs exact
wire text at INFO prefixed "DRY-RUN would air:" without dispatching.
Go-live: enabled=true + dry_run=true → observe → dry_run=false.
4. ELEVATION DEFAULT: min_elevation REGISTRY default already at 30
(confirmed, no change needed).
5. BROADCAST WIRE FORMAT: two-line LoRa-tight format with buckets:
🛰️ {name} {bucket}, {aos_compass}→{los_compass}
{duration} minute window, {rise}–{set} {AM/PM} MDT
Buckets: overhead (≥60°), high pass (30-59°), low pass (<30°).
DM format keeps exact degrees. One format_pass() function with
broadcast= mode switch — two callers, one function.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0cb175d65b
commit
e903444356
8 changed files with 704 additions and 80 deletions
|
|
@ -66,7 +66,7 @@ NOAA18_ENVELOPE = {
|
|||
}
|
||||
|
||||
|
||||
def _enable_satpass():
|
||||
def _enable_satpass(norad_ids=None):
|
||||
"""Set satpass.enabled=true in the test DB."""
|
||||
from meshai.persistence import get_db
|
||||
from meshai.adapter_config import invalidate_cache
|
||||
|
|
@ -80,6 +80,19 @@ def _enable_satpass():
|
|||
"UPDATE adapter_config SET value_json='5' "
|
||||
"WHERE adapter='satpass' AND key='min_elevation'"
|
||||
)
|
||||
# Disable dry_run for tests that expect wire output
|
||||
conn.execute(
|
||||
"UPDATE adapter_config SET value_json='false' "
|
||||
"WHERE adapter='satpass' AND key='dry_run'"
|
||||
)
|
||||
# Set norad_ids (must be non-empty for opt-in)
|
||||
if norad_ids is None:
|
||||
norad_ids = [28654]
|
||||
conn.execute(
|
||||
"UPDATE adapter_config SET value_json=? "
|
||||
"WHERE adapter='satpass' AND key='norad_ids'",
|
||||
(json.dumps(norad_ids),)
|
||||
)
|
||||
invalidate_cache()
|
||||
|
||||
|
||||
|
|
@ -93,6 +106,8 @@ def test_noaa18_envelope_produces_satpass_event():
|
|||
_enable_satpass()
|
||||
if hasattr(handle_satpass, "_disabled_logged"):
|
||||
del handle_satpass._disabled_logged
|
||||
if hasattr(handle_satpass, "_no_norad_ids_logged"):
|
||||
del handle_satpass._no_norad_ids_logged
|
||||
|
||||
now = int(time.time())
|
||||
wire = handle_satpass(
|
||||
|
|
@ -123,12 +138,14 @@ def test_noaa18_envelope_produces_satpass_event():
|
|||
|
||||
|
||||
def test_noaa18_wire_message_format():
|
||||
"""Wire message includes satellite name, elevation, observer, direction."""
|
||||
"""Wire message includes satellite name, direction in new 2-line format."""
|
||||
from meshai.central.satpass_handler import handle_satpass
|
||||
|
||||
_enable_satpass()
|
||||
if hasattr(handle_satpass, "_disabled_logged"):
|
||||
del handle_satpass._disabled_logged
|
||||
if hasattr(handle_satpass, "_no_norad_ids_logged"):
|
||||
del handle_satpass._no_norad_ids_logged
|
||||
|
||||
wire = handle_satpass(
|
||||
NOAA18_ENVELOPE,
|
||||
|
|
@ -138,17 +155,17 @@ def test_noaa18_wire_message_format():
|
|||
)
|
||||
assert wire is not None
|
||||
|
||||
# Line 1: satellite name + elevation
|
||||
assert "NOAA 18" in wire
|
||||
assert "22" in wire # int(22.69) = 22
|
||||
lines = wire.split("\n")
|
||||
assert len(lines) == 2, f"Expected 2 lines, got {len(lines)}: {wire!r}"
|
||||
|
||||
# Line 2: AOS/LOS times
|
||||
assert "AOS" in wire
|
||||
assert "LOS" in wire
|
||||
# Line 1: satellite name + bucket + compass directions
|
||||
assert "NOAA 18" in lines[0]
|
||||
assert "low pass" in lines[0] # 22.69 < 30 = low pass
|
||||
assert "SE" in lines[0] # aos_compass
|
||||
assert "N" in lines[0] # los_compass
|
||||
|
||||
# Line 3: observer + azimuth direction
|
||||
assert "Filer" in wire
|
||||
assert "ENE" in wire # azimuth_at_peak_compass
|
||||
# Line 2: duration + time window
|
||||
assert "minute window" in lines[1]
|
||||
|
||||
|
||||
def test_missing_norad_id_rejected():
|
||||
|
|
@ -158,6 +175,8 @@ def test_missing_norad_id_rejected():
|
|||
_enable_satpass()
|
||||
if hasattr(handle_satpass, "_disabled_logged"):
|
||||
del handle_satpass._disabled_logged
|
||||
if hasattr(handle_satpass, "_no_norad_ids_logged"):
|
||||
del handle_satpass._no_norad_ids_logged
|
||||
|
||||
# Deep copy and remove norad_id
|
||||
import copy
|
||||
|
|
@ -180,6 +199,8 @@ def test_missing_max_elevation_deg_rejected():
|
|||
_enable_satpass()
|
||||
if hasattr(handle_satpass, "_disabled_logged"):
|
||||
del handle_satpass._disabled_logged
|
||||
if hasattr(handle_satpass, "_no_norad_ids_logged"):
|
||||
del handle_satpass._no_norad_ids_logged
|
||||
|
||||
import copy
|
||||
env = copy.deepcopy(NOAA18_ENVELOPE)
|
||||
|
|
@ -201,6 +222,8 @@ def test_observer_fallback_to_slug():
|
|||
_enable_satpass()
|
||||
if hasattr(handle_satpass, "_disabled_logged"):
|
||||
del handle_satpass._disabled_logged
|
||||
if hasattr(handle_satpass, "_no_norad_ids_logged"):
|
||||
del handle_satpass._no_norad_ids_logged
|
||||
|
||||
import copy
|
||||
env = copy.deepcopy(NOAA18_ENVELOPE)
|
||||
|
|
@ -214,7 +237,15 @@ def test_observer_fallback_to_slug():
|
|||
now=int(time.time()),
|
||||
)
|
||||
assert wire is not None
|
||||
assert "filer" in wire
|
||||
# Observer name stored in DB, not in broadcast wire format
|
||||
from meshai.persistence import get_db
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT observer FROM satpass_events WHERE norad_id=28654"
|
||||
).fetchone()
|
||||
assert row is not None
|
||||
assert row["observer"] == "filer"
|
||||
|
||||
|
||||
|
||||
# ── Consumer category mapping ──────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue