From 4d8724dcc5cfedd75f3ce94f5374611ffdd8579b Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:51:47 +0200 Subject: [PATCH] feat(l1): Status page and screen replace Radio/GPS/Sensors pages and Diagnostics Home's Radio, GPS and Sensors pages become one Status page: four tiles split by dotted rules (radio, GPS, battery, nodes), each a headline over a detail. Enter opens the Status screen, tabs Radio / GPS / Sky / Power / Mesh / System, which also takes over Tools > Diagnostics and Tools > Satellites (the sky plot now lives in SkyView.h). GPS on/off moves to Enter on its tab. New InfoKit.h holds the shared pieces, sized from the font: label/value row (a long value takes its own line), centred section title, meter, chip, big number with unit, switch, history line (one sample a minute) and a block flow that scrolls a tab by whole blocks. The Status page keeps the old Radio bit and shows when any of the three old pages did; the save format is unchanged. The L1 degree sign was a cp437 byte the UTF-8 renderer drew as a box; now U+00B0. The perf walk covers every Home page and Status tab. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/NodePrefs.h | 2 +- .../ui-new/DiagnosticsScreen.h | 123 ------ examples/companion_radio/ui-new/InfoKit.h | 196 +++++++++ .../companion_radio/ui-new/SettingsScreen.h | 33 +- .../ui-new/{SatellitesScreen.h => SkyView.h} | 61 +-- .../companion_radio/ui-new/StatusScreen.h | 404 ++++++++++++++++++ examples/companion_radio/ui-new/ToolsScreen.h | 10 +- examples/companion_radio/ui-new/UITask.cpp | 298 +++++-------- examples/companion_radio/ui-new/UITask.h | 6 +- 9 files changed, 734 insertions(+), 399 deletions(-) delete mode 100644 examples/companion_radio/ui-new/DiagnosticsScreen.h create mode 100644 examples/companion_radio/ui-new/InfoKit.h rename examples/companion_radio/ui-new/{SatellitesScreen.h => SkyView.h} (78%) create mode 100644 examples/companion_radio/ui-new/StatusScreen.h diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 84ce91ce..fcb557db 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -694,7 +694,7 @@ struct NodePrefs { // persisted to file // Array indices match HomePageBit values. static const char* homePageLabel(uint8_t bit) { static const char* labels[HPB_COUNT] = { - "Clock", "Recent", "Radio", "Bluetooth", "Advert", + "Clock", "Recent", "Status", "Bluetooth", "Advert", // Status: the old Radio slot (ui-new) "GPS", "Sensors", "Tools", "Shutdown", "Settings", "Messages", "Favourites", "Map" }; return (bit < HPB_COUNT) ? labels[bit] : ""; diff --git a/examples/companion_radio/ui-new/DiagnosticsScreen.h b/examples/companion_radio/ui-new/DiagnosticsScreen.h deleted file mode 100644 index 77b3a904..00000000 --- a/examples/companion_radio/ui-new/DiagnosticsScreen.h +++ /dev/null @@ -1,123 +0,0 @@ -#pragma once -#include -#include -#include -#include "icons.h" -#include "PopupMenu.h" -#include "TabBar.h" -#include "../MyMesh.h" -#include "../ui-core/Diagnostics.h" - -extern MyMesh the_mesh; - -// Device diagnostics, organised as a circular tab carousel (shared TabBar.h, -// same idiom as BotScreen/NearbyScreen). LEFT/RIGHT switches tabs; UP/DOWN -// scrolls within the active tab: -// Live — live counters: uptime, packet counts by category (RX/TX), -// forwarded count, heap/stack headroom, radio signal, pool/queue -// depth, error flags. Hold Enter resets the counters. -// System — static device identity: firmware version + build date, device -// model, node name, and the active radio parameters. -// Font — a rendering test card: one sample line per script the UI font -// claims to cover (Latin, diacritics, Greek, Cyrillic, digits, -// symbols), so the on-device font can be eyeballed for coverage. -// The rows themselves come from the UI Core (ui-core/Diagnostics.h). Every tab -// falls back to scrolling on screens too small to show its rows at once. -class DiagnosticsScreen : public UIScreen { - UITask* _task; - int _scroll = 0; - uint8_t _tab = 0; // persists across visits (like BotScreen's _tab) - PopupMenu _reset_menu; // Live tab, Hold Enter → Reset/Cancel confirm (defaults to Cancel) - - enum Tab : uint8_t { TAB_LIVE, TAB_SYSTEM, TAB_FONT, TAB_COUNT }; - static const char* const TAB_LABELS[TAB_COUNT]; - - // Rows / lines built by the Core (ui-core/Diagnostics.h). - diag::Row _rows[diag::MAX_ROWS]; - int _row_count = 0; - diag::Line _lines[diag::MAX_LINES]; - int _line_count = 0; - - // Shared scrollable renderer for the label/value Live tab. Neither tab has a - // row cursor -- UP/DOWN move _scroll directly -- so this passes _scroll as - // drawList()'s `sel` too: its internal clamp-toward-sel is then a no-op - // (sel == scroll always), leaving clampScroll() below as the only thing - // that actually bounds _scroll, same as before. - void renderRows(DisplayDriver& display) { - const int item_h = display.lineStep(); - int visible = display.listVisible(item_h); - if (visible < 1) visible = 1; - clampScroll(_row_count, visible); - drawList(display, _row_count, _scroll, _scroll, - [&](int idx, int y, bool, int reserve) { - const diag::Row& r = _rows[idx]; - display.setCursor(2, y); - display.print(r.label); - display.drawTextRightAlign(display.width() - reserve - 2, y, r.value); - }); - } - - // Shared scrollable renderer for the full-width System / Font tabs. - void renderLines(DisplayDriver& display) { - const int item_h = display.lineStep(); - int visible = display.listVisible(item_h); - if (visible < 1) visible = 1; - clampScroll(_line_count, visible); - drawList(display, _line_count, _scroll, _scroll, - [&](int idx, int y, bool, int reserve) { - display.drawTextEllipsized(2, y, display.width() - reserve - 4, _lines[idx]); - }); - } - - void clampScroll(int total, int visible) { - int max_scroll = total - visible; - if (max_scroll < 0) max_scroll = 0; - if (_scroll > max_scroll) _scroll = max_scroll; - if (_scroll < 0) _scroll = 0; - } - -public: - DiagnosticsScreen(UITask* task) : _task(task) {} - - int render(DisplayDriver& display) override { - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - tabbar::draw(display, TAB_LABELS, TAB_COUNT, _tab); - - switch (_tab) { - case TAB_SYSTEM: _line_count = diag::systemLines(_lines); renderLines(display); break; - case TAB_FONT: _line_count = diag::fontLines(_lines); renderLines(display); break; - default: _row_count = diag::liveRows(_rows); renderRows(display); break; - } - - display.setColor(DisplayDriver::LIGHT); - if (_reset_menu.active) _reset_menu.render(display); - // Live counters refresh once a second; the static System/Font cards don't - // change, so they can idle. The reset popup wants a snappier redraw. - if (_reset_menu.active) return 50; - return _tab == TAB_LIVE ? 1000 : 2000; - } - - bool handleInput(char c) override { - if (_reset_menu.active) { - auto res = _reset_menu.handleInput(c); - if (res == PopupMenu::SELECTED && _reset_menu.selectedIndex() == 0) { - diag::resetCounters(); - _task->showAlert("Counters reset", 800); - } - return true; - } - if (keyIsPrev(c)) { _tab = (_tab + TAB_COUNT - 1) % TAB_COUNT; _scroll = 0; return true; } - if (keyIsNext(c)) { _tab = (_tab + 1) % TAB_COUNT; _scroll = 0; return true; } - if (c == KEY_UP) { if (_scroll > 0) _scroll--; return true; } - if (c == KEY_DOWN) { _scroll++; return true; } // clamped in render() - if (c == KEY_CONTEXT_MENU && _tab == TAB_LIVE) { // Hold Enter — reset the live counters - _reset_menu.beginConfirm("Reset counters?", "Reset"); - return true; - } - if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; } - return false; - } -}; - -const char* const DiagnosticsScreen::TAB_LABELS[DiagnosticsScreen::TAB_COUNT] = { "Live", "System", "Font" }; diff --git a/examples/companion_radio/ui-new/InfoKit.h b/examples/companion_radio/ui-new/InfoKit.h new file mode 100644 index 00000000..fe788833 --- /dev/null +++ b/examples/companion_radio/ui-new/InfoKit.h @@ -0,0 +1,196 @@ +#pragma once +// Building blocks for screens that show data (Status, the tool screens): one +// look for a value row, a section break, a meter, a chip, a big number with +// its unit, a switch and a history line. The 128x64 counterpart of the L2's +// info cards. Sizes follow the font (line height, miniIconScale), never a +// fixed 128x64, so a larger-font e-ink layout gets them scaled for free. + +#include +#include "icons.h" + +namespace info { + +// Label at the left, value flush right with its unit after a space ("50 m"). +// `reserve` keeps the value clear of a scroll column. +// Whether a value fits on its label's line (else rowH() gives it a line below). +inline bool rowFits(DisplayDriver& d, const char* label, const char* value, int reserve = 0, int x0 = 1) { + if (!value || !*value) return true; + const int room = d.width() - reserve - 2 - (x0 + d.getTextWidth(label) + d.getCharWidth()); + return (int)d.getTextWidth(value) <= room; +} +inline int rowH(DisplayDriver& d, const char* label, const char* value, int reserve = 0) { + return rowFits(d, label, value, reserve) ? d.lineStep() : d.lineStep() + d.getLineHeight(); +} +// A row of rowH(): the value beside the label, or right-aligned on the line +// under it when it's too long for that (ending in an ellipsis only past the +// full width). +inline void rowWrapped(DisplayDriver& d, int y, const char* label, const char* value, int reserve = 0, int x0 = 1) { + if (rowFits(d, label, value, reserve, x0)) { + d.setCursor(x0, y); + d.print(label); + if (value && *value) d.drawTextRightAlign(d.width() - reserve - 2, y, value); + return; + } + d.setCursor(x0, y); + d.print(label); + const int right = d.width() - reserve - 2, room = right - x0; // the whole line is its own + const int vy = y + d.getLineHeight(); + if ((int)d.getTextWidth(value) <= room) d.drawTextRightAlign(right, vy, value); + else d.drawTextEllipsized(right - room, vy, room, value); +} + +// A value too long for the space left beside its label ends in an ellipsis. +inline void row(DisplayDriver& d, int y, const char* label, const char* value, int reserve = 0, int x0 = 1) { + d.setCursor(x0, y); + d.print(label); + if (!value || !*value) return; + const int right = d.width() - reserve - 2; + const int room = right - (x0 + d.getTextWidth(label) + d.getCharWidth()); + if ((int)d.getTextWidth(value) <= room) d.drawTextRightAlign(right, y, value); + else if (room > d.getCharWidth()) d.drawTextEllipsized(right - room, y, room, value); +} + +// A dotted rule across [x, x + w), the separator between categories. +inline void rule(DisplayDriver& d, int x, int y, int w) { + const int s = miniIconScale(d); + for (int i = 0; i < w; i += 2 * s) d.fillRect(x + i, y, s, s); +} +inline void vrule(DisplayDriver& d, int x, int y, int h) { + const int s = miniIconScale(d); + for (int i = 0; i < h; i += 2 * s) d.fillRect(x, y + i, s, s); +} + +// A section title: the label centred between two dotted rules. One line tall. +inline void section(DisplayDriver& d, int y, const char* label, int reserve = 0) { + const int right = d.width() - reserve - 1, lw = d.getTextWidth(label); + const int lx = (right - lw) / 2, ry = y + d.getLineHeight() / 2, gap = 2 * d.getCharWidth() / 3; + rule(d, 1, ry, lx - gap - 1); + d.setCursor(lx, y); + d.print(label); + rule(d, lx + lw + gap, ry, right - (lx + lw + gap)); +} + +// A battery glyph the size of a mini icon (7 x 5 at 1x), filled to frac. +inline int batteryW(DisplayDriver& d) { return 7 * miniIconScale(d); } +inline void battery(DisplayDriver& d, int x, int y, float frac) { + const int s = miniIconScale(d); + if (frac < 0) frac = 0; + if (frac > 1) frac = 1; + d.drawRect(x, y, 6 * s, 5 * s); + d.fillRect(x + 6 * s, y + s, s, 3 * s); + const int fw = (int)(4 * s * frac + 0.5f); + if (fw > 0) d.fillRect(x + s, y + s, fw, 3 * s); +} + +// A soft-cornered meter, filled to frac (0..1). +inline void meter(DisplayDriver& d, int x, int y, int w, int h, float frac) { + if (frac < 0) frac = 0; + if (frac > 1) frac = 1; + d.drawSoftRect(x, y, w, h); + const int in = d.sepH() + 1, fw = (int)((w - 2 * in) * frac + 0.5f); + if (fw > 0) d.fillRect(x + in, y + in, fw, h - 2 * in); +} + +// A chip: a state as a filled pill (FIX, REC), a parameter as an outline +// (SF8). One line plus 2 px tall; returns the x after it, gap included. +inline int chipW(DisplayDriver& d, const char* t) { return d.getTextWidth(t) + 5; } +inline int chip(DisplayDriver& d, int x, int y, const char* t, bool filled = false) { + const int w = chipW(d, t), h = d.getLineHeight() + 2; + if (filled) d.fillSoftRect(x, y, w, h); + else d.drawSoftRect(x, y, w, h); + if (filled) d.setColor(DisplayDriver::DARK); + d.setCursor(x + 3, y + 1); + d.print(t); + d.setColor(DisplayDriver::LIGHT); + return x + w + 3; +} + +// A value at twice the text size with its unit beside it at the normal size, +// sharing the baseline. Two lines tall; returns its width. +inline int big(DisplayDriver& d, int x, int y, const char* value, const char* unit) { + d.setTextSize(2); + d.setCursor(x, y); + d.print(value); + int w = d.getTextWidth(value); + d.setTextSize(1); + if (unit && *unit) { + d.setCursor(x + w + 3, y + d.getLineHeight()); + d.print(unit); + w += 3 + d.getTextWidth(unit); + } + return w; +} +inline int bigH(DisplayDriver& d) { return 2 * d.getLineHeight(); } + +// An on/off switch, right edge at x_right, centred on a text line at y: a +// filled pill with the knob right when on, an outline with the knob left off. +inline int switchW(DisplayDriver& d) { return 13 * miniIconScale(d); } +inline void toggle(DisplayDriver& d, int x_right, int y, bool on) { + const int s = miniIconScale(d), w = switchW(d), h = 7 * s; + const int x = x_right - w, ty = y + (d.getLineHeight() - h) / 2; + if (on) { + d.fillSoftRect(x, ty, w, h); + d.setColor(DisplayDriver::DARK); + d.fillRect(x + w - 5 * s, ty + s, 3 * s, h - 2 * s); + d.setColor(DisplayDriver::LIGHT); + } else { + d.drawSoftRect(x, ty, w, h); + d.fillRect(x + 2 * s, ty + 2 * s, 3 * s, h - 4 * s); + } +} + +// The last N samples of one reading, oldest first, for a history line. +template +struct History { + int16_t v[N]; + uint8_t n = 0, head = 0; + void push(int16_t x) { v[head] = x; head = (head + 1) % N; if (n < N) n++; } + int16_t at(int i) const { return v[(head + N - n + i) % N]; } // 0 = oldest +}; + +// A history line in a w x h box: a dotted baseline, the samples scaled +// between their own min and max (at least `min_span` apart, so a flat +// reading stays flat), newest at the right edge. +template +inline void spark(DisplayDriver& d, int x, int y, int w, int h, const History& hs, int min_span = 4) { + const int s = miniIconScale(d); + for (int i = 0; i < w; i += 3 * s) d.fillRect(x + i, y + h - s, s, s); + if (hs.n < 2) return; + int lo = hs.at(0), hi = lo; + for (int i = 1; i < hs.n; i++) { int v = hs.at(i); if (v < lo) lo = v; if (v > hi) hi = v; } + if (hi - lo < min_span) { int mid = (hi + lo) / 2; lo = mid - min_span / 2; hi = lo + min_span; } + int px = -1, py = 0; + for (int i = 0; i < hs.n; i++) { + const int cx = x + w - 1 - (hs.n - 1 - i) * (w - 1) / (N - 1); + const int cy = y + h - 1 - (hs.at(i) - lo) * (h - 1) / (hi - lo); + if (px >= 0) { // a step: across, then up or down + d.fillRect(px, py, cx - px + 1, s); + d.fillRect(cx, py < cy ? py : cy, s, (py < cy ? cy - py : py - cy) + s); + } + px = cx; py = cy; + } +} + +// Blocks stacked down from `top`, with the first `skip` left out: the way a +// tab scrolls by whole blocks. Call place(h) before drawing each block; it +// says whether the block is on screen and where (at). The totals then give +// the scroll indicator and whether DOWN has anything left to show. +// A measuring Flow (measure()) only adds the heights up and draws nothing. +struct Flow { + int y, bottom, skip; + int idx = 0, at = 0; + long total_px = 0, skipped_px = 0; + bool more = false, measuring = false; + Flow(int top, int bottom_, int skip_) : y(top), bottom(bottom_), skip(skip_) {} + static Flow measure() { Flow f(0, 0, 0); f.measuring = true; return f; } + bool place(int h) { + total_px += h; + if (measuring) return false; + if (idx++ < skip) { skipped_px += h; return false; } + if (more || y + h > bottom) { more = true; return false; } + at = y; y += h; + return true; + } +}; + +} // namespace info diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 82fa785b..b53563b7 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -31,13 +31,7 @@ class SettingsScreen : public UIScreen { SCHEMA_SOUND, // volume, quiet hours, what plays for what // Home pages section SECTION_HOME_PAGES, - HOME_CLOCK, HOME_FAVOURITES, HOME_RADIO, HOME_BT, HOME_ADVERT, -#if ENV_INCLUDE_GPS == 1 - HOME_GPS, -#endif -#if UI_SENSORS_PAGE == 1 - HOME_SENSORS, -#endif + HOME_CLOCK, HOME_FAVOURITES, HOME_STATUS, HOME_BT, HOME_ADVERT, HOME_SETTINGS, HOME_QUICK_MSG, HOME_TOOLS, HOME_SHUTDOWN, HOME_MAP, // Radio section @@ -257,17 +251,10 @@ class SettingsScreen : public UIScreen { } bool isHomePage(int item) const { - return item == HOME_CLOCK || item == HOME_RADIO || item == HOME_BT || + return item == HOME_CLOCK || item == HOME_STATUS || item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS || item == HOME_SHUTDOWN || item == HOME_SETTINGS || item == HOME_QUICK_MSG || - item == HOME_FAVOURITES || item == HOME_MAP -#if ENV_INCLUDE_GPS == 1 - || item == HOME_GPS -#endif -#if UI_SENSORS_PAGE == 1 - || item == HOME_SENSORS -#endif - ; + item == HOME_FAVOURITES || item == HOME_MAP; } uint16_t homePageBit(int item) const { @@ -275,6 +262,8 @@ class SettingsScreen : public UIScreen { // SETTINGS and QUICK_MSG are always visible (no mask bit). All other pages // — including FAVOURITES — toggle via home_pages_mask. if (bit < 0 || bit == NodePrefs::HPB_SETTINGS || bit == NodePrefs::HPB_QUICK_MSG) return 0; + // Status took over the Radio, GPS and Sensors pages: it owns all three bits. + if (item == HOME_STATUS) return NodePrefs::HP_RADIO | NodePrefs::HP_GPS | NodePrefs::HP_SENSORS; return (uint16_t)(1 << bit); } @@ -300,15 +289,9 @@ class SettingsScreen : public UIScreen { int homePageBitIndex(int item) const { if (item == HOME_CLOCK) return NodePrefs::HPB_CLOCK; if (item == HOME_FAVOURITES) return NodePrefs::HPB_FAVOURITES; - if (item == HOME_RADIO) return NodePrefs::HPB_RADIO; + if (item == HOME_STATUS) return NodePrefs::HPB_RADIO; // the old Radio slot if (item == HOME_BT) return NodePrefs::HPB_BLUETOOTH; if (item == HOME_ADVERT) return NodePrefs::HPB_ADVERT; -#if ENV_INCLUDE_GPS == 1 - if (item == HOME_GPS) return NodePrefs::HPB_GPS; -#endif -#if UI_SENSORS_PAGE == 1 - if (item == HOME_SENSORS) return NodePrefs::HPB_SENSORS; -#endif if (item == HOME_TOOLS) return NodePrefs::HPB_TOOLS; if (item == HOME_SHUTDOWN) return NodePrefs::HPB_SHUTDOWN; if (item == HOME_MAP) return NodePrefs::HPB_MAP; @@ -982,7 +965,9 @@ public: } if (enter && homePageToggleable(_selected)) { if (!p->home_pages_mask) p->home_pages_mask = NodePrefs::HP_ALL; - p->home_pages_mask ^= homePageBit(_selected); + // Set or clear the page's bits together (Status has three). + if (homePageVisible(_selected, p)) p->home_pages_mask &= ~homePageBit(_selected); + else p->home_pages_mask |= homePageBit(_selected); _dirty = true; return true; } diff --git a/examples/companion_radio/ui-new/SatellitesScreen.h b/examples/companion_radio/ui-new/SkyView.h similarity index 78% rename from examples/companion_radio/ui-new/SatellitesScreen.h rename to examples/companion_radio/ui-new/SkyView.h index fa81b53f..0703690c 100644 --- a/examples/companion_radio/ui-new/SatellitesScreen.h +++ b/examples/companion_radio/ui-new/SkyView.h @@ -1,6 +1,6 @@ #pragma once -// Tools › Satellites (-D GPS_SKYVIEW): what the GPS receiver sees, in two -// views swapped with left / right (or Enter): +// What the GPS receiver sees (-D GPS_SKYVIEW), drawn by Status › Sky in two +// views swapped with 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 @@ -12,7 +12,7 @@ // // 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). +// The GPS is kept awake while the Sky tab is open (UITask's keep-awake list). #include #include @@ -23,13 +23,11 @@ #endif #include "icons.h" // miniIconScale() -class SatellitesScreen : public UIScreen { - UITask* _task; - bool _signal = false; // false: the sky view +namespace skyview { // The board's location provider is a MicroNMEALocationProvider wherever // GPS_SKYVIEW is set (the flag's contract, as on the L2). - static GpsSky* sky() { + inline GpsSky* sky() { #ifdef SIM_PLATFORM return gpsSkySim(); #else @@ -38,10 +36,10 @@ class SatellitesScreen : public UIScreen { #endif } - static void dot(DisplayDriver& d, int x, int y) { d.fillRect(x, y, 1, 1); } + inline 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) { + inline 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) { @@ -54,13 +52,13 @@ class SatellitesScreen : public UIScreen { } } - static int usedCount(const GpsSky& g) { + inline 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) { + inline void renderSky(DisplayDriver& d, GpsSky& g, int top) { const int lh = d.getLineHeight(); const int cw = d.getCharWidth(); const int s = miniIconScale(d); @@ -121,7 +119,7 @@ class SatellitesScreen : public UIScreen { d.setCursor(x0, y); d.print(buf); } - void renderSignal(DisplayDriver& d, GpsSky& g, int top) { + inline void renderSignal(DisplayDriver& d, GpsSky& g, int top) { const int lh = d.getLineHeight(); const int W = d.width(); @@ -172,41 +170,24 @@ class SatellitesScreen : public UIScreen { } } -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(); - + // The sky (or, with `signal`, the bars) under `top`, or why there's none. + // Returns the ms until the next redraw. + inline int render(DisplayDriver& d, int top, bool signal, bool gps_on) { + const int cx = d.width() / 2, lh = d.getLineHeight(); GpsSky* g = sky(); if (!g) { - display.drawTextCentered(cx, top + lh, "No GPS data"); + d.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; } - return drawLoadingDots(display, cx, top + 3 * lh + 4); + d.drawTextCentered(cx, top + lh, gps_on ? "Waiting for GPS" : "GPS is off"); + if (!gps_on) { d.drawTextCentered(cx, top + 2 * lh + 2, "Enter on GPS tab"); return 1000; } + return drawLoadingDots(d, cx, top + 3 * lh + 4); } - if (_signal) renderSignal(display, *g, top); - else renderSky(display, *g, top); + if (signal) renderSignal(d, *g, top); + else renderSky(d, *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; - } -}; +} // namespace skyview diff --git a/examples/companion_radio/ui-new/StatusScreen.h b/examples/companion_radio/ui-new/StatusScreen.h new file mode 100644 index 00000000..9be97040 --- /dev/null +++ b/examples/companion_radio/ui-new/StatusScreen.h @@ -0,0 +1,404 @@ +#pragma once +#include +#include +#include +#include "icons.h" +#include "InfoKit.h" +#include "PopupMenu.h" +#include "TabBar.h" +#include "../MyMesh.h" +#include "../ui-core/Diagnostics.h" +#include "../ui-core/Battery.h" +#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW) + #include "SkyView.h" + #define STATUS_HAS_SKY 1 +#elif ENV_INCLUDE_GPS == 1 + #include // the type only: no receiver data without GPS_SKYVIEW +#endif + +extern MyMesh the_mesh; + +// Home › Status (Enter on the Status page): what the device can tell about +// itself, one tab per subject. LEFT/RIGHT switches tabs, UP/DOWN scrolls a +// tab by whole blocks. +// Radio -- frequency, SF / BW / CR, TX power, the noise floor with its +// last half hour, the last packet's signal. +// GPS -- on / off (Enter toggles), fix, satellites, position, altitude, +// speed, precision, time to first fix. +// Sky -- the sky plot or the signal bars (SkyView.h); Enter swaps them. +// Power -- battery level, voltage, source, its trend, then any sensors. +// Mesh -- packet counts with a traffic line, nodes heard, contacts. +// Hold Enter resets the counters. +// System -- uptime, firmware, device, memory, queue, errors, a font card. +// Replaces Tools › Diagnostics and Tools › Satellites. Drawn with InfoKit.h. +class StatusScreen : public UIScreen { +public: + enum Tab : uint8_t { TAB_RADIO, TAB_GPS, TAB_SKY, TAB_POWER, TAB_MESH, TAB_SYSTEM, TAB_ALL }; + +private: + UITask* _task; + uint8_t _tabs[TAB_ALL]; // the tabs this board has, in order + const char* _labels[TAB_ALL]; + uint8_t _count = 0; + uint8_t _cur = 0; // index into _tabs + int _scroll = 0; // blocks scrolled off the top + bool _more = false; // the last frame cut blocks off at the bottom + bool _signal = false; // Sky: the bars instead of the plot + PopupMenu _reset_menu; // Mesh, Hold Enter -> confirm + CayenneLPP _lpp; + unsigned long _lpp_at = 0; + + // History lines, one sample a minute (32 min). + static const unsigned long SAMPLE_MS = 60000; + unsigned long _next_sample = 0; + uint32_t _last_rx = 0; + info::History<32> _noise, _batt, _traffic; + + void addTab(uint8_t t, const char* label) { _tabs[_count] = t; _labels[_count] = label; _count++; } + uint8_t tab() const { return _tabs[_cur]; } + + static uint32_t totalRx() { + uint32_t n = 0; + for (uint8_t t = 0; t < 16; t++) n += the_mesh.getNumRecvByType(t); + return n; + } + static uint32_t totalTx() { + uint32_t n = 0; + for (uint8_t t = 0; t < 16; t++) n += the_mesh.getNumSentByType(t); + return n; + } + + int battMv() const { return _task->getBattMilliVolts(); } + int battPct() const { + NodePrefs* p = the_mesh.getNodePrefs(); + return battery::percent(battMv(), p ? (int)p->low_batt_mv : 0); + } + bool imperial() const { + NodePrefs* p = the_mesh.getNodePrefs(); + return p && p->units_imperial; + } + + // One label/value row as a block. + // A long value takes the line under its label (no cursor here to scroll it). + void row(DisplayDriver& d, info::Flow& f, int reserve, const char* label, const char* value) { + if (f.place(info::rowH(d, label, value, reserve))) info::rowWrapped(d, f.at, label, value, reserve); + } + void section(DisplayDriver& d, info::Flow& f, int reserve, const char* label) { + if (f.place(d.lineStep())) info::section(d, f.at, label, reserve); + } + + // ── tabs ────────────────────────────────────────────────────────────── + void tabRadio(DisplayDriver& d, info::Flow& f, int rsv) { + NodePrefs* p = the_mesh.getNodePrefs(); + char a[24], b[24]; + const int lh = d.getLineHeight(); + if (f.place(info::bigH(d) + 2)) { + snprintf(a, sizeof(a), "%.3f", p ? p->freq : 0.0f); + info::big(d, 1, f.at, a, "MHz"); + } + if (f.place(lh + 5)) { + int x = 1; + snprintf(a, sizeof(a), "SF%u", p ? (unsigned)p->sf : 0); x = info::chip(d, x, f.at, a); + snprintf(a, sizeof(a), "BW%g", p ? p->bw : 0.0f); x = info::chip(d, x, f.at, a); + snprintf(a, sizeof(a), "CR%u", p ? (unsigned)p->cr : 0); info::chip(d, x, f.at, a); + } + snprintf(a, sizeof(a), "%d dBm", (int)radio_driver.getTxPower()); + row(d, f, rsv, "TX power", a); + if (f.place(d.lineStep())) { // label, the last half hour, the value now + const int nf = (int)radio_driver.getNoiseFloor(); + if (nf) snprintf(a, sizeof(a), "%d dBm", nf); else strcpy(a, "-"); + info::row(d, f.at, "Noise", a, rsv); + const int x0 = 1 + d.getTextWidth("Noise") + 6; + const int x1 = d.width() - rsv - 2 - d.getTextWidth(a) - 6; + if (x1 - x0 > 12) info::spark(d, x0, f.at, x1 - x0, lh - 1, _noise); + } + if (f.place(d.lineStep())) { + const float snr = radio_driver.getLastSNR(); + snprintf(b, sizeof(b), "%d dBm", (int)radio_driver.getLastRSSI()); + info::row(d, f.at, "Last RX", b, rsv); + drawSignalBars(d, d.width() - rsv - 2 - d.getTextWidth(b) - 5, f.at, (int)(snr * 4)); + } + snprintf(b, sizeof(b), "%.1f dB", radio_driver.getLastSNR()); + row(d, f, rsv, "Last SNR", b); + } + +#if ENV_INCLUDE_GPS == 1 + void tabGps(DisplayDriver& d, info::Flow& f, int rsv) { + char a[24]; + const int lh = d.getLineHeight(); + const bool on = _task->getGPSState(); + if (f.place(d.lineStep())) { + info::row(d, f.at, "GPS", nullptr, rsv); + info::toggle(d, d.width() - rsv - 2, f.at, on); + } + LocationProvider* loc = sensors.getLocationProvider(); +#ifdef STATUS_HAS_SKY + GpsSky* g = skyview::sky(); +#else + GpsSky* g = nullptr; +#endif + if (!loc) { row(d, f, rsv, "Receiver", "none"); return; } + const bool fix = loc->isValid(); + if (f.place(lh + 5)) { // the fix as a chip, satellites used / in view + const char* st = !on ? "OFF" : !fix ? "NO FIX" : (g && g->fix_mode == 2) ? "2D FIX" : "3D FIX"; + info::chip(d, 1, f.at, st, on && fix); +#ifdef STATUS_HAS_SKY + if (g) snprintf(a, sizeof(a), "%d/%d sats", skyview::usedCount(*g), g->count); + else +#endif + snprintf(a, sizeof(a), "%ld sats", loc->satellitesCount()); + d.drawTextRightAlign(d.width() - rsv - 2, f.at + 1, a); + } + const long lat = loc->getLatitude(), lon = loc->getLongitude(); + // The driver reports 999 deg before a first fix (0,0 in the simulator). + const bool known = labs(lat) <= 90000000L && (lat || lon); + if (known) snprintf(a, sizeof(a), "%.5f\xc2\xb0%c", labs(lat) / 1e6, lat < 0 ? 'S' : 'N'); else strcpy(a, "-"); + row(d, f, rsv, fix ? "Lat" : "Lat (last)", a); + if (known) snprintf(a, sizeof(a), "%.5f\xc2\xb0%c", labs(lon) / 1e6, lon < 0 ? 'W' : 'E'); else strcpy(a, "-"); + row(d, f, rsv, fix ? "Lon" : "Lon (last)", a); + if (fix) { + telemetry::altText(loc->getAltitude() / 1000.0f, imperial(), L1_INFO, a, sizeof(a)); + row(d, f, rsv, "Altitude", a); + } + if (g && fix) { + if (imperial()) snprintf(a, sizeof(a), "%.0f mph", g->speed_kmh * 0.621371f); + else snprintf(a, sizeof(a), "%.0f km/h", g->speed_kmh); + row(d, f, rsv, "Speed", a); + } + if (g) { + if (g->hdop > 0 && g->hdop < 99) snprintf(a, sizeof(a), "%.1f", g->hdop); else strcpy(a, "-"); + row(d, f, rsv, "HDOP", a); + if (g->ttff_ms) snprintf(a, sizeof(a), "%lu s", (unsigned long)(g->ttff_ms / 1000)); + else if (g->start_ms) snprintf(a, sizeof(a), "%lu s", (unsigned long)((millis() - g->start_ms) / 1000)); + else strcpy(a, "-"); + row(d, f, rsv, g->ttff_ms ? "First fix" : "Searching", a); + } + } +#endif + + void tabPower(DisplayDriver& d, info::Flow& f, int rsv) { + char a[24]; + const int mv = battMv(), pct = battPct(); + if (f.place(info::bigH(d) + 2)) { + snprintf(a, sizeof(a), "%d", pct); + const int w = info::big(d, 1, f.at, a, "%"); + const int mx = 1 + w + 8, mh = d.getLineHeight() + 2; + info::meter(d, mx, f.at + info::bigH(d) - mh - 1, d.width() - rsv - 2 - mx, mh, pct / 100.0f); + } + snprintf(a, sizeof(a), "%d.%02d V", mv / 1000, (mv % 1000) / 10); + row(d, f, rsv, "Battery", a); + row(d, f, rsv, "Source", board.isExternalPowered() ? "USB" : "Battery"); + if (f.place(d.lineStep())) { + d.setCursor(1, f.at); + d.print("Trend"); + const int x0 = 1 + d.getTextWidth("Trend") + 6; + info::spark(d, x0, f.at, d.width() - rsv - 2 - x0, d.getLineHeight() - 1, _batt, 20); + } + + // Sensors: every reading but the battery (above) and the position (GPS). + if (!_lpp_at || millis() - _lpp_at > 5000) { // both passes (measure, draw) read one query + _lpp.reset(); + sensors.querySensors(0xFF, _lpp); + _lpp_at = millis() | 1; + } + static const struct { uint8_t type; const char* name; } NAMES[] = { + { LPP_TEMPERATURE, "Temperature" }, { LPP_RELATIVE_HUMIDITY, "Humidity" }, + { LPP_BAROMETRIC_PRESSURE, "Pressure" }, { LPP_ALTITUDE, "Altitude" }, + { LPP_CURRENT, "Current" }, { LPP_POWER, "Power" }, { LPP_LUMINOSITY, "Light" }, + { LPP_PERCENTAGE, "Moisture" }, { LPP_DISTANCE, "Distance" }, { LPP_CONCENTRATION, "CO2" }, + }; + bool titled = false; + LPPReader r(_lpp.getBuffer(), _lpp.getSize()); + uint8_t ch, type; + while (r.readHeader(ch, type)) { + const char* name = nullptr; + for (auto& n : NAMES) if (n.type == type) { name = n.name; break; } + if (!name) { r.skipData(type); continue; } + telemetry::lppText(r, type, imperial(), L1_INFO, a, sizeof(a)); + if (!titled) { section(d, f, rsv, "Sensors"); titled = true; } + row(d, f, rsv, name, a); + } + } + + void tabMesh(DisplayDriver& d, info::Flow& f, int rsv) { + char a[24]; + const int lh = d.getLineHeight(); + if (f.place(d.lineStep())) { + snprintf(a, sizeof(a), "RX %lu", (unsigned long)totalRx()); + d.setCursor(1, f.at); d.print(a); + snprintf(a, sizeof(a), "TX %lu", (unsigned long)totalTx()); + d.drawTextRightAlign(d.width() - rsv - 2, f.at, a); + } + if (f.place(lh + 6)) info::spark(d, 1, f.at, d.width() - rsv - 3, lh + 3, _traffic, 4); + + static const uint8_t MSG[] = { PAYLOAD_TYPE_TXT_MSG, PAYLOAD_TYPE_GRP_TXT }; + static const uint8_t ADVERT[] = { PAYLOAD_TYPE_ADVERT }; + static const uint8_t ROUTE[] = { PAYLOAD_TYPE_ACK, PAYLOAD_TYPE_PATH, PAYLOAD_TYPE_TRACE }; + snprintf(a, sizeof(a), "%lu / %lu", (unsigned long)diag::sumByTypes(true, MSG, 2), (unsigned long)diag::sumByTypes(false, MSG, 2)); + row(d, f, rsv, "Messages", a); + snprintf(a, sizeof(a), "%lu / %lu", (unsigned long)diag::sumByTypes(true, ADVERT, 1), (unsigned long)diag::sumByTypes(false, ADVERT, 1)); + row(d, f, rsv, "Adverts", a); + snprintf(a, sizeof(a), "%lu / %lu", (unsigned long)diag::sumByTypes(true, ROUTE, 3), (unsigned long)diag::sumByTypes(false, ROUTE, 3)); + row(d, f, rsv, "Acks, paths", a); + snprintf(a, sizeof(a), "%lu", (unsigned long)the_mesh.getNumForwarded()); + row(d, f, rsv, "Forwarded", a); + snprintf(a, sizeof(a), "%d", heardLastHour()); + row(d, f, rsv, "Heard in 1 h", a); + snprintf(a, sizeof(a), "%d", the_mesh.getNumContacts()); + row(d, f, rsv, "Contacts", a); + } + + void tabSystem(DisplayDriver& d, info::Flow& f, int rsv) { + char a[32]; + uint32_t up = millis() / 1000; + unsigned long dd = up / 86400, h = (up % 86400) / 3600, m = (up % 3600) / 60; + if (dd) snprintf(a, sizeof(a), "%lud %luh %lum", dd, h, m); + else snprintf(a, sizeof(a), "%luh %lum", h, m); + row(d, f, rsv, "Uptime", a); + diag::shortVersion(a, sizeof(a)); + row(d, f, rsv, "Firmware", a); + row(d, f, rsv, "Built", FIRMWARE_BUILD_DATE); + row(d, f, rsv, "Device", board.getManufacturerName()); + uint32_t heap_free, heap_total; + DeviceDiag::getHeapStats(heap_free, heap_total); + if (heap_total) snprintf(a, sizeof(a), "%lu of %lu KB", (unsigned long)(heap_free / 1024), (unsigned long)(heap_total / 1024)); + else strcpy(a, "-"); + row(d, f, rsv, "Free heap", a); + snprintf(a, sizeof(a), "%lu B", (unsigned long)DeviceDiag::getStackFreeBytes()); + row(d, f, rsv, "Free stack", a); + snprintf(a, sizeof(a), "%d", the_mesh.getPoolFreeCount()); + row(d, f, rsv, "Packet pool", a); + snprintf(a, sizeof(a), "%d", the_mesh.getOutboundQueueLen()); + row(d, f, rsv, "Send queue", a); + const uint16_t err = the_mesh.getErrFlags(); + if (!err) strcpy(a, "none"); + else snprintf(a, sizeof(a), "%s%s%s", (err & ERR_EVENT_FULL) ? "full " : "", + (err & ERR_EVENT_CAD_TIMEOUT) ? "CAD " : "", (err & ERR_EVENT_STARTRX_TIMEOUT) ? "RX" : ""); + row(d, f, rsv, "Errors", a); + section(d, f, rsv, "Font"); + diag::Line lines[diag::MAX_LINES]; + const int n = diag::fontLines(lines); + for (int i = 0; i < n; i++) + if (f.place(d.lineStep())) d.drawTextEllipsized(1, f.at, d.width() - rsv - 3, lines[i]); + } + + // All of a tab's blocks, drawn (or, through a measuring Flow, only counted). + void layout(DisplayDriver& d, info::Flow& f, int rsv) { + switch (tab()) { + case TAB_RADIO: tabRadio(d, f, rsv); break; +#if ENV_INCLUDE_GPS == 1 + case TAB_GPS: tabGps(d, f, rsv); break; +#endif + case TAB_POWER: tabPower(d, f, rsv); break; + case TAB_MESH: tabMesh(d, f, rsv); break; + case TAB_SYSTEM: tabSystem(d, f, rsv); break; + default: break; + } + } + +public: + StatusScreen(UITask* task) : _task(task), _lpp(160) { + addTab(TAB_RADIO, "Radio"); +#if ENV_INCLUDE_GPS == 1 + addTab(TAB_GPS, "GPS"); +#endif +#ifdef STATUS_HAS_SKY + addTab(TAB_SKY, "Sky"); +#endif + addTab(TAB_POWER, "Power"); + addTab(TAB_MESH, "Mesh"); + addTab(TAB_SYSTEM, "System"); + } + + // Open on a given tab (Status page: Radio; the GPS shortcuts: GPS / Sky). + void showTab(uint8_t t) { + for (uint8_t i = 0; i < _count; i++) if (_tabs[i] == t) { _cur = i; _scroll = 0; } + } + // GPS stays awake while its live tabs are on screen. + bool wantsLiveGps() const { return tab() == TAB_GPS || tab() == TAB_SKY; } + + // Called from the UI loop on every pass: one sample a minute for the lines. + void sample() { + const unsigned long now = millis(); + if (_next_sample && (long)(now - _next_sample) < 0) return; + _next_sample = now + SAMPLE_MS; + const int nf = (int)radio_driver.getNoiseFloor(); + if (nf) _noise.push((int16_t)nf); + _batt.push((int16_t)battMv()); + const uint32_t rx = totalRx(); + if (_last_rx || rx) _traffic.push((int16_t)(rx - _last_rx > 30000 ? 30000 : rx - _last_rx)); + _last_rx = rx; + } + + // Contacts heard from in the last hour (cached for 10 s: it walks them all). + static int heardLastHour() { + static unsigned long at = 0; + static int n = 0; + if (at && millis() - at < 10000) return n; + at = millis() | 1; + n = 0; + const uint32_t now = rtc_clock.getCurrentTime(); + const int nc = the_mesh.getNumContacts(); + for (int i = 0; i < nc; i++) { + ContactInfo ci; + if (!the_mesh.getContactByIdx(i + MAX_ANON_CONTACTS, ci)) continue; + if (ci.lastmod && ci.lastmod + 3600 >= now) n++; + } + return n; + } + + int render(DisplayDriver& display) override { + display.setTextSize(1); + display.setColor(DisplayDriver::LIGHT); + tabbar::draw(display, _labels, _count, _cur); + const int top = display.listStart(), bottom = display.height(); + int next = 1000; + +#ifdef STATUS_HAS_SKY + if (tab() == TAB_SKY) { + next = skyview::render(display, top, _signal, _task->getGPSState()); + } else +#endif + { + // Measure first, so a tab taller than the screen gets its scroll column. + info::Flow m = info::Flow::measure(); + layout(display, m, 0); + const long view = bottom - top; + const int rsv = m.total_px > view ? scrollIndicatorColWidth(display) : 0; + info::Flow f(top, bottom, _scroll); + layout(display, f, rsv); + _more = f.more; + if (rsv) drawScrollIndicatorPx(display, top, view, f.total_px, view, f.skipped_px); + } + + display.setColor(DisplayDriver::LIGHT); + if (_reset_menu.active) { _reset_menu.render(display); return 50; } + return next; + } + + bool handleInput(char c) override { + if (_reset_menu.active) { + auto res = _reset_menu.handleInput(c); + if (res == PopupMenu::SELECTED && _reset_menu.selectedIndex() == 0) { + diag::resetCounters(); + _last_rx = 0; + _task->showAlert("Counters reset", 800); + } + return true; + } + if (keyIsPrev(c)) { _cur = (_cur + _count - 1) % _count; _scroll = 0; return true; } + if (keyIsNext(c)) { _cur = (_cur + 1) % _count; _scroll = 0; return true; } + if (c == KEY_UP) { if (_scroll > 0) _scroll--; return true; } + if (c == KEY_DOWN) { if (_more) _scroll++; return true; } +#if ENV_INCLUDE_GPS == 1 + if (c == KEY_ENTER && tab() == TAB_GPS) { _task->toggleGPS(); return true; } +#endif + if (c == KEY_ENTER && tab() == TAB_SKY) { _signal = !_signal; return true; } + if (c == KEY_CONTEXT_MENU && tab() == TAB_MESH) { + _reset_menu.beginConfirm("Reset counters?", "Reset"); + return true; + } + if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; } + return false; + } +}; diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 71b06d2b..529e91b4 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -13,9 +13,9 @@ class ToolsScreen : public UIScreen { UITask* _task; enum Action { - ACT_NEARBY, ACT_LIVESHARE, ACT_TRAIL, ACT_LOCATOR, ACT_COMPASS, ACT_SATELLITES, + ACT_NEARBY, ACT_LIVESHARE, ACT_TRAIL, ACT_LOCATOR, ACT_COMPASS, ACT_BOT, ACT_AUTOADVERT, ACT_REPEATER, ACT_ADMIN, - ACT_CLOCK, ACT_RINGTONE, ACT_DIAGNOSTICS + ACT_CLOCK, ACT_RINGTONE #if defined(PIN_GPIO1) , ACT_GPIO #endif @@ -54,14 +54,12 @@ 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; case ACT_ADMIN: _task->pickAdminTarget(); break; // Admin is remote-only: pick a node first case ACT_CLOCK: _task->gotoClockTools(); break; case ACT_RINGTONE: _task->gotoRingtoneEditor(); break; - case ACT_DIAGNOSTICS: _task->gotoDiagnosticsScreen(); break; #if defined(PIN_GPIO1) case ACT_GPIO: _task->gotoGpioScreen(); break; #endif @@ -129,9 +127,6 @@ 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 }, @@ -142,7 +137,6 @@ const ToolsScreen::Tool ToolsScreen::COMMS_TOOLS[] = { const ToolsScreen::Tool ToolsScreen::SYSTEM_TOOLS[] = { { "Clock Tools", &ICON_ALARM, ACT_CLOCK }, { "Ringtone Editor", &ICON_NOTE, ACT_RINGTONE }, - { "Diagnostics", &ICON_CHART, ACT_DIAGNOSTICS }, #if defined(PIN_GPIO1) { "GPIO", &ICON_PINS, ACT_GPIO }, #endif diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 394594a4..bbb59315 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -157,6 +157,15 @@ static const int QUICK_MSGS_MAX = 10; // scope (e.g. NearbyScreen::FILTER_LABELS), so including any of them from a // second .cpp is a duplicate-symbol link error. Keep them UITask-internal; // anything genuinely shareable belongs in a real header (icons.h, GeoUtils.h). +// Telemetry (ui-core/Telemetry.h) in this display's font and width: tight for +// the dashboard's short fields, spaced ("49 m") on the Status screen's rows. +static const telemetry::Style L1_TELEMETRY = { "\xc2\xb0", false, false, 3 }; +static const telemetry::Style L1_INFO = { "\xc2\xb0", true, false, 5 }; +// Altitude (baro or GPS) in Settings > System > Units, always the small unit. +static void fmtAlt(char* buf, int n, float meters, bool imperial) { + telemetry::altText(meters, imperial, L1_TELEMETRY, buf, n); +} + #include "FullscreenMsgView.h" #include "../ui-core/MessageText.h" #include "SensorPlaceholders.h" @@ -178,10 +187,7 @@ 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 "StatusScreen.h" // Home › Status (was Tools › Diagnostics + Satellites) #include "RepeaterScreen.h" #if defined(PIN_GPIO1) #include "GpioScreen.h" @@ -276,27 +282,15 @@ static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti, static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv, uint16_t low_batt_mv, int unread, bool unread_overflow, bool imperial, CayenneLPP* lpp, bool nouns); -// Telemetry (ui-core/Telemetry.h) in this display's font and width. -static const telemetry::Style L1_TELEMETRY = { "\xf8", false, false, 3 }; -// Altitude (baro or GPS) in Settings > System > Units, always the small unit. -static void fmtAlt(char* buf, int n, float meters, bool imperial) { - telemetry::altText(meters, imperial, L1_TELEMETRY, buf, n); -} class HomeScreen : public UIScreen { enum HomePage { CLOCK, FAVOURITES, RECENT, - RADIO, + STATUS, // radio, GPS, power, mesh at a glance (took over Radio / GPS / Sensors) BLUETOOTH, ADVERT, -#if ENV_INCLUDE_GPS == 1 - GPS, -#endif -#if UI_SENSORS_PAGE == 1 - SENSORS, -#endif SETTINGS, MAP, TOOLS, @@ -367,15 +361,9 @@ class HomeScreen : public UIScreen { if (page == CLOCK) return NodePrefs::HPB_CLOCK; if (page == FAVOURITES) return NodePrefs::HPB_FAVOURITES; if (page == RECENT) return NodePrefs::HPB_RECENT; - if (page == RADIO) return NodePrefs::HPB_RADIO; + if (page == STATUS) return NodePrefs::HPB_RADIO; // the old Radio slot if (page == BLUETOOTH) return NodePrefs::HPB_BLUETOOTH; if (page == ADVERT) return NodePrefs::HPB_ADVERT; -#if ENV_INCLUDE_GPS == 1 - if (page == GPS) return NodePrefs::HPB_GPS; -#endif -#if UI_SENSORS_PAGE == 1 - if (page == SENSORS) return NodePrefs::HPB_SENSORS; -#endif if (page == TOOLS) return NodePrefs::HPB_TOOLS; if (page == SHUTDOWN) return NodePrefs::HPB_SHUTDOWN; if (page == MAP) return NodePrefs::HPB_MAP; @@ -389,15 +377,9 @@ class HomeScreen : public UIScreen { case NodePrefs::HPB_CLOCK: return CLOCK; case NodePrefs::HPB_FAVOURITES: return FAVOURITES; case NodePrefs::HPB_RECENT: return RECENT; - case NodePrefs::HPB_RADIO: return RADIO; + case NodePrefs::HPB_RADIO: return STATUS; case NodePrefs::HPB_BLUETOOTH: return BLUETOOTH; case NodePrefs::HPB_ADVERT: return ADVERT; -#if ENV_INCLUDE_GPS == 1 - case NodePrefs::HPB_GPS: return GPS; -#endif -#if UI_SENSORS_PAGE == 1 - case NodePrefs::HPB_SENSORS: return SENSORS; -#endif case NodePrefs::HPB_TOOLS: return TOOLS; case NodePrefs::HPB_SHUTDOWN: return SHUTDOWN; case NodePrefs::HPB_SETTINGS: return SETTINGS; @@ -412,6 +394,8 @@ class HomeScreen : public UIScreen { int bit = pageBit(page); if (bit < 0) return true; uint16_t mask = (_node_prefs && _node_prefs->home_pages_mask) ? _node_prefs->home_pages_mask : NodePrefs::HP_ALL; + // Status stands in for the Radio, GPS and Sensors pages: shown if any was. + if (page == STATUS) return (mask & (NodePrefs::HP_RADIO | NodePrefs::HP_GPS | NodePrefs::HP_SENSORS)) != 0; return (mask >> bit) & 1; } @@ -547,7 +531,6 @@ class HomeScreen : public UIScreen { CayenneLPP sensors_lpp; int sensors_nb = 0; - int sensors_scroll_offset = 0; int next_sensors_refresh = 0; void refresh_sensors() { @@ -725,21 +708,75 @@ public: return nearest_km; } + // The Status page: four tiles split by dotted rules, each one subject's + // headline (icon + main value) over a detail line. Enter opens the Status + // screen with all of it. + void drawStatusTiles(DisplayDriver& d, int top) { + const int W = d.width(), H = d.height() - top; + const int lh = d.getLineHeight(), s = miniIconScale(d); + const int mid_x = W / 2, mid_y = top + H / 2; + d.setColor(DisplayDriver::LIGHT); + info::vrule(d, mid_x, top, H); + info::rule(d, 0, mid_y, W); + auto tile = [&](int col, int row, const MiniIcon* ic, float batt, const char* head, const char* detail) { + const int x = col ? mid_x + 3 : 1, y = row ? mid_y + 2 : top + 1; + const int w = (col ? W - x : mid_x - 2 - x); + const int iy = y + (lh - 5 * s) / 2 - s; + int tx = x; + if (ic) { miniIconDraw(d, x, iy, *ic); tx = x + ic->w * s + 2 * s; } + else if (batt >= 0) { info::battery(d, x, iy, batt); tx = x + info::batteryW(d) + 2 * s; } + d.drawTextEllipsized(tx, y, x + w - tx, head); + d.drawTextEllipsized(x, y + lh + 2, w, detail); + }; + char h[20], t[20]; + + snprintf(h, sizeof(h), "%.3f", _node_prefs->freq); + snprintf(t, sizeof(t), "SF%u %gk", (unsigned)_node_prefs->sf, _node_prefs->bw); + tile(0, 0, &ICON_PG_RADIO, -1, h, t); + +#if ENV_INCLUDE_GPS == 1 + { + LocationProvider* loc = sensors.getLocationProvider(); + const bool on = _task->getGPSState(); + t[0] = 0; + if (!loc) strcpy(h, "No GPS"); + else if (!on) strcpy(h, "GPS off"); + else if (!loc->isValid()) strcpy(h, "No fix"); + else { +#ifdef STATUS_HAS_SKY + GpsSky* g = skyview::sky(); + strcpy(h, g && g->fix_mode == 2 ? "2D fix" : "3D fix"); +#else + strcpy(h, "Fix"); +#endif + } + if (loc && on) snprintf(t, sizeof(t), "%ld sats", loc->satellitesCount()); + tile(1, 0, &ICON_PG_GPS, -1, h, t); + } +#else + tile(1, 0, &ICON_PG_GPS, -1, "No GPS", ""); +#endif + + const int mv = _task->getBattMilliVolts(); + const int pct = battMvToPercent(mv, _node_prefs ? (int)_node_prefs->low_batt_mv : 0); + snprintf(h, sizeof(h), "%d%%", pct); + snprintf(t, sizeof(t), "%d.%02d V%s", mv / 1000, (mv % 1000) / 10, board.isExternalPowered() ? " USB" : ""); + tile(0, 1, nullptr, pct / 100.0f, h, t); + + snprintf(h, sizeof(h), "%d nodes", the_mesh.getNumContacts()); + snprintf(t, sizeof(t), "%d in 1 h", StatusScreen::heardLastHour()); + tile(1, 1, &ICON_MAP_CONTACT, -1, h, t); + } + // Small 5x5 glyph shown in the page-indicator row for each HomePage. static const MiniIcon* pageIcon(int page) { switch (page) { case CLOCK: return &ICON_PG_CLOCK; case FAVOURITES: return &ICON_PG_STAR; case RECENT: return &ICON_PG_RECENT; - case RADIO: return &ICON_PG_RADIO; + case STATUS: return &ICON_CHART; case BLUETOOTH: return &ICON_PG_BT; case ADVERT: return &ICON_PG_ADVERT; -#if ENV_INCLUDE_GPS == 1 - case GPS: return &ICON_PG_GPS; -#endif -#if UI_SENSORS_PAGE == 1 - case SENSORS: return &ICON_PG_SENSORS; -#endif case SETTINGS: return &ICON_PG_SETTINGS; case MAP: return &ICON_PG_MAP; case TOOLS: return &ICON_PG_TOOLS; @@ -994,29 +1031,8 @@ public: display.drawPanel(hx - p, hy - p, hw + p*2, lk_lh + p*2); display.setCursor(hx, hy); display.print(hint); - } else if (_page == HomePage::RADIO) { - display.setColor(DisplayDriver::LIGHT); - // freq / sf - display.setCursor(0, content_y); - snprintf(tmp, sizeof(tmp),"FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf); - display.print(tmp); - - display.setCursor(0, content_y + step); - snprintf(tmp, sizeof(tmp),"BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); - display.print(tmp); - - // tx power, noise floor - display.setCursor(0, content_y + step * 2); - snprintf(tmp, sizeof(tmp),"TX: %ddBm", radio_driver.getTxPower()); // live value (reflects APC) - display.print(tmp); - display.setCursor(0, content_y + step * 3); - // Was gated to "n/a" while duty-cycle RX (Pwr save) was active, on the - // assumption that the floor only gets sampled during continuous RX -- - // stale since RadioLibWrapper's periodic recalibration (noiseFloorCalibCheck(), - // NF_CALIB_INTERVAL_MS) started keeping it fresh even under duty-cycle, - // same live value Diagnostics already showed unconditionally. - snprintf(tmp, sizeof(tmp),"Noise floor: %d", radio_driver.getNoiseFloor()); - display.print(tmp); + } else if (_page == HomePage::STATUS) { + drawStatusTiles(display, content_y); } else if (_page == HomePage::BLUETOOTH) { display.setColor(DisplayDriver::LIGHT); display.setTextSize(1); @@ -1042,114 +1058,6 @@ public: display.setColor(DisplayDriver::LIGHT); anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_ADVERT); display.drawTextCentered(display.width() / 2, content_y + BIG_ADVERT.h + HOVER_GAP, "Advert"); -#if ENV_INCLUDE_GPS == 1 - } else if (_page == HomePage::GPS) { - LocationProvider* nmea = sensors.getLocationProvider(); - char buf[50]; - int y = content_y; - bool gps_state = _task->getGPSState(); -#ifdef PIN_GPS_SWITCH - bool hw_gps_state = digitalRead(PIN_GPS_SWITCH); - if (gps_state != hw_gps_state) { - strcpy(buf, gps_state ? "gps off(hw)" : "gps off(sw)"); - } else { - strcpy(buf, gps_state ? "gps on" : "gps off"); - } -#else - strcpy(buf, gps_state ? "gps on" : "gps off"); -#endif - display.drawTextLeftAlign(0, y, buf); - if (nmea == NULL) { - y += step; - display.drawTextLeftAlign(0, y, "Can't access GPS"); - } else { - strcpy(buf, nmea->isValid()?"fix":"no fix"); - display.drawTextRightAlign(display.width()-1, y, buf); - y += step; - display.drawTextLeftAlign(0, y, "sat"); - snprintf(buf, sizeof(buf),"%d", nmea->satellitesCount()); - display.drawTextRightAlign(display.width()-1, y, buf); - y += step; - display.drawTextLeftAlign(0, y, "pos"); - // The driver reports 999 deg until a first fix: show a dash, not the - // placeholder. After a fix the last known position stays. - if (labs(nmea->getLatitude()) > 90000000L) strcpy(buf, "-"); - else snprintf(buf, sizeof(buf),"%.4f %.4f", - nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); - display.drawTextRightAlign(display.width()-1, y, buf); - y += step; - display.drawTextLeftAlign(0, y, "alt"); - fmtAlt(buf, sizeof(buf), nmea->getAltitude() / 1000.0f, _node_prefs && _node_prefs->units_imperial); - display.drawTextRightAlign(display.width()-1, y, buf); - y += step; - } -#endif -#if UI_SENSORS_PAGE == 1 - } else if (_page == HomePage::SENSORS) { - int y = content_y; - refresh_sensors(); - - // Enumerate the distinct telemetry types directly from the freshly - // populated buffer. (Upstream replaced the per-sensor *_initialized flags - // with a generic registration model, so we derive availability from what - // querySensors() actually produced instead of asking the manager.) - uint8_t avail_types[16]; - int avail_count = 0; - { - LPPReader er(sensors_lpp.getBuffer(), sensors_lpp.getSize()); - uint8_t ech, etype; - while (er.readHeader(ech, etype) && avail_count < 16) { - er.skipData(etype); - bool dup = false; - for (int k = 0; k < avail_count; k++) if (avail_types[k] == etype) { dup = true; break; } - if (!dup) avail_types[avail_count++] = etype; - } - } - bool need_scroll = avail_count > UI_RECENT_LIST_SIZE; - int offset = need_scroll ? (sensors_scroll_offset % avail_count) : 0; - int show_n = need_scroll ? UI_RECENT_LIST_SIZE : avail_count; - - for (int i = 0; i < show_n; i++) { - uint8_t target = avail_types[(offset + i) % avail_count]; - - // scan LPP buffer for this type - LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize()); - uint8_t ch, type; - char buf[22] = "--"; - while (r.readHeader(ch, type)) { - if (type == target) { - telemetry::lppText(r, type, _node_prefs && _node_prefs->units_imperial, L1_TELEMETRY, buf, sizeof(buf)); - break; - } - r.skipData(type); - } - - static const struct { uint8_t type; const char* name; } TYPE_NAMES[] = { - { LPP_VOLTAGE, "voltage" }, - { LPP_GPS, "gps" }, - { LPP_TEMPERATURE, "temp" }, - { LPP_RELATIVE_HUMIDITY, "humidity" }, - { LPP_BAROMETRIC_PRESSURE,"pressure" }, - { LPP_ALTITUDE, "altitude" }, - { LPP_CURRENT, "current" }, - { LPP_POWER, "power" }, - { LPP_LUMINOSITY, "light" }, - { LPP_PERCENTAGE, "moisture" }, - { LPP_DISTANCE, "distance" }, - { LPP_CONCENTRATION, "CO2" }, - }; - const char* name = "sensor"; - for (auto& tn : TYPE_NAMES) { if (tn.type == target) { name = tn.name; break; } } - - display.setCursor(0, y); - display.print(name); - display.setCursor(display.width() - display.getTextWidth(buf) - 1, y); - display.print(buf); - y += step; - } - if (need_scroll) sensors_scroll_offset = (sensors_scroll_offset + 1) % avail_count; - else sensors_scroll_offset = 0; -#endif } else if (_page == HomePage::SETTINGS) { display.setColor(DisplayDriver::LIGHT); display.setTextSize(1); @@ -1437,19 +1345,10 @@ public: } return true; } -#if ENV_INCLUDE_GPS == 1 - if (c == KEY_ENTER && _page == HomePage::GPS) { - _task->toggleGPS(); + if (c == KEY_ENTER && _page == HomePage::STATUS) { + _task->gotoStatusScreen(StatusScreen::TAB_RADIO); return true; } -#endif -#if UI_SENSORS_PAGE == 1 - if (c == KEY_ENTER && _page == HomePage::SENSORS) { - // _task->toggleGPS(); - next_sensors_refresh=0; - return true; - } -#endif if (c == KEY_ENTER && _page == HomePage::SETTINGS) { _task->gotoSettingsScreen(); return true; @@ -1593,9 +1492,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no 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); + status_screen = new StatusScreen(this); repeater_screen = new RepeaterScreen(this); clock_tools = new ClockToolsScreen(this, node_prefs); #if defined(PIN_GPIO1) @@ -1628,8 +1526,10 @@ 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::gotoStatusScreen(uint8_t tab) { + ((StatusScreen*)status_screen)->showTab(tab); + setCurrScreen(status_screen); +} void UITask::gotoRepeaterScreen() { setCurrScreen(repeater_screen); } void UITask::gotoClockTools() { setCurrScreen(clock_tools); } void UITask::gotoGpioScreen() { @@ -2459,8 +2359,8 @@ extern uint32_t g_disp_flushes; namespace perfl1 { // Each step opens a screen directly (so the walk doesn't depend on which // Home pages are enabled), then types keys: R/L/U/D/E/X, H = hold Enter. - enum Open : uint8_t { NONE, HOME, SETTINGS, MESSAGES, TOOLS, NEARBY, TRAIL, MAP, COMPASS, SATS, - DIAG, LIVE, LOCATOR, ADVERT, REPEATER, BOT, CLOCKTOOLS, CHANNEL0 }; + enum Open : uint8_t { NONE, HOME, SETTINGS, MESSAGES, TOOLS, NEARBY, TRAIL, MAP, COMPASS, STATUS, + LIVE, LOCATOR, ADVERT, REPEATER, BOT, CLOCKTOOLS, CHANNEL0 }; struct Step { const char* name; Open open; const char* keys; uint16_t dwell_ms; }; static const Step STEPS[] = { {"boot-idle", NONE, "", 3000}, @@ -2468,6 +2368,8 @@ namespace perfl1 { {"home-R1", NONE, "R", 1500}, {"home-R2", NONE, "R", 1500}, {"home-R3", NONE, "R", 1500}, {"home-R4", NONE, "R", 1500}, {"home-R5", NONE, "R", 1500}, {"home-R6", NONE, "R", 1500}, {"home-R7", NONE, "R", 1500}, {"home-R8", NONE, "R", 1500}, + {"home-R9", NONE, "R", 1500}, {"home-R10", NONE, "R", 1500}, {"home-R11", NONE, "R", 1500}, + {"home-R12", NONE, "R", 1500}, {"home-R13", NONE, "R", 1500}, {"settings", SETTINGS, "", 2000}, {"set-display", NONE, "E", 2000}, {"set-scroll", NONE, "DDDDDDDDDDDD", 2000}, {"messages", MESSAGES, "", 2000}, {"dm-list", NONE, "E", 2000}, @@ -2478,8 +2380,11 @@ namespace perfl1 { {"kbd-close", NONE, "XXX", 1500}, {"nearby", NEARBY, "", 3000}, {"nearby-scroll", NONE, "DDDDDDDDDDDDDDDDDDDD", 2000}, {"tools", TOOLS, "", 1500}, {"trail", TRAIL, "", 2000}, {"map", MAP, "", 3000}, - {"compass", COMPASS, "", 3000}, {"sats", SATS, "", 4000}, {"sats-signal", NONE, "R", 3000}, - {"diag", DIAG, "", 4000}, {"diag-scroll", NONE, "DDDD", 3000}, + {"compass", COMPASS, "", 3000}, + {"st-radio", STATUS, "", 3000}, {"st-radio-dn", NONE, "DD", 2000}, {"st-gps", NONE, "UUR", 3000}, + {"st-gps-dn", NONE, "DDD", 2000}, {"st-sky", NONE, "UUUR", 4000}, {"st-signal", NONE, "E", 3000}, + {"st-power", NONE, "R", 2000}, {"st-mesh", NONE, "R", 2000}, {"st-mesh-dn", NONE, "DDD", 2000}, + {"st-system", NONE, "UUUR", 2000}, {"st-system-dn", NONE, "DDDDDD", 2000}, {"liveshare", LIVE, "", 1500}, {"locator", LOCATOR, "", 1500}, {"autoadvert", ADVERT, "", 1500}, {"repeater", REPEATER, "", 1500}, {"bot", BOT, "", 1500}, {"clocktools", CLOCKTOOLS, "", 1500}, {"home-end", HOME, "", 3000}, @@ -2582,16 +2487,11 @@ void UITask::loop() { perfl1::disp = _display; { static bool sz = false; if (!sz && millis() - start > 7000) { sz = true; - Serial.printf("SIZE UiCore %u Splash %u Home %u Settings %u Messages %u Tools %u Ringtone %u Bot %u Admin %u Nearby %u Dash %u AutoAdv %u LiveSh %u Locator %u Trail %u Compass %u Sats %u Diag %u Repeater %u ClockTools %u Kbd %u UITask %u StaticPool32 %u Packet %u\n", + Serial.printf("SIZE UiCore %u Splash %u Home %u Settings %u Messages %u Tools %u Ringtone %u Bot %u Admin %u Nearby %u Dash %u AutoAdv %u LiveSh %u Locator %u Trail %u Compass %u Status %u Repeater %u ClockTools %u Kbd %u UITask %u StaticPool32 %u Packet %u\n", sizeof(UiCore), sizeof(SplashScreen), sizeof(HomeScreen), sizeof(SettingsScreen), sizeof(MessagesScreen), sizeof(ToolsScreen), sizeof(RingtoneEditorScreen), sizeof(BotScreen), sizeof(AdminScreen), sizeof(NearbyScreen), sizeof(DashboardConfigScreen), sizeof(AutoAdvertScreen), sizeof(LiveShareScreen), sizeof(LocatorScreen), sizeof(TrailScreen), sizeof(CompassScreen), -#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW) - sizeof(SatellitesScreen), -#else - 0u, -#endif - sizeof(DiagnosticsScreen), sizeof(RepeaterScreen), sizeof(ClockToolsScreen), sizeof(KeyboardWidget), sizeof(UITask), + sizeof(StatusScreen), sizeof(RepeaterScreen), sizeof(ClockToolsScreen), sizeof(KeyboardWidget), sizeof(UITask), sizeof(StaticPoolPacketManager), sizeof(mesh::Packet)); TaskStatus_t st[16]; uint32_t tot = 0; UBaseType_t n = uxTaskGetSystemState(st, 16, NULL); @@ -2611,8 +2511,7 @@ void UITask::loop() { case perfl1::TRAIL: gotoTrailScreen(); break; case perfl1::MAP: gotoMapScreen(); break; case perfl1::COMPASS: gotoCompassScreen(); break; - case perfl1::SATS: gotoSatellitesScreen(); break; - case perfl1::DIAG: gotoDiagnosticsScreen(); break; + case perfl1::STATUS: gotoStatusScreen(StatusScreen::TAB_RADIO); break; case perfl1::LIVE: gotoLiveShareScreen(); break; case perfl1::LOCATOR: gotoLocatorScreen(); break; case perfl1::ADVERT: gotoAutoAdvertScreen(); break; @@ -2629,6 +2528,7 @@ void UITask::loop() { #endif pollConnection(); // BLE link state -> hasConnection() (see UITaskBase) drainCoreEvents(); // react to what the Core filed during mesh processing (alerts, wake, sounds) + if (status_screen) ((StatusScreen*)status_screen)->sample(); // Status history lines, once a minute #if UI_HAS_JOYSTICK uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION; int ev = user_btn.check(); @@ -3031,7 +2931,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 == status_screen && ((StatusScreen*)status_screen)->wantsLiveGps()) || (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 31436607..6043e224 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -116,8 +116,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* status_screen = nullptr; // Home › Status (radio, GPS, sky, power, mesh, system) UIScreen* repeater_screen = nullptr; UIScreen* clock_tools = nullptr; #if defined(PIN_GPIO1) @@ -331,8 +330,7 @@ public: void gotoTrailScreen(); void gotoMapScreen(); // opens the Trail screen directly in its Map view void gotoCompassScreen(); - void gotoSatellitesScreen(); - void gotoDiagnosticsScreen(); + void gotoStatusScreen(uint8_t tab); // StatusScreen::Tab void gotoRepeaterScreen(); void gotoGpioScreen(); // no-op on boards without user GPIO pins (see PIN_GPIO1) void gotoClockTools(); // Alarm / Timer / Stopwatch (from the home Clock page)