#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; } };