11#include " SpaceFeedClient.h"
22
3+ #include < time.h>
4+
35using space::SpaceFetchRequest;
46using space::SpaceFetchResult;
57
68namespace {
79
810// Default poll intervals (ms).
9- constexpr uint32_t ISS_MS = 5000 ; // ~5 s: a smoothly gliding blip (well under wheretheiss' ~1 req/s)
10- constexpr uint32_t LAUNCH_MS = 1200000 ; // ~20 m: editorial, slow-moving; the T-minus clock ticks locally
11- constexpr uint32_t KP_MS = 720000 ; // ~12 m: SWPC Kp is 3-hourly with a recent estimate
11+ constexpr uint32_t ISS_MS = 5000 ; // ~5 s: a smoothly gliding blip (well under wheretheiss' ~1 req/s)
12+ constexpr uint32_t LAUNCH_MS = 1200000 ; // ~20 m: editorial, slow-moving; the T-minus clock ticks locally
13+ constexpr uint32_t KP_MS = 720000 ; // ~12 m: SWPC Kp is 3-hourly with a recent estimate
14+ constexpr uint32_t DSN_MS = 30000 ; // ~30 s: which dish talks to whom changes slowly
15+ constexpr uint32_t DEEPSPACE_MS = 120000 ; // ~2 m per target (round-robin); probes move slowly
16+ constexpr uint32_t FLARE_MS = 90000 ; // ~90 s: GOES X-ray updates ~1 min; flares evolve fast
17+ constexpr uint32_t HUMANS_MS = 3600000 ; // ~1 h: the crew roster changes rarely
1218
1319constexpr uint32_t MAX_BACKOFF_MS = 600000 ; // cap exponential backoff at 10 m
1420
1521constexpr size_t LAUNCH_RETAIN = 5 ; // a few upcoming launches (the screen shows the next)
1622constexpr size_t KP_HISTORY = 24 ; // recent Kp samples kept for the gauge sparkline
23+ constexpr size_t DSN_LINK_CAP = 12 ; // bound the parsed active-link list
24+
25+ // Deep-space probes fetched round-robin from JPL Horizons (one per DeepSpace poll). cmd is the
26+ // Horizons body code; CENTER=500@399 yields range from Earth. Verified -31 (Voyager 1) live.
27+ struct DeepTarget { const char * name; const char * cmd; };
28+ const DeepTarget DEEP_TARGETS [] = {
29+ {" Voyager 1" , " -31" }, {" Voyager 2" , " -32" }, {" New Horizons" , " -98" },
30+ {" JWST" , " -170" }, {" Parker Solar Probe" , " -96" },
31+ };
32+ constexpr int DEEP_N = (int )(sizeof (DEEP_TARGETS ) / sizeof (DEEP_TARGETS [0 ]));
1733
1834// A polite UA: some public APIs reject default/empty agents. No key, nothing identifying the user.
1935const char * USER_AGENT = " Blipscope-Spacescope/1 (+https://github.com/Valar-Systems/Blipscope)" ;
@@ -24,6 +40,10 @@ void SpaceFeedClient::Begin()
2440{
2541 if (taskHandle != nullptr ) return ; // survives config reloads
2642
43+ // Seed the round-robin deep-space target list with friendly names (distances fill in as polled).
44+ deepTargets.resize (DEEP_N );
45+ for (int i = 0 ; i < DEEP_N ; ++i) { deepTargets[i].name = DEEP_TARGETS [i].name ; deepTargets[i].valid = false ; }
46+
2747 reqQueue = xQueueCreate (1 , sizeof (SpaceFetchRequest*));
2848 resQueue = xQueueCreate (1 , sizeof (SpaceFetchResult*));
2949
@@ -41,9 +61,13 @@ void SpaceFeedClient::Configure(const Config& newCfg)
4161 if (sc < 1 .0f ) sc = 1 .0f ;
4262 if (sc > 8 .0f ) sc = 8 .0f ;
4363
44- feeds[F_ISS ].intervalMs = (uint32_t )(ISS_MS * sc);
45- feeds[F_LAUNCH ].intervalMs = (uint32_t )(LAUNCH_MS * sc);
46- feeds[F_KP ].intervalMs = (uint32_t )(KP_MS * sc);
64+ feeds[F_ISS ].intervalMs = (uint32_t )(ISS_MS * sc);
65+ feeds[F_LAUNCH ].intervalMs = (uint32_t )(LAUNCH_MS * sc);
66+ feeds[F_KP ].intervalMs = (uint32_t )(KP_MS * sc);
67+ feeds[F_DSN ].intervalMs = (uint32_t )(DSN_MS * sc);
68+ feeds[F_DEEPSPACE ].intervalMs = (uint32_t )(DEEPSPACE_MS * sc);
69+ feeds[F_FLARE ].intervalMs = (uint32_t )(FLARE_MS * sc);
70+ feeds[F_HUMANS ].intervalMs = (uint32_t )(HUMANS_MS * sc);
4771
4872 // Stage the first poll of each endpoint shortly after (re)config, fanned out by ~400 ms so
4973 // they don't all hit the single TLS client at once.
@@ -101,7 +125,7 @@ int SpaceFeedClient::PickDueFeed(uint32_t now) const
101125 return best;
102126}
103127
104- bool SpaceFeedClient::BuildRequest (int feedIdx, SpaceFetchRequest& req) const
128+ bool SpaceFeedClient::BuildRequest (int feedIdx, SpaceFetchRequest& req)
105129{
106130 req.headers .push_back ({" User-Agent" , USER_AGENT });
107131 switch (feedIdx) {
@@ -117,6 +141,47 @@ bool SpaceFeedClient::BuildRequest(int feedIdx, SpaceFetchRequest& req) const
117141 req.endpoint = space::SpaceEndpoint::Kp;
118142 req.url = " https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json" ;
119143 return true ;
144+ case F_DSN :
145+ req.endpoint = space::SpaceEndpoint::Dsn;
146+ req.url = " https://eyes.nasa.gov/dsn/data/dsn.xml" ;
147+ return true ;
148+ case F_FLARE :
149+ req.endpoint = space::SpaceEndpoint::Flare;
150+ req.url = " https://services.swpc.noaa.gov/json/goes/primary/xrays-6-hour.json" ;
151+ return true ;
152+ case F_HUMANS :
153+ req.endpoint = space::SpaceEndpoint::Humans;
154+ // corquaid GitHub-Pages mirror: fresh + reliable HTTPS (open-notify is stale/flaky).
155+ req.url = " https://corquaid.github.io/international-space-station-APIs/JSON/people-in-space.json" ;
156+ return true ;
157+ case F_DEEPSPACE : {
158+ // Needs NTP for the START/STOP date window; skip (re-arm) until the clock is set.
159+ const time_t now = time (nullptr );
160+ if (now < 1600000000 ) return false ;
161+ const time_t tmrw = now + 86400 ;
162+ struct tm a, b;
163+ gmtime_r (&now, &a);
164+ gmtime_r (&tmrw, &b);
165+ char start[12 ], stop[12 ];
166+ strftime (start, sizeof (start), " %Y-%m-%d" , &a);
167+ strftime (stop, sizeof (stop), " %Y-%m-%d" , &b);
168+
169+ const DeepTarget& t = DEEP_TARGETS [deepIdx];
170+ req.endpoint = space::SpaceEndpoint::DeepSpace;
171+ req.targetIdx = deepIdx;
172+ // Pre-encoded Horizons query (%27=' %40=@); STEP_SIZE='1' = one interval, no spaces in
173+ // the request line. format=json so the existing GetJson de-chunker handles the body.
174+ String u = " https://ssd.jpl.nasa.gov/api/horizons.api?format=json" ;
175+ u += " &OBJ_DATA=%27NO%27&MAKE_EPHEM=%27YES%27&EPHEM_TYPE=%27OBSERVER%27" ;
176+ u += " &CENTER=%27500%40399%27&QUANTITIES=%2720%27&STEP_SIZE=%271%27" ;
177+ u += " &COMMAND=%27" ; u += t.cmd ; u += " %27" ;
178+ u += " &START_TIME=%27" ; u += start; u += " %27" ;
179+ u += " &STOP_TIME=%27" ; u += stop; u += " %27" ;
180+ req.url = u;
181+
182+ deepIdx = (deepIdx + 1 ) % DEEP_N ; // advance round-robin for the next poll
183+ return true ;
184+ }
120185 default :
121186 return false ;
122187 }
@@ -125,9 +190,13 @@ bool SpaceFeedClient::BuildRequest(int feedIdx, SpaceFetchRequest& req) const
125190int SpaceFeedClient::FeedForEndpoint (space::SpaceEndpoint e)
126191{
127192 switch (e) {
128- case space::SpaceEndpoint::Iss: return F_ISS ;
129- case space::SpaceEndpoint::Launch: return F_LAUNCH ;
130- case space::SpaceEndpoint::Kp: return F_KP ;
193+ case space::SpaceEndpoint::Iss: return F_ISS ;
194+ case space::SpaceEndpoint::Launch: return F_LAUNCH ;
195+ case space::SpaceEndpoint::Kp: return F_KP ;
196+ case space::SpaceEndpoint::Dsn: return F_DSN ;
197+ case space::SpaceEndpoint::DeepSpace: return F_DEEPSPACE ;
198+ case space::SpaceEndpoint::Flare: return F_FLARE ;
199+ case space::SpaceEndpoint::Humans: return F_HUMANS ;
131200 }
132201 return F_ISS ;
133202}
@@ -161,6 +230,22 @@ void SpaceFeedClient::ApplyResult(const SpaceFetchResult& res)
161230 case space::SpaceEndpoint::Kp:
162231 wx = res.wx ;
163232 break ;
233+ case space::SpaceEndpoint::Dsn:
234+ dsn = res.dsn ;
235+ break ;
236+ case space::SpaceEndpoint::DeepSpace:
237+ if (res.targetIdx >= 0 && res.targetIdx < (int )deepTargets.size ()) {
238+ const String nm = deepTargets[res.targetIdx ].name ; // preserve the friendly name
239+ deepTargets[res.targetIdx ] = res.deepTarget ;
240+ deepTargets[res.targetIdx ].name = nm;
241+ }
242+ break ;
243+ case space::SpaceEndpoint::Flare:
244+ flare = res.flare ;
245+ break ;
246+ case space::SpaceEndpoint::Humans:
247+ crew = res.crew ;
248+ break ;
164249 }
165250
166251 // One-line confirmation the first time each feed lands (handy for field/serial diagnostics).
@@ -180,6 +265,22 @@ void SpaceFeedClient::ApplyResult(const SpaceFetchResult& res)
180265 case space::SpaceEndpoint::Kp:
181266 Serial.printf (" [space] Kp ok: %.2f (%u samples)\n " , wx.kp , (unsigned )wx.history .size ());
182267 break ;
268+ case space::SpaceEndpoint::Dsn:
269+ Serial.printf (" [space] DSN ok: %u active links\n " , (unsigned )dsn.links .size ());
270+ break ;
271+ case space::SpaceEndpoint::DeepSpace:
272+ if (res.targetIdx >= 0 && res.targetIdx < (int )deepTargets.size ())
273+ Serial.printf (" [space] deepspace ok: %s %.2f AU\n " ,
274+ deepTargets[res.targetIdx ].name .c_str (),
275+ deepTargets[res.targetIdx ].distanceAu );
276+ break ;
277+ case space::SpaceEndpoint::Flare:
278+ Serial.printf (" [space] flare ok: %s (%.1e W/m2)\n " ,
279+ space::XrayClass (flare.fluxWm2 ).c_str (), flare.fluxWm2 );
280+ break ;
281+ case space::SpaceEndpoint::Humans:
282+ Serial.printf (" [space] humans ok: %d in space\n " , crew.number );
283+ break ;
183284 }
184285 }
185286}
@@ -198,6 +299,7 @@ void SpaceFeedClient::RunWorker()
198299
199300 SpaceFetchResult* res = new SpaceFetchResult ();
200301 res->endpoint = req->endpoint ;
302+ res->targetIdx = req->targetIdx ;
201303 Fetch (http, *req, *res);
202304 delete req;
203305
@@ -208,6 +310,15 @@ void SpaceFeedClient::RunWorker()
208310void SpaceFeedClient::Fetch (HttpRequestManager& http,
209311 const SpaceFetchRequest& req, SpaceFetchResult& res)
210312{
313+ // DSN is XML over a Content-Length response: fetch as (yielding) text and scan it.
314+ if (req.endpoint == space::SpaceEndpoint::Dsn) {
315+ const HttpResult r = http.Get (req.url , req.params , req.headers );
316+ if (!r.success || r.statusCode < 200 || r.statusCode >= 300 ) { res.ok = false ; return ; }
317+ space::ParseDsn (r.response , res.dsn , DSN_LINK_CAP );
318+ res.ok = true ; // a successful fetch is valid even with zero active links
319+ return ;
320+ }
321+
211322 JsonDocument doc;
212323 const HttpResult r = http.GetJson (req.url , doc, req.params , req.headers );
213324 if (!r.success || r.statusCode < 200 || r.statusCode >= 300 ) {
@@ -228,5 +339,29 @@ void SpaceFeedClient::Fetch(HttpRequestManager& http,
228339 // SWPC serves a bare JSON array (of {time_tag, Kp, ...} objects), not an object root.
229340 res.ok = space::ParseKp (doc.as <JsonArrayConst>(), res.wx , KP_HISTORY );
230341 break ;
342+ case space::SpaceEndpoint::DeepSpace: {
343+ // Horizons format=json wraps the OBSERVER table text in "result"; scan its $$SOE line.
344+ const char * result = doc[" result" ] | " " ;
345+ double au = 0 , dd = 0 ;
346+ if (space::ParseHorizonsRange (String (result), au, dd)) {
347+ res.deepTarget .distanceAu = au;
348+ res.deepTarget .speedKms = dd < 0 ? -dd : dd;
349+ res.deepTarget .receding = dd >= 0 ;
350+ res.deepTarget .valid = true ;
351+ res.ok = true ;
352+ } else {
353+ res.ok = false ;
354+ }
355+ break ;
356+ }
357+ case space::SpaceEndpoint::Flare:
358+ // SWPC GOES X-ray: a bare JSON array of {time_tag, flux, energy} objects.
359+ res.ok = space::ParseFlare (doc.as <JsonArrayConst>(), res.flare );
360+ break ;
361+ case space::SpaceEndpoint::Humans:
362+ res.ok = space::ParseCrew (doc.as <JsonObjectConst>(), res.crew , 16 );
363+ break ;
364+ case space::SpaceEndpoint::Dsn:
365+ break ; // handled above
231366 }
232367}
0 commit comments