Skip to content

Commit d6aac2b

Browse files
Merge pull request #20 from Valar-Systems/feat/space-alerts
Spacescope: ntfy alerts (launch T-10/T-1, high-Kp aurora)
2 parents 31a3fca + 2aa1c74 commit d6aac2b

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

src/space/SpaceManager.cpp

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ namespace {
1717
constexpr unsigned long AUTO_DWELL_MS = 8000; // ms per screen when auto-rotating
1818
constexpr unsigned long INTERACT_HOLD_MS = 30000; // pause auto-rotate this long after a touch
1919

20+
// Alert thresholds.
21+
constexpr long LAUNCH_T10_S = 600; // fire the T-10 alert once inside this lead time
22+
constexpr long LAUNCH_T1_S = 60; // fire the T-1 alert once inside this lead time
23+
constexpr float KP_AURORA_THRESH = 6.0f; // Kp >= this (G2+) -> "aurora likely"
24+
2025
double Deg2Rad(double d) { return d * M_PI / 180.0; }
2126

2227
// Low-precision solar elevation (degrees) at lat/lon for a UTC epoch -- drives the same night
@@ -98,6 +103,19 @@ void SpaceManager::Initialise()
98103
feed.Begin();
99104
feed.Configure(fcfg);
100105

106+
// ntfy alerts (shared ntfy-topic key + per-trigger toggles). An empty topic disables all.
107+
// The edge-state flags are NOT reset here, so saving config mid-event doesn't re-fire.
108+
ntfyTopic = configServer.GetStoredString("ntfy-topic");
109+
auto boolCfg = [&](const char* key, bool def) {
110+
const String v = configServer.GetStoredString(key);
111+
return v.isEmpty() ? def : (v == "true");
112+
};
113+
alertLaunch = boolCfg("sp-alert-launch", true);
114+
alertAurora = boolCfg("sp-alert-aurora", true);
115+
alertFlare = boolCfg("sp-alert-flare", false); // reserved (no GOES X-ray feed yet)
116+
alertIss = boolCfg("sp-alert-iss", false); // reserved (no ISS-pass feed yet)
117+
alertDsn = boolCfg("sp-alert-dsn", false); // reserved (no DSN feed yet)
118+
101119
currentBrightness = configuredBrightness;
102120
tft.setBrightness(currentBrightness);
103121
lastBrightnessCheck = 0;
@@ -110,7 +128,7 @@ void SpaceManager::Initialise()
110128
void SpaceManager::Update()
111129
{
112130
feed.Poll();
113-
// Stage 3 will check ntfy alert triggers here (launch T-10/T-1, high Kp, ...).
131+
CheckAlerts();
114132
UpdateBrightness();
115133
HandleTouch();
116134
AutoRotate();
@@ -227,6 +245,68 @@ void SpaceManager::UpdateBrightness()
227245
}
228246
}
229247

248+
void SpaceManager::CheckAlerts()
249+
{
250+
const time_t nowUtc = time(nullptr);
251+
const bool synced = nowUtc > 1600000000;
252+
253+
// --- Launch: fire once at T-10 min and once at T-1 min for the next launch with a real time.
254+
// Edge-detected on the T-minus seconds crossing each threshold downward, so a boot mid-window
255+
// doesn't emit a mislabeled alert (on first sight of a launch we seed lastLaunchSecs = now).
256+
const std::vector<space::Launch>& ls = feed.Launches();
257+
if (synced && !ls.empty() && ls.front().precise && ls.front().t0Epoch > 0) {
258+
const space::Launch& L = ls.front();
259+
const long secs = L.t0Epoch - (long)nowUtc;
260+
if (L.t0Epoch != alertLaunchT0) { // a new launch became next: re-arm
261+
alertLaunchT0 = L.t0Epoch;
262+
firedT10 = firedT1 = false;
263+
lastLaunchSecs = secs; // seed so first sight isn't treated as a crossing
264+
}
265+
String who = L.provider;
266+
if (L.vehicle.length()) who += " " + L.vehicle;
267+
if (L.mission.length()) who += " - " + L.mission;
268+
if (!firedT10 && lastLaunchSecs > LAUNCH_T10_S && secs <= LAUNCH_T10_S) {
269+
firedT10 = true;
270+
if (alertLaunch) SendNtfy("Launch T-10 min", who, "rocket", 4);
271+
}
272+
if (!firedT1 && lastLaunchSecs > LAUNCH_T1_S && secs <= LAUNCH_T1_S) {
273+
firedT1 = true;
274+
if (alertLaunch) SendNtfy("Launch imminent (T-1 min)", who, "rocket,rotating_light", 5);
275+
}
276+
lastLaunchSecs = secs;
277+
}
278+
279+
// --- Aurora: fire once when Kp crosses up to the threshold; re-arm when it drops back below.
280+
const space::SpaceWx& wx = feed.Wx();
281+
if (wx.valid) {
282+
const bool high = wx.kp >= KP_AURORA_THRESH;
283+
if (high && !kpAlerted) {
284+
kpAlerted = true;
285+
int g = (int)floorf(wx.kp) - 4; // Kp 5..9 -> G1..G5
286+
if (g < 1) g = 1; else if (g > 5) g = 5;
287+
char body[48];
288+
snprintf(body, sizeof(body), "Kp %.1f (G%d) - aurora likely", wx.kp, g);
289+
if (alertAurora) SendNtfy("Aurora watch", body, "zap", 4);
290+
} else if (!high) {
291+
kpAlerted = false;
292+
}
293+
}
294+
}
295+
296+
void SpaceManager::SendNtfy(const String& title, const String& body, const String& tags, int priority)
297+
{
298+
if (ntfyTopic.isEmpty()) return;
299+
if (lastNotifyMs != 0 && millis() - lastNotifyMs < 5000) return; // throttle bursts
300+
lastNotifyMs = millis();
301+
302+
// Blocking POST on the loop task, serialized with the feed worker via HttpRequestManager's
303+
// mutex -- the same pattern the radar/EAM use. Triggers are rare, so the brief stall is fine.
304+
const std::vector<std::pair<String, String>> headers = {
305+
{"Title", title}, {"Tags", tags}, {"Priority", String(priority)}
306+
};
307+
(void)http.Post(String("https://ntfy.sh/") + ntfyTopic, body, headers);
308+
}
309+
230310
void SpaceManager::DrawScreenDots(BandCanvas& c, const std::vector<Screen>& rot) const
231311
{
232312
const int n = (int)rot.size();

src/space/SpaceManager.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ class SpaceManager
7474
bool nightDim = false;
7575
unsigned long lastBrightnessCheck = 0;
7676

77+
// ---- ntfy alerts (reuses the shared ntfy-topic key + POST pattern, like the radar/EAM) ----
78+
String ntfyTopic; // ntfy.sh topic; empty disables all alerts
79+
bool alertLaunch = true; // launch crossing T-10 / T-1
80+
bool alertAurora = true; // high Kp (aurora likely)
81+
bool alertFlare = false; // reserved: needs the GOES X-ray feed (later stage)
82+
bool alertIss = false; // reserved: needs ISS pass prediction (later stage)
83+
bool alertDsn = false; // reserved: needs the DSN feed (later stage)
84+
// edge state so an alert fires once per event, not every frame (persists across config reloads)
85+
long alertLaunchT0 = 0; // t0 the fired-flags below refer to (reset when it changes)
86+
long lastLaunchSecs = 0; // previous T-minus seconds, for up->down crossing detection
87+
bool firedT10 = false, firedT1 = false; // launch lead-time edges already consumed
88+
bool kpAlerted = false; // high-Kp episode already alerted (reset when Kp drops)
89+
unsigned long lastNotifyMs = 0; // throttle ntfy POSTs
90+
7791
// ---- touch / gestures ----
7892
bool wasTouched = false;
7993
int touchStartX = 0, touchStartY = 0;
@@ -100,6 +114,10 @@ class SpaceManager
100114
void UpdateBrightness();
101115
float GlowFactor() const { return nightDim ? 0.5f : 1.0f; }
102116

117+
// ntfy alerts (loop task)
118+
void CheckAlerts(); // evaluate the toggleable triggers (launch / aurora)
119+
void SendNtfy(const String& title, const String& body, const String& tags, int priority);
120+
103121
// small helpers
104122
static std::vector<String> SplitList(const String& s, bool lower);
105123
// "T-HH:MM:SS" / "T-2d 03:14" before T0; "T+HH:MM:SS" / "T+2d 03:14" after (negative = elapsed).

0 commit comments

Comments
 (0)