From 95c4c093f0aaabce08224a56577116ba393b2200 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:33:41 +0200 Subject: [PATCH] feat(ui-new): Tools > Satellites -- sky plot and signal bars on the 128x64 OLED Two views, swapped with left / right: a polar sky plot (filled = used in the fix, hollow = tracked, a pixel = no signal) beside fix, used / in view, HDOP, PDOP and time to first fix; and a signal bar per satellite (used first, a dotted 30 dB-Hz line) under per-constellation counts. The GPS stays awake while it's open. Data from GpsSky (-D GPS_SKYVIEW, now on the L1 solo build). The simulator's made-up sky moves from the L2's GpsScreen into helpers/sensors/GpsSkySim.h, so both sims show it. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/ui-lvgl/GpsScreen.h | 60 +---- .../companion_radio/ui-new/SatellitesScreen.h | 212 ++++++++++++++++++ examples/companion_radio/ui-new/ToolsScreen.h | 8 +- examples/companion_radio/ui-new/UITask.cpp | 8 + examples/companion_radio/ui-new/UITask.h | 2 + solo/wio-tracker-l1/platformio.ini | 1 + src/helpers/sensors/GpsSkySim.h | 69 ++++++ variants/sim/build_wasm.sh | 3 +- 8 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 examples/companion_radio/ui-new/SatellitesScreen.h create mode 100644 src/helpers/sensors/GpsSkySim.h diff --git a/examples/companion_radio/ui-lvgl/GpsScreen.h b/examples/companion_radio/ui-lvgl/GpsScreen.h index 17148882..06a0d4ff 100644 --- a/examples/companion_radio/ui-lvgl/GpsScreen.h +++ b/examples/companion_radio/ui-lvgl/GpsScreen.h @@ -44,62 +44,7 @@ static uint32_t snrColor(int snr) { } #if defined(SIM_PLATFORM) && defined(GPS_SKYVIEW) -// The sim has no receiver: a made-up sky, sent through the real parser as -// NMEA once a second. The fix comes 20 s after the screen first asks. -static GpsSky s_sim_sky; -static void simSentence(const char* body) { - uint8_t cs = 0; - for (const char* p = body; *p; p++) cs ^= (uint8_t)*p; - char line[120]; - snprintf(line, sizeof(line), "$%s*%02X\r\n", body, cs); - for (const char* p = line; *p; p++) s_sim_sky.feed(*p); -} -static void simFeed() { - static uint32_t s_next = 0, s_t0 = 0; - uint32_t now = millis(); - if (!s_t0) s_t0 = now; - if ((int32_t)(now - s_next) < 0) return; - s_next = now + 1000; - struct SimSat { const char* talker; int prn, elev, az, snr; }; - static const SimSat SATS[] = { - { "GP", 2, 72, 40, 44 }, { "GP", 5, 48, 120, 41 }, { "GP", 12, 31, 205, 36 }, { "GP", 15, 18, 290, 27 }, - { "GP", 20, 63, 310, 43 }, { "GP", 25, 9, 75, 18 }, { "GP", 29, 22, 160, 31 }, { "GP", 44, 30, 190, 33 }, - { "BD", 7, 55, 250, 38 }, { "BD", 10, 40, 20, 35 }, { "BD", 23, 12, 140, -1 }, { "BD", 37, 67, 95, 42 }, - { "GL", 71, 35, 330, 29 }, { "GL", 72, 50, 260, 34 }, { "GL", 80, 6, 30, -1 }, - }; - const int N = sizeof(SATS) / sizeof(SATS[0]); - float t = (now - s_t0) / 1000.0f; - bool fix = t > 20; - char b[110]; - for (const char* talker : { "GP", "BD", "GL" }) { - const SimSat* mine[8]; - int k = 0; - for (int i = 0; i < N; i++) if (!strcmp(SATS[i].talker, talker)) mine[k++] = &SATS[i]; - int msgs = (k + 3) / 4; - for (int m = 0; m < msgs; m++) { - int o = snprintf(b, sizeof(b), "%sGSV,%d,%d,%02d", talker, msgs, m + 1, k); - for (int j = m * 4; j < k && j < m * 4 + 4; j++) { - const SimSat& s = *mine[j]; - int az = ((int)(s.az + t / 6) % 360); - int snr = s.snr < 0 ? -1 : s.snr - (fix ? 0 : 8) + (int)(3 * sinf(t / 3 + s.prn)); - if (snr >= 0) o += snprintf(b + o, sizeof(b) - o, ",%02d,%02d,%03d,%02d", s.prn, s.elev, az, snr); - else o += snprintf(b + o, sizeof(b) - o, ",%02d,%02d,%03d,", s.prn, s.elev, az); - } - simSentence(b); - } - } - if (fix) { - simSentence("GNGSA,A,3,02,05,12,15,20,29,,,,,,,1.6,0.9,1.3,1"); - simSentence("GNGSA,A,3,07,10,37,,,,,,,,,,1.6,0.9,1.3,4"); - simSentence("GNGSA,A,3,72,,,,,,,,,,,,1.6,0.9,1.3,2"); - simSentence("GNGGA,120000.00,5213.0000,N,02100.0000,E,1,10,0.9,112.4,M,34.5,M,,"); - simSentence("GNRMC,120000.00,A,5213.0000,N,02100.0000,E,0.4,87.0,260926,,,A"); - } else { - simSentence("GNGSA,A,1,,,,,,,,,,,,,99.9,99.9,99.9,1"); - simSentence("GNGGA,120000.00,,,,,0,00,99.9,,,,,,"); - simSentence("GNRMC,120000.00,V,,,,,,,260926,,,N"); - } -} + #include #endif // The receiver's data, or nullptr on a board without the parser. @@ -107,8 +52,7 @@ static GpsSky* sky() { #if defined(SEEED_WIO_TRACKER_L2) && defined(GPS_SKYVIEW) return &gps.sky(); #elif defined(SIM_PLATFORM) && defined(GPS_SKYVIEW) - simFeed(); - return &s_sim_sky; + return gpsSkySim(); #else return nullptr; #endif diff --git a/examples/companion_radio/ui-new/SatellitesScreen.h b/examples/companion_radio/ui-new/SatellitesScreen.h new file mode 100644 index 00000000..5b6ab789 --- /dev/null +++ b/examples/companion_radio/ui-new/SatellitesScreen.h @@ -0,0 +1,212 @@ +#pragma once +// Tools › Satellites (-D GPS_SKYVIEW): what the GPS receiver sees, in two +// views swapped with left / right (or Enter): +// Sky -- a polar plot (the rim is the horizon, the centre straight up, +// north at the top): a filled dot is a satellite the fix uses, a +// hollow one is tracked but unused, a single pixel is in view with +// no signal. Beside it: fix, used / in view, HDOP, PDOP and the +// time to first fix (or how long it's been looking). +// Signal -- a bar per satellite, its height the signal (C/N0, 50 dB-Hz = +// full), filled when used; a dotted line marks 30 dB-Hz (good). +// The top line counts used / in view per constellation. +// +// The data is helpers/sensors/GpsSky.h, fed the board's NMEA by +// MicroNMEALocationProvider (a made-up sky in the simulator: GpsSkySim.h). +// The GPS is kept awake while the screen is open (UITask's keep-awake list). + +#include +#include +#ifdef SIM_PLATFORM + #include +#else + #include +#endif +#include "icons.h" // miniIconScale() + +class SatellitesScreen : public UIScreen { + UITask* _task; + bool _signal = false; // false: the sky view + + // The board's location provider is a MicroNMEALocationProvider wherever + // GPS_SKYVIEW is set (the flag's contract, as on the L2). + static GpsSky* sky() { +#ifdef SIM_PLATFORM + return gpsSkySim(); +#else + LocationProvider* p = sensors.getLocationProvider(); + return p ? &((MicroNMEALocationProvider*)p)->sky() : nullptr; +#endif + } + + static void dot(DisplayDriver& d, int x, int y) { d.fillRect(x, y, 1, 1); } + + // Midpoint circle; `step` > 1 dots it (every step-th point of each octant). + static void circle(DisplayDriver& d, int cx, int cy, int r, int step = 1) { + int x = r, y = 0, err = 1 - r, i = 0; + while (x >= y) { + if (i++ % step == 0) { + dot(d, cx + x, cy + y); dot(d, cx - x, cy + y); dot(d, cx + x, cy - y); dot(d, cx - x, cy - y); + dot(d, cx + y, cy + x); dot(d, cx - y, cy + x); dot(d, cx + y, cy - x); dot(d, cx - y, cy - x); + } + y++; + if (err < 0) err += 2 * y + 1; + else { x--; err += 2 * (y - x) + 1; } + } + } + + static int usedCount(const GpsSky& g) { + int n = 0; + for (int i = 0; i < g.count; i++) if (g.used(g.sats[i])) n++; + return n; + } + + void renderSky(DisplayDriver& d, GpsSky& g, int top) { + const int lh = d.getLineHeight(); + const int cw = d.getCharWidth(); + const int s = miniIconScale(d); + const int r = (d.height() - 1 - top) / 2 - 1; + const int cx = r + 1, cy = top + r + 1; + + circle(d, cx, cy, r); // the horizon + circle(d, cx, cy, r / 2, 2); // 45 degrees up, dotted + dot(d, cx, cy); // straight up + d.setColor(DisplayDriver::DARK); // north, on a gap in the rim + d.fillRect(cx - cw / 2 - 1, cy - r - lh / 2, cw + 2, lh); + d.setColor(DisplayDriver::LIGHT); + d.setCursor(cx - cw / 2, cy - r - lh / 2); + d.print("N"); + + for (int pass = 0; pass < 2; pass++) { // the used ones last, on top + for (int i = 0; i < g.count; i++) { + const GpsSky::Sat& sat = g.sats[i]; + bool used = g.used(sat); + if (sat.elev < 0 || sat.azim < 0 || used != (pass == 1)) continue; + float rr = r * (90 - sat.elev) / 90.0f; + float a = sat.azim * (float)M_PI / 180.0f; + int x = cx + (int)lroundf(rr * sinf(a)), y = cy - (int)lroundf(rr * cosf(a)); + if (used) { + d.fillRect(x - s, y - s, 2 * s + 1, 2 * s + 1); + } else if (sat.snr >= 0) { + d.setColor(DisplayDriver::DARK); // clear inside, over a ring + d.fillRect(x - s, y - s, 2 * s + 1, 2 * s + 1); + d.setColor(DisplayDriver::LIGHT); + d.drawRect(x - s, y - s, 2 * s + 1, 2 * s + 1); + } else { + dot(d, x, y); + } + } + } + + // The numbers, right of the plot. + const int x0 = 2 * r + 6, step = lh + 1; + int y = top; + char buf[24]; + d.setCursor(x0, y); + d.print(g.fix_mode == 3 ? "3D fix" : g.hasFix() ? "2D fix" : "No fix"); + y += step; + snprintf(buf, sizeof(buf), "Sats %d/%d", usedCount(g), g.count); + d.setCursor(x0, y); d.print(buf); + y += step; + if (g.hdop > 0 && g.hdop < 99) snprintf(buf, sizeof(buf), "HDOP %.1f", g.hdop); + else snprintf(buf, sizeof(buf), "HDOP -"); + d.setCursor(x0, y); d.print(buf); + y += step; + if (g.pdop > 0 && g.pdop < 99) snprintf(buf, sizeof(buf), "PDOP %.1f", g.pdop); + else snprintf(buf, sizeof(buf), "PDOP -"); + d.setCursor(x0, y); d.print(buf); + y += step; + if (g.ttff_ms) snprintf(buf, sizeof(buf), "TTFF %lus", (unsigned long)(g.ttff_ms / 1000)); + else if (g.start_ms) snprintf(buf, sizeof(buf), "Wait %lus", (unsigned long)((millis() - g.start_ms) / 1000)); + else buf[0] = 0; + d.setCursor(x0, y); d.print(buf); + } + + void renderSignal(DisplayDriver& d, GpsSky& g, int top) { + const int lh = d.getLineHeight(); + const int W = d.width(); + + // Used / in view per constellation, the ones with any in view. + char line[40]; + int o = 0; + for (uint8_t sys = 0; sys < GpsSky::SYS_COUNT && o < (int)sizeof(line) - 8; sys++) { + int seen = 0, used = 0; + for (int i = 0; i < g.count; i++) { + if (g.sats[i].sys != sys) continue; + seen++; + if (g.used(g.sats[i])) used++; + } + if (seen) o += snprintf(line + o, sizeof(line) - o, "%s%c%d/%d", o ? " " : "", GpsSky::sysLetter(sys), used, seen); + } + d.drawTextEllipsized(0, top, W, o ? line : "None in view"); + + // Used first, then the strongest. + int idx[GpsSky::MAX_SATS]; + int n = g.count; + for (int i = 0; i < n; i++) idx[i] = i; + auto key = [&](int i) { return (g.used(g.sats[i]) ? 100 : 0) + g.sats[i].snr; }; + for (int i = 1; i < n; i++) { // insertion sort, <= 64 + int v = idx[i], j = i - 1; + while (j >= 0 && key(idx[j]) < key(v)) { idx[j + 1] = idx[j]; j--; } + idx[j + 1] = v; + } + int fit = (W + 1) / 3; // bars at least 2 px wide, 1 px apart + if (n > fit) n = fit; + if (!n) return; + int bw = (W + 1) / n - 1; + if (bw > 8) bw = 8; + + const int y0 = top + lh + 3, y1 = d.height() - 1, H = y1 - y0; + for (int x = 0; x < W; x += 3) dot(d, x, y1 - H * 30 / 50); // 30 dB-Hz + for (int k = 0; k < n; k++) { + const GpsSky::Sat& sat = g.sats[idx[k]]; + int x = k * (bw + 1); + int snr = sat.snr > 50 ? 50 : sat.snr; + int h = snr < 0 ? 1 : (H * snr / 50 > 2 ? H * snr / 50 : 2); + if (g.used(sat) || h <= 2) d.fillRect(x, y1 - h + 1, bw, h); + else { + d.setColor(DisplayDriver::DARK); // over the dotted line + d.fillRect(x, y1 - h + 1, bw, h); + d.setColor(DisplayDriver::LIGHT); + d.drawRect(x, y1 - h + 1, bw, h); + } + } + } + +public: + SatellitesScreen(UITask* task) : _task(task) {} + void onShow() override {} + + int render(DisplayDriver& display) override { + display.setTextSize(1); + display.setColor(DisplayDriver::LIGHT); + display.drawCenteredHeader(_signal ? "SIGNAL" : "SKY"); + display.drawTextRightAlign(display.width() - 1, 0, _signal ? "2/2" : "1/2"); + const int top = display.listStart(); + const int cx = display.width() / 2, lh = display.getLineHeight(); + + GpsSky* g = sky(); + if (!g) { + display.drawTextCentered(cx, top + lh, "No GPS data"); + return 1000; + } + g->expire(); + if (!g->sentences || millis() - g->last_ms > 5000) { + bool on = _task->getGPSState(); + display.drawTextCentered(cx, top + lh, on ? "Waiting for GPS..." : "GPS is off"); + if (!on) display.drawTextCentered(cx, top + 2 * lh + 2, "Home > GPS page"); + return 1000; + } + if (_signal) renderSignal(display, *g, top); + else renderSky(display, *g, top); + return 1000; + } + + bool handleInput(char c) override { + if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; } + if (c == KEY_LEFT || c == KEY_RIGHT || c == KEY_ENTER || c == KEY_NEXT || c == KEY_PREV) { + _signal = !_signal; + return true; + } + return true; + } +}; diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 45281f76..71b06d2b 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -13,7 +13,7 @@ class ToolsScreen : public UIScreen { UITask* _task; enum Action { - ACT_NEARBY, ACT_LIVESHARE, ACT_TRAIL, ACT_LOCATOR, ACT_COMPASS, + ACT_NEARBY, ACT_LIVESHARE, ACT_TRAIL, ACT_LOCATOR, ACT_COMPASS, ACT_SATELLITES, ACT_BOT, ACT_AUTOADVERT, ACT_REPEATER, ACT_ADMIN, ACT_CLOCK, ACT_RINGTONE, ACT_DIAGNOSTICS #if defined(PIN_GPIO1) @@ -54,6 +54,7 @@ class ToolsScreen : public UIScreen { case ACT_TRAIL: _task->gotoTrailScreen(); break; case ACT_LOCATOR: _task->gotoLocatorScreen(); break; case ACT_COMPASS: _task->gotoCompassScreen(); break; + case ACT_SATELLITES: _task->gotoSatellitesScreen(); break; case ACT_BOT: _task->gotoBotScreen(); break; case ACT_AUTOADVERT: _task->gotoAutoAdvertScreen(); break; case ACT_REPEATER: _task->gotoRepeaterScreen(); break; @@ -128,6 +129,9 @@ const ToolsScreen::Tool ToolsScreen::LOCATION_TOOLS[] = { { "Trail", &ICON_TRAIL, ACT_TRAIL }, { "Locator", &ICON_MAP_WAYPOINT, ACT_LOCATOR }, { "Compass", &ICON_MAP_NORTH, ACT_COMPASS }, +#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW) + { "Satellites", &ICON_GPS, ACT_SATELLITES }, +#endif }; const ToolsScreen::Tool ToolsScreen::COMMS_TOOLS[] = { { "Remote Bot", &ICON_BOT, ACT_BOT }, @@ -144,7 +148,7 @@ const ToolsScreen::Tool ToolsScreen::SYSTEM_TOOLS[] = { #endif }; const ToolsScreen::Section ToolsScreen::SECTIONS[] = { - { "Location", &ICON_MAP_CONTACT, LOCATION_TOOLS, 5 }, + { "Location", &ICON_MAP_CONTACT, LOCATION_TOOLS, (uint8_t)(sizeof(LOCATION_TOOLS)/sizeof(LOCATION_TOOLS[0])) }, { "Comms", &ICON_ADVERT, COMMS_TOOLS, 4 }, { "System", &ICON_GEAR, SYSTEM_TOOLS, (uint8_t)(sizeof(SYSTEM_TOOLS)/sizeof(SYSTEM_TOOLS[0])) }, }; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index deee9948..58ba1249 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -178,6 +178,9 @@ static const int QUICK_MSGS_MAX = 10; #include "LocatorScreen.h" #include "TrailScreen.h" #include "CompassScreen.h" +#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW) + #include "SatellitesScreen.h" +#endif #include "DiagnosticsScreen.h" #include "RepeaterScreen.h" #if defined(PIN_GPIO1) @@ -1623,6 +1626,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no locator_screen = new LocatorScreen(this, node_prefs); trail_screen = new TrailScreen(this, &_core->trail.store()); compass_screen = new CompassScreen(this); +#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW) + satellites_screen = new SatellitesScreen(this); +#endif diag_screen = new DiagnosticsScreen(this); repeater_screen = new RepeaterScreen(this); clock_tools = new ClockToolsScreen(this, node_prefs); @@ -1654,6 +1660,7 @@ void UITask::openAdminFor(const ContactInfo& ci, bool from_picker) { void UITask::gotoDashboardConfig() { setCurrScreen(dashboard_config); } void UITask::gotoTrailScreen() { setCurrScreen(trail_screen); } void UITask::gotoCompassScreen() { setCurrScreen(compass_screen); } +void UITask::gotoSatellitesScreen() { if (satellites_screen) setCurrScreen(satellites_screen); } void UITask::gotoDiagnosticsScreen() { setCurrScreen(diag_screen); } void UITask::gotoRepeaterScreen() { setCurrScreen(repeater_screen); } void UITask::gotoClockTools() { setCurrScreen(clock_tools); } @@ -2813,6 +2820,7 @@ void UITask::loop() { || (_node_prefs && _node_prefs->loc_share_enabled) || (_node_prefs && _node_prefs->locator_enabled && _node_prefs->locator_has_target) || curr == compass_screen + || (satellites_screen && curr == satellites_screen) || (curr == nearby_screen && ((NearbyScreen*)nearby_screen)->isNavigating()) || (curr == trail_screen && ((TrailScreen*)trail_screen)->wpNeedsLiveGps()) || (curr == messages_screen && ((MessagesScreen*)messages_screen)->navActive()) diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 2e193c71..8d2cdba8 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -112,6 +112,7 @@ class UITask : public UITaskBase, public UiCoreHost { UIScreen* locator_screen = nullptr; UIScreen* trail_screen = nullptr; UIScreen* compass_screen = nullptr; + UIScreen* satellites_screen = nullptr; // Tools > Satellites (-D GPS_SKYVIEW) UIScreen* diag_screen = nullptr; UIScreen* repeater_screen = nullptr; UIScreen* clock_tools = nullptr; @@ -316,6 +317,7 @@ public: void gotoTrailScreen(); void gotoMapScreen(); // opens the Trail screen directly in its Map view void gotoCompassScreen(); + void gotoSatellitesScreen(); void gotoDiagnosticsScreen(); void gotoRepeaterScreen(); void gotoGpioScreen(); // no-op on boards without user GPIO pins (see PIN_GPIO1) diff --git a/solo/wio-tracker-l1/platformio.ini b/solo/wio-tracker-l1/platformio.ini index e727680f..97f69e82 100644 --- a/solo/wio-tracker-l1/platformio.ini +++ b/solo/wio-tracker-l1/platformio.ini @@ -15,5 +15,6 @@ build_flags = ${WioTrackerL1CompanionDual.build_flags} -D UI_HAS_JOYSTICK_UPDOWN=1 -D ENABLE_SCREENSHOT -D DUAL_SERIAL=1 + -D GPS_SKYVIEW ; Tools > Satellites (helpers/sensors/GpsSky.h) -Os extra_scripts = post:create-uf2.py diff --git a/src/helpers/sensors/GpsSkySim.h b/src/helpers/sensors/GpsSkySim.h new file mode 100644 index 00000000..7939d465 --- /dev/null +++ b/src/helpers/sensors/GpsSkySim.h @@ -0,0 +1,69 @@ +#pragma once +// The browser simulator has no GPS receiver: a made-up sky (GPS, BeiDou and +// GLONASS satellites drifting slowly round), sent through the real GpsSky +// parser as NMEA once a second. The fix comes 20 s after the first ask. +// Both UIs' satellite screens read it through gpsSkySim(). + +#include "GpsSky.h" +#include +#include + +static GpsSky s_gps_sim_sky; +static void gpsSimSentence(const char* body) { + uint8_t cs = 0; + for (const char* p = body; *p; p++) cs ^= (uint8_t)*p; + char line[120]; + snprintf(line, sizeof(line), "$%s*%02X\r\n", body, cs); + for (const char* p = line; *p; p++) s_gps_sim_sky.feed(*p); +} +static void gpsSimFeed() { + static uint32_t s_next = 0, s_t0 = 0; + uint32_t now = millis(); + if (!s_t0) s_t0 = now; + if ((int32_t)(now - s_next) < 0) return; + s_next = now + 1000; + struct SimSat { const char* talker; int prn, elev, az, snr; }; + static const SimSat SATS[] = { + { "GP", 2, 72, 40, 44 }, { "GP", 5, 48, 120, 41 }, { "GP", 12, 31, 205, 36 }, { "GP", 15, 18, 290, 27 }, + { "GP", 20, 63, 310, 43 }, { "GP", 25, 9, 75, 18 }, { "GP", 29, 22, 160, 31 }, { "GP", 44, 30, 190, 33 }, + { "BD", 7, 55, 250, 38 }, { "BD", 10, 40, 20, 35 }, { "BD", 23, 12, 140, -1 }, { "BD", 37, 67, 95, 42 }, + { "GL", 71, 35, 330, 29 }, { "GL", 72, 50, 260, 34 }, { "GL", 80, 6, 30, -1 }, + }; + const int N = sizeof(SATS) / sizeof(SATS[0]); + float t = (now - s_t0) / 1000.0f; + bool fix = t > 20; + char b[110]; + for (const char* talker : { "GP", "BD", "GL" }) { + const SimSat* mine[8]; + int k = 0; + for (int i = 0; i < N; i++) if (!strcmp(SATS[i].talker, talker)) mine[k++] = &SATS[i]; + int msgs = (k + 3) / 4; + for (int m = 0; m < msgs; m++) { + int o = snprintf(b, sizeof(b), "%sGSV,%d,%d,%02d", talker, msgs, m + 1, k); + for (int j = m * 4; j < k && j < m * 4 + 4; j++) { + const SimSat& s = *mine[j]; + int az = ((int)(s.az + t / 6) % 360); + int snr = s.snr < 0 ? -1 : s.snr - (fix ? 0 : 8) + (int)(3 * sinf(t / 3 + s.prn)); + if (snr >= 0) o += snprintf(b + o, sizeof(b) - o, ",%02d,%02d,%03d,%02d", s.prn, s.elev, az, snr); + else o += snprintf(b + o, sizeof(b) - o, ",%02d,%02d,%03d,", s.prn, s.elev, az); + } + gpsSimSentence(b); + } + } + if (fix) { + gpsSimSentence("GNGSA,A,3,02,05,12,15,20,29,,,,,,,1.6,0.9,1.3,1"); + gpsSimSentence("GNGSA,A,3,07,10,37,,,,,,,,,,1.6,0.9,1.3,4"); + gpsSimSentence("GNGSA,A,3,72,,,,,,,,,,,,1.6,0.9,1.3,2"); + gpsSimSentence("GNGGA,120000.00,5213.0000,N,02100.0000,E,1,10,0.9,112.4,M,34.5,M,,"); + gpsSimSentence("GNRMC,120000.00,A,5213.0000,N,02100.0000,E,0.4,87.0,260926,,,A"); + } else { + gpsSimSentence("GNGSA,A,1,,,,,,,,,,,,,99.9,99.9,99.9,1"); + gpsSimSentence("GNGGA,120000.00,,,,,0,00,99.9,,,,,,"); + gpsSimSentence("GNRMC,120000.00,V,,,,,,,260926,,,N"); + } +} + +static GpsSky* gpsSkySim() { + gpsSimFeed(); + return &s_gps_sim_sky; +} diff --git a/variants/sim/build_wasm.sh b/variants/sim/build_wasm.sh index 27049e53..1d002417 100755 --- a/variants/sim/build_wasm.sh +++ b/variants/sim/build_wasm.sh @@ -159,6 +159,7 @@ DEFINES=( # surface them as on-device pages. -DENV_INCLUDE_GPS=1 -DUI_SENSORS_PAGE=1 + -DGPS_SKYVIEW # satellite screens (GpsSkySim.h feeds a made-up sky) ) # The on-device splash's "Solo " bar shows FIRMWARE_VERSION. Without @@ -184,7 +185,7 @@ if [ "$SIM_UI" = "lvgl" ]; then done # The L2's PSRAM-sized limits (solo/wio-tracker-l2/platformio.ini). DEFINES=("${kept[@]}" -DDISPLAY_CLASS=SimLcdDisplay -DMAX_GROUP_CHANNELS=40 -DUI_ZOOM=1 -DLV_CONF_INCLUDE_SIMPLE - -DTRAIL_CAPACITY=4096 -DWAYPOINT_CAPACITY=64 -DHIST_CH_MAX=256 -DHIST_DM_MAX=128 -DNEARBY_MAX=64 -DHIST_ARCHIVE -DGPS_SKYVIEW) + -DTRAIL_CAPACITY=4096 -DWAYPOINT_CAPACITY=64 -DHIST_CH_MAX=256 -DHIST_DM_MAX=128 -DNEARBY_MAX=64 -DHIST_ARCHIVE) INCLUDES+=("-I$LVGL_DIR") LINK_EXTRA=(-sFETCH=1) # map downloads (LvglPort.h) go through emscripten_fetch fi