Make environmental feeds band-agnostic; add Environment page

- Remove band_assessment and band_detail from SWPC adapter
- Remove all frequency-specific conclusions (906 MHz, 10m-20m, etc.)
- Store only raw indices: SFI, Kp, R/S/G scales, dM/dz gradients
- Let LLM interpret propagation data based on user's band of interest
- Add full Environment page with feed status, solar indices, and ducting data
- Update Dashboard RF Propagation card to show raw values only
- Update alert messages to be frequency-agnostic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zvx-echo6 2026-05-12 14:59:54 -06:00
commit 1158e30c0b
12 changed files with 863 additions and 273 deletions

View file

@ -641,20 +641,22 @@ class AlertEngine:
"is_critical": r_scale >= 4,
})
# UHF ducting (informational -- not critical but operators want to know)
# Tropospheric ducting (informational -- not critical but operators want to know)
ducting = env_store.get_ducting_status()
if ducting and ducting.get("condition") in ("surface_duct", "elevated_duct"):
key = "env_ducting_active"
state = self._get_state(key)
if state.should_fire(now):
state.fire(now)
condition = ducting.get("condition", "ducting").replace("_", " ")
gradient = ducting.get("min_gradient", "?")
alerts.append({
"type": "uhf_ducting",
"message": "UHF ducting detected -- 906 MHz range may be extended, expect distant nodes",
"type": "tropospheric_ducting",
"message": f"Tropospheric {condition} detected (dM/dz {gradient} M-units/km)",
"severity": "info",
"node_num": None,
"node_name": "Ducting",
"node_short": "UHF",
"node_short": "TROPO",
"region": "",
"scope_type": "mesh",
"scope_value": None,

View file

@ -20,44 +20,36 @@ class SolarCommand(CommandHandler):
lines = []
# HF section
# Space weather indices (raw data - no band conclusions)
s = self._env_store.get_swpc_status()
if s:
assessment = s.get("band_assessment", "Unknown")
kp = s.get("kp_current", "?")
sfi = s.get("sfi", "?")
r = s.get("r_scale", 0)
s_sc = s.get("s_scale", 0)
g = s.get("g_scale", 0)
lines.append(f"HF: {assessment} -- SFI {sfi}, Kp {kp}")
lines.append(f"Solar: SFI {sfi}, Kp {kp}")
lines.append(f" R{r}/S{s_sc}/G{g} scales")
if assessment in ("Excellent", "Good"):
lines.append(" 10m-20m open, solid DX")
elif assessment == "Fair":
lines.append(" 20m-40m usable, upper bands marginal")
else:
lines.append(" Degraded -- lower bands only")
warnings = s.get("active_warnings", [])
for w in warnings[:2]:
lines.append(f" Warning: {w[:100]}")
else:
lines.append("HF: Data not available")
lines.append("Solar: Data not available")
# UHF ducting section
# Tropospheric ducting (raw data - no frequency conclusions)
d = self._env_store.get_ducting_status()
if d:
cond = d.get("condition", "unknown")
gradient = d.get("min_gradient", "?")
if cond == "normal":
lines.append("UHF: Normal propagation (906 MHz)")
lines.append(f"Ducting: Normal (dM/dz {gradient})")
else:
gradient = d.get("min_gradient", "?")
lines.append(f"UHF: {cond.replace('_', ' ').title()} (906 MHz)")
lines.append(f" dM/dz: {gradient} M-units/km")
lines.append(" Extended range -- expect distant nodes")
thickness = d.get("duct_thickness_m", "?")
lines.append(f"Ducting: {cond.replace('_', ' ').title()}")
lines.append(f" dM/dz: {gradient} M-units/km, ~{thickness}m thick")
else:
lines.append("UHF: Ducting data not available")
lines.append("Ducting: Data not available")
return "\n".join(lines)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,17 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MeshAI Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-CELmCk_K.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DKYlTqQ1.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MeshAI Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-Hvb4qZ75.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B5wp_1Dg.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

20
meshai/env/store.py vendored
View file

@ -134,32 +134,32 @@ class EnvironmentalStore:
else:
lines.append("NWS: No active alerts for mesh area.")
# HF
# Space weather indices (raw - LLM interprets)
s = self._swpc_status
if s:
kp = s.get("kp_current", "?")
sfi = s.get("sfi", "?")
assessment = s.get("band_assessment", "Unknown")
lines.append(f"HF: {assessment} -- SFI {sfi}, Kp {kp}")
r = s.get("r_scale", 0)
g = s.get("g_scale", 0)
lines.append(f"Space Weather: SFI {sfi}, Kp {kp}, R{r}/G{g}")
warnings = s.get("active_warnings", [])
if warnings:
for w in warnings[:2]:
lines.append(f" Warning: {w}")
else:
lines.append("HF: Space weather data not available.")
lines.append("Space Weather: Data not available.")
# UHF ducting
# Tropospheric ducting (raw - LLM interprets)
d = self._ducting_status
if d:
condition = d.get("condition", "unknown")
gradient = d.get("min_gradient", "?")
if condition == "normal":
lines.append("UHF Ducting: Normal propagation, no ducting detected.")
elif condition in ("super_refraction", "ducting", "surface_duct", "elevated_duct"):
gradient = d.get("min_gradient", "?")
lines.append(f"Tropospheric: Normal (dM/dz {gradient} M-units/km)")
else:
thickness = d.get("duct_thickness_m", "?")
lines.append(f"UHF Ducting: {condition.replace('_', ' ').title()} detected")
lines.append(f"Tropospheric: {condition.replace('_', ' ').title()}")
lines.append(f" dM/dz: {gradient} M-units/km, duct ~{thickness}m thick")
lines.append(" Extended range likely on 906 MHz -- expect distant nodes")
return "\n".join(lines)

30
meshai/env/swpc.py vendored
View file

@ -52,7 +52,7 @@ class SWPCAdapter:
changed = True
if changed:
self._update_assessment()
self._update_events()
return changed
@ -197,29 +197,9 @@ class SWPCAdapter:
except (ValueError, TypeError):
pass
def _update_assessment(self):
"""Compute band assessment from SFI and Kp."""
sfi = self._status.get("sfi", 0)
kp = self._status.get("kp_current", 0)
# Band assessment formula
if sfi > 150 and kp <= 1:
assessment = "Excellent"
detail = "Upper HF bands (10m-20m) open, solid DX conditions"
elif sfi >= 100 and kp <= 3:
assessment = "Good"
detail = "Upper HF bands (10m-20m) open, solid DX conditions"
elif sfi >= 80 and kp <= 4:
assessment = "Fair"
detail = "Mid HF bands (20m-40m) usable, upper bands marginal"
else:
assessment = "Poor"
detail = "HF conditions degraded, stick to lower bands (40m-80m)"
self._status["band_assessment"] = assessment
self._status["band_detail"] = detail
# Generate events for R-scale >= 3
def _update_events(self):
"""Generate events for significant space weather conditions."""
# Generate events for R-scale >= 3 (radio blackout)
self._events = []
r_scale = self._status.get("r_scale", 0)
if r_scale >= 3:
@ -228,7 +208,7 @@ class SWPCAdapter:
"event_id": f"swpc_r{r_scale}_{int(time.time())}",
"event_type": f"R{r_scale} Radio Blackout",
"severity": "warning" if r_scale >= 3 else "advisory",
"headline": f"R{r_scale} HF Radio Blackout -- HF comms degraded",
"headline": f"R{r_scale} Radio Blackout in progress",
"expires": time.time() + 3600, # 1hr TTL
"areas": [],
"fetched_at": time.time(),