mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 09:24:44 +02:00
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:
parent
61684f0ee2
commit
1158e30c0b
12 changed files with 863 additions and 273 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
1
meshai/dashboard/static/assets/index-B5wp_1Dg.css
Normal file
1
meshai/dashboard/static/assets/index-B5wp_1Dg.css
Normal file
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
355
meshai/dashboard/static/assets/index-Hvb4qZ75.js
Normal file
355
meshai/dashboard/static/assets/index-Hvb4qZ75.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
20
meshai/env/store.py
vendored
|
|
@ -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
30
meshai/env/swpc.py
vendored
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue