feat(ui-core,ui-lvgl): diagnostics rows in the Core, L2 diagnostics, auto-advert setting, compass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-25 23:01:42 +02:00
co-authored by Claude Opus 5.5
parent 8663d50412
commit 63db1e2e07
15 changed files with 666 additions and 343 deletions
@@ -0,0 +1,152 @@
#pragma once
// Device diagnostics shared by ui-new's Tools > Diagnostics and ui-lvgl's
// Settings > Diagnostics: the live counters (uptime, packets by category,
// heap / stack, radio, queue, error flags), the static system card (firmware,
// board, radio parameters) and the font test card. Each frontend lays the rows
// out its own way.
//
// Packet counts come from Dispatcher's per-type counters, grouped into a few
// categories rather than all 16 raw PAYLOAD_TYPE_* values, so they stay
// readable on a 128x64 OLED.
#include <helpers/DeviceDiag.h>
#include <stdarg.h>
namespace diag {
struct Row { const char* label; char value[20]; };
static const int MAX_ROWS = 14;
static const int MAX_LINES = 15;
typedef char Line[40];
inline void addRow(Row* rows, int& n, const char* label, const char* fmt, ...) {
if (n >= MAX_ROWS) return;
rows[n].label = label;
va_list ap; va_start(ap, fmt);
vsnprintf(rows[n].value, sizeof(rows[n].value), fmt, ap);
va_end(ap);
n++;
}
inline void addLine(Line* lines, int& n, const char* fmt, ...) {
if (n >= MAX_LINES) return;
va_list ap; va_start(ap, fmt);
vsnprintf(lines[n], sizeof(lines[n]), fmt, ap);
va_end(ap);
n++;
}
inline uint32_t sumByTypes(bool recv, const uint8_t* types, int n) {
uint32_t total = 0;
for (int i = 0; i < n; i++)
total += recv ? the_mesh.getNumRecvByType(types[i]) : the_mesh.getNumSentByType(types[i]);
return total;
}
// Live counters, label / value. Returns the row count.
inline int liveRows(Row* rows) {
int n = 0;
uint32_t up = millis() / 1000;
unsigned long d = up / 86400, h = (up % 86400) / 3600, m = (up % 3600) / 60, s = up % 60;
if (d > 0) addRow(rows, n, "Uptime", "%lud %02lu:%02lu:%02lu", d, h, m, s);
else addRow(rows, n, "Uptime", "%02lu:%02lu:%02lu", h, m, s);
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 };
static const uint8_t OTHER[] = { PAYLOAD_TYPE_REQ, PAYLOAD_TYPE_RESPONSE, PAYLOAD_TYPE_ANON_REQ,
PAYLOAD_TYPE_GRP_DATA, PAYLOAD_TYPE_MULTIPART, PAYLOAD_TYPE_CONTROL,
PAYLOAD_TYPE_RAW_CUSTOM };
unsigned long msg_rx = sumByTypes(true, MSG, 2), msg_tx = sumByTypes(false, MSG, 2);
unsigned long adv_rx = sumByTypes(true, ADVERT, 1), adv_tx = sumByTypes(false, ADVERT, 1);
unsigned long rte_rx = sumByTypes(true, ROUTE, 3), rte_tx = sumByTypes(false, ROUTE, 3);
unsigned long oth_rx = sumByTypes(true, OTHER, 7), oth_tx = sumByTypes(false, OTHER, 7);
// "12345/12345" rather than "rx12345 tx12345": 5-digit counts would collide
// with a long label on a 128 px OLED. "Total rx/tx" spells out the order once.
addRow(rows, n, "Total rx/tx", "%lu/%lu", msg_rx + adv_rx + rte_rx + oth_rx, msg_tx + adv_tx + rte_tx + oth_tx);
addRow(rows, n, "Msg", "%lu/%lu", msg_rx, msg_tx);
addRow(rows, n, "Advert", "%lu/%lu", adv_rx, adv_tx);
addRow(rows, n, "Ack/Path", "%lu/%lu", rte_rx, rte_tx);
addRow(rows, n, "Other", "%lu/%lu", oth_rx, oth_tx);
addRow(rows, n, "Forwarded", "%lu", (unsigned long)the_mesh.getNumForwarded());
uint32_t heap_free, heap_total;
DeviceDiag::getHeapStats(heap_free, heap_total);
if (heap_total > 0) addRow(rows, n, "Heap free", "%lu/%luKB", (unsigned long)(heap_free / 1024), (unsigned long)(heap_total / 1024));
else addRow(rows, n, "Heap free", "N/A");
addRow(rows, n, "Stack free", "%luB", (unsigned long)DeviceDiag::getStackFreeBytes());
addRow(rows, n, "Noise floor", "%d dBm", (int)radio_driver.getNoiseFloor());
addRow(rows, n, "RSSI/SNR", "%d/%.1f", (int)radio_driver.getLastRSSI(), radio_driver.getLastSNR());
addRow(rows, n, "Pool free", "%d", the_mesh.getPoolFreeCount());
addRow(rows, n, "Queue", "%d", the_mesh.getOutboundQueueLen());
// Radio error flags since boot / reset (F = queue full, C = CAD timeout,
// R = RX-start timeout), "OK" when none have fired.
uint16_t err = the_mesh.getErrFlags();
if (err == 0) addRow(rows, n, "Errors", "OK");
else addRow(rows, n, "Errors", "%s%s%s", (err & ERR_EVENT_FULL) ? "F " : "",
(err & ERR_EVENT_CAD_TIMEOUT) ? "C " : "", (err & ERR_EVENT_STARTRX_TIMEOUT) ? "R " : "");
if (err) { // drop the trailing space so right-alignment sits flush
char* v = rows[n - 1].value;
size_t len = strlen(v);
if (len && v[len - 1] == ' ') v[len - 1] = '\0';
}
#if FEAT_RX_POWERSAVE
// RX duty-cycle watchdog recoveries since boot / reset (soft re-arm / hard
// chip reset); 0/0 where duty-cycle power-save never arms.
addRow(rows, n, "RXPS wd s/h", "%lu/%lu", (unsigned long)radio_driver.getRxPsWatchdogSoftCount(),
(unsigned long)radio_driver.getRxPsWatchdogHardCount());
#endif
return n;
}
// Zeroes the packet / forward counters, error flags and the radio's own counts.
inline void resetCounters() {
the_mesh.resetStats();
radio_driver.resetStats();
}
// Firmware version minus the commit-hash suffix build.sh appends as the last
// dash-segment (v1.16-solo.0-abcdef -> v1.16-solo.0; the last dash, since a
// tag like v1.21-rc1 has one of its own).
inline void shortVersion(char* out, size_t n) {
const char* ver = FIRMWARE_VERSION;
const char* dash = strrchr(ver, '-');
size_t len = dash ? (size_t)(dash - ver) : strlen(ver);
if (len >= n) len = n - 1;
memcpy(out, ver, len);
out[len] = '\0';
}
// Static system card, one full-width line each. Returns the line count.
inline int systemLines(Line* lines) {
int n = 0;
char sv[24];
shortVersion(sv, sizeof(sv));
addLine(lines, n, "FW %s", sv);
addLine(lines, n, "Built %s", FIRMWARE_BUILD_DATE);
addLine(lines, n, "Dev %s", board.getManufacturerName());
addLine(lines, n, "Node %s", the_mesh.getNodeName());
NodePrefs* p = the_mesh.getNodePrefs();
if (p) {
addLine(lines, n, "Freq %.3f MHz", p->freq);
addLine(lines, n, "SF%u BW%.0f CR%u", (unsigned)p->sf, p->bw, (unsigned)p->cr);
addLine(lines, n, "TX %d dBm", (int)p->tx_power_dbm);
}
return n;
}
// One sample per script the UI font covers, to check coverage by eye. "Acc"
// takes one letter from each of the keyboard's accent-popup groups.
inline int fontLines(Line* lines) {
int n = 0;
addLine(lines, n, "Latin ABCabc xyz");
addLine(lines, n, "Acc áçďéíłñóřśťúýź");
addLine(lines, n, "Grk ΑΒΓ αβγξω");
addLine(lines, n, "Cyr АБВ абвжя");
addLine(lines, n, "Num 0123456789");
addLine(lines, n, "Sym @#&*()[]{}/\\+=");
return n;
}
} // namespace diag
@@ -112,6 +112,9 @@ static const int32_t TZ[] = { -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1,
static void optTz(uint8_t v, char* b, int n, const NodePrefs&) {
if (TZ[v] == 0) snprintf(b, n, "UTC"); else snprintf(b, n, "UTC%+ld", (long)TZ[v]);
}
// The same steps as ui-new's Tools > Auto-Advert.
static const int32_t AUTO_ADVERT[] = { 0, 30, 60, 120, 300, 600, 1800, 3600 };
static void optAutoAdvert(uint8_t v, char* b, int n, const NodePrefs&) { fmtSecs(b, n, AUTO_ADVERT[v], "Off"); }
static const int32_t INVERTED[] = { 1, 0 }; // switch over a field stored as "off" flag
static void optResend(uint8_t v, char* b, int n, const NodePrefs&) {
if (v == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u more", (unsigned)v);
@@ -151,6 +154,8 @@ static const Setting ALL[] = {
NodePrefs::LOC_SHARE_INTERVAL_COUNT, optGap, nullptr),
IDX("Heartbeat", "Resend while standing still", SEC_LIVE_SHARE, loc_share_heartbeat_idx,
NodePrefs::LOC_SHARE_HEARTBEAT_COUNT, optHeartbeat, nullptr),
MAP("Auto-advert", "Advert with your position", SEC_LIVE_SHARE, advert_auto_interval_sec, AUTO_ADVERT,
optAutoAdvert, nullptr),
SW("Arrival alert", "When you reach the target", SEC_LOCATOR, locator_enabled, rearmLocator),
IDX("Radius", nullptr, SEC_LOCATOR, locator_radius_idx,