Skip to content

Commit a289de1

Browse files
Merge pull request #22 from Valar-Systems/feat/space-wow-screens
Spacescope: 5 new screens (DSN Now, deep space, flare, humans, moon)
2 parents 526b0e3 + 768bdb8 commit a289de1

8 files changed

Lines changed: 667 additions & 29 deletions

src/ConfigurationWebServer.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ static const char CONFIG_HTML[] PROGMEM = R"(
687687
<input name="space-screens" value='%SPACE_SCREENS%'
688688
class="border border-sky-400 bg-gray-900 w-full px-3 py-2 text-lg sm:text-base sm:px-1 sm:py-0">
689689
</label>
690-
<span class="text-xs text-sky-600 mt-1">ids: iss, launch, kp, splash, clock. Empty rotates all. Each screen appears only when its feed has data; the clock always shows when nothing else does. (More screens arrive in upcoming firmware.)</span>
690+
<span class="text-xs text-sky-600 mt-1">ids: iss, launch, kp, flare, dsn, deepspace, humans, moon, splash, clock. Empty rotates all. Each screen appears only when its feed has data; the clock and moon always show. (More screens arrive in upcoming firmware.)</span>
691691
</fieldset>
692692
693693
<fieldset class="border border-sky-400 p-3">
@@ -864,7 +864,7 @@ void ConfigurationWebServer::Initialise() {
864864
const String brightness = prefs.getString("brightness", "255");
865865
const String spaceScreens = prefs.isKey("space-screens")
866866
? prefs.getString("space-screens", "")
867-
: String("iss,launch,kp,clock");
867+
: String("iss,launch,kp,flare,dsn,deepspace,humans,moon,clock");
868868
#endif
869869
prefs.end();
870870

src/space/SpaceFeedClient.cpp

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
#include "SpaceFeedClient.h"
22

3+
#include <time.h>
4+
35
using space::SpaceFetchRequest;
46
using space::SpaceFetchResult;
57

68
namespace {
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

1319
constexpr uint32_t MAX_BACKOFF_MS = 600000; // cap exponential backoff at 10 m
1420

1521
constexpr size_t LAUNCH_RETAIN = 5; // a few upcoming launches (the screen shows the next)
1622
constexpr 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.
1935
const 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
125190
int 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()
208310
void 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
}

src/space/SpaceFeedClient.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ class SpaceFeedClient {
4343
const space::IssState& Iss() const { return iss; }
4444
const std::vector<space::Launch>& Launches() const { return launches; }
4545
const space::SpaceWx& Wx() const { return wx; }
46+
const space::DsnState& Dsn() const { return dsn; }
47+
const std::vector<space::DeepSpaceTarget>& DeepTargets() const { return deepTargets; }
48+
const space::Flare& Flare() const { return flare; }
49+
const space::Crew& Crew() const { return crew; }
4650

4751
private:
48-
enum FeedIdx : uint8_t { F_ISS, F_LAUNCH, F_KP, F_COUNT };
52+
enum FeedIdx : uint8_t { F_ISS, F_LAUNCH, F_KP, F_DSN, F_DEEPSPACE, F_FLARE, F_HUMANS, F_COUNT };
4953
struct Feed {
5054
uint32_t intervalMs = 0;
5155
uint32_t nextDueMs = 0;
@@ -60,6 +64,11 @@ class SpaceFeedClient {
6064
space::IssState iss;
6165
std::vector<space::Launch> launches;
6266
space::SpaceWx wx;
67+
space::DsnState dsn;
68+
std::vector<space::DeepSpaceTarget> deepTargets; // one per tracked probe, sized in Begin()
69+
int deepIdx = 0; // DeepSpace round-robin cursor (one target per poll)
70+
space::Flare flare;
71+
space::Crew crew;
6372

6473
bool firstLogged[F_COUNT] = {}; // log a one-line summary the first time each feed lands
6574

@@ -75,7 +84,7 @@ class SpaceFeedClient {
7584
const space::SpaceFetchRequest& req, space::SpaceFetchResult& res);
7685

7786
int PickDueFeed(uint32_t now) const; // most-overdue ready feed, or -1
78-
bool BuildRequest(int feedIdx, space::SpaceFetchRequest& req) const;
87+
bool BuildRequest(int feedIdx, space::SpaceFetchRequest& req); // non-const: advances deepIdx
7988
void ApplyResult(const space::SpaceFetchResult& res);
8089
static int FeedForEndpoint(space::SpaceEndpoint e);
8190
};

0 commit comments

Comments
 (0)