@@ -17,6 +17,11 @@ namespace {
1717constexpr unsigned long AUTO_DWELL_MS = 8000 ; // ms per screen when auto-rotating
1818constexpr 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+
2025double 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()
110128void 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+
230310void SpaceManager::DrawScreenDots (BandCanvas& c, const std::vector<Screen>& rot) const
231311{
232312 const int n = (int )rot.size ();
0 commit comments