mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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] : "";
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
#pragma once
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <Arduino.h>
|
||||
#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" };
|
||||
@@ -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 <helpers/ui/DisplayDriver.h>
|
||||
#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 <int N>
|
||||
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 <int N>
|
||||
inline void spark(DisplayDriver& d, int x, int y, int w, int h, const History<N>& 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+21
-40
@@ -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 <math.h>
|
||||
#include <helpers/sensors/GpsSky.h>
|
||||
@@ -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
|
||||
@@ -0,0 +1,404 @@
|
||||
#pragma once
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <Arduino.h>
|
||||
#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 <helpers/sensors/GpsSky.h> // 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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user