mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 11:16:39 +00:00
feat(ui-lvgl): readable info cards for diagnostics, GPS, node detail and more
Shared infoCard/infoRow/infoNote helpers: muted label left, value right, hairlines between rows, empty rows hidden. Diagnostics grouped into sections with a received/sent packet table; GPS gets a position card and constellation pills; About, OTA, Storage and map popups use the same cards. Text no longer breaks after '.', clock alarm hint unclipped. Sim-only sim_open() hook for jumping to a screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -148,10 +148,14 @@ void UITask::buildClock() {
|
||||
lv_obj_set_size(col, 120, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_style_pad_row(col, 6, 0);
|
||||
lv_obj_set_style_pad_row(col, 4, 0);
|
||||
lv_obj_set_style_pad_left(col, 8, 0);
|
||||
label(col, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* sw = s_alarm_sw = lv_switch_create(col);
|
||||
lv_obj_t* on = flexBox(col, LV_FLEX_FLOW_ROW); // the switch and its label: one line
|
||||
lv_obj_set_flex_align(on, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(on, 8, 0);
|
||||
lv_obj_set_style_pad_bottom(on, 4, 0);
|
||||
lv_obj_t* sw = s_alarm_sw = lv_switch_create(on);
|
||||
label(on, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_set_size(sw, 50, 26);
|
||||
if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
|
||||
@@ -11,7 +11,10 @@ enum : uint8_t { TAB_LIVE, TAB_SYSTEM, TAB_FONT, TAB_COUNT };
|
||||
static uint8_t s_tab = TAB_LIVE; // kept across visits
|
||||
static lv_obj_t* s_list = nullptr;
|
||||
static const int EXTRA = 4; // GPS, power, last reset, last crash (L2 only)
|
||||
static lv_obj_t* s_vals[diag::MAX_ROWS + EXTRA];
|
||||
// A Live row's value labels: `in` / `out` for a packet count ("12/3" from
|
||||
// ui-core), else `val` alone.
|
||||
struct LiveVal { lv_obj_t* val; lv_obj_t* in; lv_obj_t* out; };
|
||||
static LiveVal s_vals[diag::MAX_ROWS + EXTRA];
|
||||
static int s_rows = 0;
|
||||
|
||||
static void extraRow(diag::Row* rows, int& n, const char* lbl, const char* fmt, ...) {
|
||||
@@ -44,6 +47,75 @@ static int allRows(diag::Row* rows, bool gps_on) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// ui-core's labels are cut for a 128 px OLED; here each gets a section and a
|
||||
// full name. Packet rows are "received/sent" pairs, shown in two columns.
|
||||
enum : uint8_t { SEC_DEVICE, SEC_PACKETS, SEC_RADIO, SEC_MEMORY, SEC_COUNT };
|
||||
static const char* const SEC_TITLE[SEC_COUNT] = { "DEVICE", "PACKETS", "RADIO", "MEMORY" };
|
||||
struct Name { const char* core; uint8_t sec; const char* text; bool pair; };
|
||||
static const Name NAMES[] = {
|
||||
{ "Uptime", SEC_DEVICE, "Uptime", false }, { "Last start", SEC_DEVICE, "Last start", false },
|
||||
{ "Last crash", SEC_DEVICE, "Last crash", false }, { "Power", SEC_DEVICE, "Power", false },
|
||||
{ "GPS", SEC_DEVICE, "GPS", false },
|
||||
{ "Total rx/tx", SEC_PACKETS, "All", true }, { "Msg", SEC_PACKETS, "Messages", true },
|
||||
{ "Advert", SEC_PACKETS, "Adverts", true }, { "Ack/Path", SEC_PACKETS, "Acks & paths", true },
|
||||
{ "Other", SEC_PACKETS, "Other", true }, { "Forwarded", SEC_PACKETS, "Forwarded", false },
|
||||
{ "Noise floor", SEC_RADIO, "Noise floor", false }, { "RSSI/SNR", SEC_RADIO, "Last packet", false },
|
||||
{ "Queue", SEC_RADIO, "Send queue", false }, { "Errors", SEC_RADIO, "Errors", false },
|
||||
{ "RXPS wd s/h", SEC_RADIO, "RX watchdog soft / hard", false },
|
||||
{ "Heap free", SEC_MEMORY, "Heap free", false }, { "Stack free", SEC_MEMORY, "Stack free", false },
|
||||
{ "Pool free", SEC_MEMORY, "Packet pool free", false },
|
||||
};
|
||||
static const Name* nameOf(const char* core) {
|
||||
for (const Name& n : NAMES) if (!strcmp(n.core, core)) return &n;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// A value as shown: "-60/40.0" -> "-60 dBm, SNR 40.0"; "12/456KB" -> "12 of 456 KB".
|
||||
static void showValue(const char* core, const char* v, char* out, size_t n) {
|
||||
const char* slash = strchr(v, '/');
|
||||
if (!strcmp(core, "RSSI/SNR") && slash) snprintf(out, n, "%.*s dBm, SNR %s", (int)(slash - v), v, slash + 1);
|
||||
else if (!strcmp(core, "Heap free") && slash) snprintf(out, n, "%.*s of %s", (int)(slash - v), v, slash + 1);
|
||||
else snprintf(out, n, "%s", v);
|
||||
size_t l = strlen(out); // "KB" / "B" units get their space
|
||||
if (l > 2 && !strcmp(out + l - 2, "KB") && out[l - 3] != ' ') snprintf(out + l - 2, n - (l - 2), " KB");
|
||||
else if (l > 1 && out[l - 1] == 'B' && out[l - 2] >= '0' && out[l - 2] <= '9') snprintf(out + l - 1, n - (l - 1), " B");
|
||||
}
|
||||
|
||||
static void setLive(int i, const diag::Row& r) {
|
||||
LiveVal& lv = s_vals[i];
|
||||
if (lv.in) {
|
||||
const char* slash = strchr(r.value, '/');
|
||||
char a[12];
|
||||
snprintf(a, sizeof(a), "%.*s", slash ? (int)(slash - r.value) : (int)strlen(r.value), r.value);
|
||||
lv_label_set_text(lv.in, a);
|
||||
lv_label_set_text(lv.out, slash ? slash + 1 : "");
|
||||
return;
|
||||
}
|
||||
char v[40];
|
||||
showValue(r.label, r.value, v, sizeof(v));
|
||||
lv_label_set_text(lv.val, v);
|
||||
if (!strcmp(r.label, "Errors")) lv_obj_set_style_text_color(lv.val, lv_color_hex(strcmp(r.value, "OK") ? theme::FAIL : theme::OK), 0);
|
||||
}
|
||||
|
||||
// A packets row: the name, then received and sent in fixed columns.
|
||||
static const int COL_W = 56;
|
||||
static lv_obj_t* pairLine(lv_obj_t* card, const char* name, lv_obj_t** in, lv_obj_t** out, bool head) {
|
||||
lv_obj_t* r = infoLine(card);
|
||||
lv_obj_t* k = label(r, name, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_align(k, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
const lv_font_t* f = head ? THEME_FONT_SMALL : THEME_FONT_BODY;
|
||||
uint32_t col = head ? theme::TEXT_MUTED : theme::TEXT;
|
||||
*out = label(r, head ? "Sent" : "", f, col);
|
||||
lv_obj_set_width(*out, COL_W);
|
||||
lv_obj_set_style_text_align(*out, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
lv_obj_align(*out, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
*in = label(r, head ? "Received" : "", f, col);
|
||||
lv_obj_set_width(*in, COL_W + 16);
|
||||
lv_obj_set_style_text_align(*in, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
lv_obj_align(*in, LV_ALIGN_RIGHT_MID, -COL_W, 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace diagview
|
||||
|
||||
static void onOpenDiag(lv_event_t* e) { (void)e; s_ui->showDiag(); }
|
||||
@@ -75,37 +147,68 @@ void UITask::buildDiag() {
|
||||
lv_obj_set_style_bg_color(tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS);
|
||||
lv_obj_add_event_cb(tabs, onDiagTab, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
|
||||
s_list = lv_obj_create(body);
|
||||
styleSurface(s_list, theme::SURFACE);
|
||||
lv_obj_set_style_radius(s_list, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_all(s_list, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(s_list, 3, 0);
|
||||
lv_obj_set_width(s_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(s_list, 1);
|
||||
lv_obj_set_flex_flow(s_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_scrollbar_mode(s_list, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
s_list = scrollList(body);
|
||||
lv_obj_set_style_pad_row(s_list, 4, 0);
|
||||
s_rows = 0;
|
||||
|
||||
if (s_tab == TAB_LIVE) {
|
||||
// Section by section, in ui-core's order within each.
|
||||
diag::Row rows[diag::MAX_ROWS + EXTRA];
|
||||
s_rows = allRows(rows, _core->gpsEnabled());
|
||||
for (int i = 0; i < s_rows; i++) {
|
||||
lv_obj_t* r = lv_obj_create(s_list);
|
||||
lv_obj_remove_style_all(r);
|
||||
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_align(label(r, rows[i].label, THEME_FONT_BODY, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
s_vals[i] = label(r, rows[i].value, THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(s_vals[i], LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
for (uint8_t sec = 0; sec < SEC_COUNT; sec++) {
|
||||
lv_obj_t* card = nullptr;
|
||||
for (int i = 0; i < s_rows; i++) {
|
||||
const Name* nm = nameOf(rows[i].label);
|
||||
if ((nm ? nm->sec : SEC_DEVICE) != sec) continue;
|
||||
if (!card) {
|
||||
sectionTitle(s_list, SEC_TITLE[sec]);
|
||||
card = infoCard(s_list);
|
||||
if (sec == SEC_PACKETS) { lv_obj_t *a, *b; pairLine(card, "", &a, &b, true); }
|
||||
}
|
||||
s_vals[i] = { nullptr, nullptr, nullptr };
|
||||
if (nm && nm->pair) pairLine(card, nm->text, &s_vals[i].in, &s_vals[i].out, false);
|
||||
else s_vals[i].val = infoRow(card, nm ? nm->text : rows[i].label, "");
|
||||
setLive(i, rows[i]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
diag::Line lines[diag::MAX_LINES];
|
||||
int n = s_tab == TAB_SYSTEM ? diag::systemLines(lines) : diag::fontLines(lines);
|
||||
for (int i = 0; i < n; i++) {
|
||||
lv_obj_t* l = label(s_list, lines[i], s_tab == TAB_FONT ? THEME_FONT_TITLE : THEME_FONT_BODY, theme::TEXT);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
|
||||
lv_obj_t* card = infoCard(s_list);
|
||||
if (s_tab == TAB_FONT) { // "Latin ABCabc": the script, then its sample
|
||||
diag::Line lines[diag::MAX_LINES];
|
||||
int n = diag::fontLines(lines);
|
||||
for (int i = 0; i < n; i++) {
|
||||
char* sp = strchr(lines[i], ' ');
|
||||
if (sp) *sp = '\0';
|
||||
infoRow(card, lines[i], sp ? sp + 1 : "", theme::TEXT, THEME_FONT_TITLE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// System: this build and the radio settings.
|
||||
char v[48];
|
||||
diag::shortVersion(v, sizeof(v));
|
||||
infoRow(card, "Firmware", v);
|
||||
infoRow(card, "Built", FIRMWARE_BUILD_DATE);
|
||||
#ifdef MESHCORE_VERSION
|
||||
infoRow(card, "MeshCore", MESHCORE_VERSION);
|
||||
#endif
|
||||
infoRow(card, "Device", board.getManufacturerName());
|
||||
infoRow(card, "Node name", the_mesh.getNodeName());
|
||||
if (NodePrefs* p = the_mesh.getNodePrefs()) {
|
||||
sectionTitle(s_list, "RADIO");
|
||||
card = infoCard(s_list);
|
||||
snprintf(v, sizeof(v), "%.3f MHz", p->freq);
|
||||
infoRow(card, "Frequency", v);
|
||||
snprintf(v, sizeof(v), "%.1f kHz", p->bw);
|
||||
if (strstr(v, ".0 ")) snprintf(v, sizeof(v), "%.0f kHz", p->bw);
|
||||
infoRow(card, "Bandwidth", v);
|
||||
snprintf(v, sizeof(v), "%u", (unsigned)p->sf);
|
||||
infoRow(card, "Spreading factor", v);
|
||||
snprintf(v, sizeof(v), "4/%u", (unsigned)p->cr);
|
||||
infoRow(card, "Coding rate", v);
|
||||
snprintf(v, sizeof(v), "%d dBm", (int)p->tx_power_dbm);
|
||||
infoRow(card, "TX power", v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +219,7 @@ void UITask::refreshDiag() {
|
||||
diag::Row rows[diag::MAX_ROWS + EXTRA];
|
||||
int n = allRows(rows, _core->gpsEnabled());
|
||||
if (n != s_rows) { buildDiag(); return; }
|
||||
for (int i = 0; i < n; i++)
|
||||
lv_label_set_text(s_vals[i], rows[i].value); // unchanged text isn't redrawn (UITask.cpp)
|
||||
for (int i = 0; i < n; i++) setLive(i, rows[i]); // unchanged text isn't redrawn (UITask.cpp)
|
||||
}
|
||||
|
||||
void UITask::diagResetPopup() {
|
||||
|
||||
@@ -19,9 +19,14 @@ static lv_obj_t* s_sky = nullptr; // sky plot (drawn)
|
||||
static lv_obj_t* s_bars = nullptr; // signal bars (drawn, as wide as the satellites need)
|
||||
static lv_obj_t* s_status = nullptr;
|
||||
static lv_obj_t* s_used = nullptr;
|
||||
static lv_obj_t* s_info = nullptr;
|
||||
static lv_obj_t* s_systems = nullptr;
|
||||
static lv_obj_t* s_note = nullptr;
|
||||
// Beside the plot: in view, search time / time to first fix, UTC.
|
||||
static lv_obj_t *s_inview = nullptr, *s_search = nullptr, *s_ttff = nullptr, *s_utc = nullptr;
|
||||
// Under the bars: the position card (hidden without a fix), one pill per
|
||||
// constellation, the USB warning.
|
||||
static lv_obj_t *s_fix_title = nullptr, *s_fix_card = nullptr;
|
||||
static lv_obj_t *s_pos = nullptr, *s_alt = nullptr, *s_speed = nullptr, *s_dop = nullptr;
|
||||
static lv_obj_t* s_sys_pill[GpsSky::SYS_COUNT];
|
||||
static lv_obj_t* s_usb = nullptr;
|
||||
static lv_obj_t* s_on_btn = nullptr;
|
||||
static const int SKY = 150;
|
||||
static const int BAR_PITCH = 26, BAR_W = 16, BARS_H = 104;
|
||||
@@ -273,7 +278,7 @@ void UITask::showGps(bool from_settings) {
|
||||
void UITask::buildGps() {
|
||||
using namespace gpsview;
|
||||
lv_obj_t* body = newScreen("GPS", true);
|
||||
s_sky = s_bars = s_status = s_used = s_info = s_systems = s_note = s_on_btn = nullptr;
|
||||
s_sky = s_bars = s_status = s_used = s_on_btn = nullptr;
|
||||
|
||||
// Sky plot left, the fix right.
|
||||
lv_obj_t* top = lv_obj_create(body);
|
||||
@@ -298,12 +303,16 @@ void UITask::buildGps() {
|
||||
lv_obj_set_style_pad_top(col, 4, 0);
|
||||
s_status = label(col, "", THEME_FONT_LARGE, theme::TEXT);
|
||||
s_used = label(col, "", THEME_FONT_BODY, theme::TEXT);
|
||||
s_info = label(col, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_set_style_text_line_space(s_info, 2, 0);
|
||||
for (lv_obj_t* l : { s_used, s_info }) {
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
}
|
||||
lv_label_set_long_mode(s_used, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(s_used, LV_PCT(100));
|
||||
lv_obj_set_style_pad_bottom(s_used, 4, 0);
|
||||
lv_obj_t* facts = infoCard(col); // no card of its own: rows on the page
|
||||
lv_obj_set_style_bg_opa(facts, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_pad_hor(facts, 0, 0);
|
||||
s_inview = infoRow(facts, "In view", "", theme::TEXT, THEME_FONT_SMALL);
|
||||
s_search = infoRow(facts, "Searching", "", theme::TEXT, THEME_FONT_SMALL);
|
||||
s_ttff = infoRow(facts, "First fix", "", theme::TEXT, THEME_FONT_SMALL);
|
||||
s_utc = infoRow(facts, "UTC", "", theme::TEXT, THEME_FONT_SMALL);
|
||||
s_on_btn = lv_button_create(col);
|
||||
lv_obj_set_height(s_on_btn, 34);
|
||||
lv_obj_set_style_radius(s_on_btn, theme::RADIUS, 0);
|
||||
@@ -313,6 +322,16 @@ void UITask::buildGps() {
|
||||
lv_obj_add_event_cb(s_on_btn, onGpsTurnOn, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_add_flag(s_on_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
s_usb = label(body, LV_SYMBOL_WARNING " On USB power: charging disturbs GPS", THEME_FONT_SMALL, theme::ACCENT);
|
||||
lv_obj_add_flag(s_usb, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
s_fix_title = sectionTitle(body, "POSITION");
|
||||
s_fix_card = infoCard(body);
|
||||
s_pos = infoRow(s_fix_card, "Coordinates", "");
|
||||
s_alt = infoRow(s_fix_card, "Altitude", "");
|
||||
s_speed = infoRow(s_fix_card, "Speed", "");
|
||||
s_dop = infoRow(s_fix_card, "Precision (DOP)", "");
|
||||
|
||||
sectionTitle(body, "SIGNAL (dB-Hz)");
|
||||
lv_obj_t* sc = lv_obj_create(body); // scrolls sideways when the satellites don't fit
|
||||
styleSurface(sc, theme::BG);
|
||||
@@ -326,13 +345,28 @@ void UITask::buildGps() {
|
||||
lv_obj_set_size(s_bars, BAR_PITCH, BARS_H);
|
||||
lv_obj_add_event_cb(s_bars, onGpsBarsDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
|
||||
s_systems = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(s_systems, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(s_systems, LV_PCT(100));
|
||||
s_note = label(body, "Solid = used in the fix. Green is a strong signal (30+ dB-Hz); "
|
||||
"indoors, below 25 a fix is unlikely.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(s_note, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(s_note, LV_PCT(100));
|
||||
// Constellations: used / in view, a pill each (a pill never breaks in two).
|
||||
lv_obj_t* pills = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(pills);
|
||||
lv_obj_set_size(pills, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(pills, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_style_pad_column(pills, 6, 0);
|
||||
lv_obj_set_style_pad_row(pills, 6, 0);
|
||||
for (uint8_t k = 0; k < GpsSky::SYS_COUNT; k++) {
|
||||
lv_obj_t* p = label(pills, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_obj_set_style_bg_color(p, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_set_style_bg_opa(p, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_radius(p, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_pad_hor(p, 10, 0);
|
||||
lv_obj_set_style_pad_ver(p, 3, 0);
|
||||
lv_obj_add_flag(p, LV_OBJ_FLAG_HIDDEN);
|
||||
s_sys_pill[k] = p;
|
||||
}
|
||||
|
||||
lv_obj_t* note = label(body, "Solid bar = used in the fix. Green is a strong signal (30+ dB-Hz); "
|
||||
"indoors, below 25 a fix is unlikely.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(note, LV_PCT(100));
|
||||
refreshGps();
|
||||
}
|
||||
|
||||
@@ -348,7 +382,9 @@ void UITask::refreshGps() {
|
||||
if (off) lv_obj_remove_flag(s_on_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
else lv_obj_add_flag(s_on_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
char used[48] = "", info[240] = "";
|
||||
char used[48] = "", inview[16] = "", search[16] = "", ttff[16] = "", utc[16] = "";
|
||||
char pos[32] = "", alt[16] = "", speed[16] = "", dop[32] = "";
|
||||
bool fix = false;
|
||||
if (!g) {
|
||||
lv_label_set_text(s_status, "Not available");
|
||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
|
||||
@@ -359,51 +395,58 @@ void UITask::refreshGps() {
|
||||
lv_label_set_text(s_status, "No data");
|
||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::FAIL), 0);
|
||||
#if defined(SEEED_WIO_TRACKER_L2)
|
||||
snprintf(info, sizeof(info), "Nothing from the receiver\n(%lu bytes so far)", (unsigned long)gps.rxChars());
|
||||
snprintf(used, sizeof(used), "Nothing from the receiver (%lu B)", (unsigned long)gps.rxChars());
|
||||
#else
|
||||
snprintf(info, sizeof(info), "Nothing from the receiver");
|
||||
snprintf(used, sizeof(used), "Nothing from the receiver");
|
||||
#endif
|
||||
} else {
|
||||
int tracked = 0, used_n = 0;
|
||||
for (int i = 0; i < s_n; i++) { if (s_sats[i].snr >= 0) tracked++; if (s_used_flag[i]) used_n++; }
|
||||
if (g->hasFix()) {
|
||||
fix = g->hasFix();
|
||||
if (fix) {
|
||||
lv_label_set_text(s_status, g->fix_mode == 2 ? "2D fix" : "3D fix");
|
||||
lv_obj_set_style_text_color(s_status, lv_color_hex(0x6FCF6F), 0);
|
||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::OK), 0);
|
||||
} else {
|
||||
lv_label_set_text(s_status, "Searching");
|
||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::ACCENT), 0);
|
||||
}
|
||||
snprintf(used, sizeof(used), "%d of %d used", g->hasFix() ? (used_n ? used_n : g->sats_used) : 0, s_n);
|
||||
int o = snprintf(info, sizeof(info), "%d with a signal\n", tracked);
|
||||
snprintf(used, sizeof(used), "%d of %d used", fix ? (used_n ? used_n : g->sats_used) : 0, s_n);
|
||||
snprintf(inview, sizeof(inview), "%d heard", tracked);
|
||||
uint32_t since = g->start_ms ? (millis() - g->start_ms) / 1000 : 0;
|
||||
if (g->ttff_ms) o += snprintf(info + o, sizeof(info) - o, "First fix after %lu s\n", (unsigned long)(g->ttff_ms / 1000));
|
||||
else o += snprintf(info + o, sizeof(info) - o, "Searching for %lu:%02lu\n", (unsigned long)(since / 60), (unsigned long)(since % 60));
|
||||
if (g->hasFix() && g->hdop > 0)
|
||||
o += snprintf(info + o, sizeof(info) - o, "DOP H %.1f V %.1f P %.1f\n", g->hdop, g->vdop, g->pdop);
|
||||
if (g->ttff_ms) snprintf(ttff, sizeof(ttff), "%lu s", (unsigned long)(g->ttff_ms / 1000));
|
||||
else snprintf(search, sizeof(search), "%lu:%02lu", (unsigned long)(since / 60), (unsigned long)(since % 60));
|
||||
if (g->utc_valid) snprintf(utc, sizeof(utc), "%02u:%02u:%02u", g->utc_h, g->utc_m, g->utc_s);
|
||||
int32_t lat, lon;
|
||||
if (g->hasFix() && _core->course.currentLocation(lat, lon))
|
||||
o += snprintf(info + o, sizeof(info) - o, "%.5f, %.5f\n", lat / 1e6, lon / 1e6);
|
||||
if (g->hasFix() && g->alt_valid)
|
||||
o += snprintf(info + o, sizeof(info) - o, "%.0f m - %.1f km/h\n", g->alt_m, g->speed_kmh);
|
||||
if (g->utc_valid) o += snprintf(info + o, sizeof(info) - o, "UTC %02u:%02u:%02u", g->utc_h, g->utc_m, g->utc_s);
|
||||
}
|
||||
if (g && !off && _board->isExternalPowered()) { // measured: on the cable the L76K never decodes indoors
|
||||
size_t o = strlen(info);
|
||||
snprintf(info + o, sizeof(info) - o, "%sOn USB power: charging disturbs GPS", o ? "\n" : "");
|
||||
if (fix && _core->course.currentLocation(lat, lon)) snprintf(pos, sizeof(pos), "%.5f, %.5f", lat / 1e6, lon / 1e6);
|
||||
if (fix && g->alt_valid) {
|
||||
snprintf(alt, sizeof(alt), "%.0f m", g->alt_m);
|
||||
snprintf(speed, sizeof(speed), "%.1f km/h", g->speed_kmh);
|
||||
}
|
||||
if (fix && g->hdop > 0) snprintf(dop, sizeof(dop), "H %.1f V %.1f P %.1f", g->hdop, g->vdop, g->pdop);
|
||||
}
|
||||
lv_label_set_text(s_used, used);
|
||||
lv_label_set_text(s_info, info);
|
||||
infoSet(s_inview, inview);
|
||||
infoSet(s_search, search);
|
||||
infoSet(s_ttff, ttff);
|
||||
infoSet(s_utc, utc);
|
||||
infoSet(s_pos, pos);
|
||||
infoSet(s_alt, alt);
|
||||
infoSet(s_speed, speed);
|
||||
infoSet(s_dop, dop);
|
||||
if (fix && pos[0]) { lv_obj_remove_flag(s_fix_title, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(s_fix_card, LV_OBJ_FLAG_HIDDEN); }
|
||||
else { lv_obj_add_flag(s_fix_title, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(s_fix_card, LV_OBJ_FLAG_HIDDEN); }
|
||||
// measured: on the cable the L76K never decodes indoors
|
||||
if (g && !off && _board->isExternalPowered()) lv_obj_remove_flag(s_usb, LV_OBJ_FLAG_HIDDEN);
|
||||
else lv_obj_add_flag(s_usb, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Per constellation: used / in view.
|
||||
char sys[160] = "";
|
||||
int o = 0;
|
||||
for (uint8_t k = 0; k < GpsSky::SYS_COUNT; k++) {
|
||||
int inview = 0, u = 0;
|
||||
for (int i = 0; i < s_n; i++) if (s_sats[i].sys == k) { inview++; if (s_used_flag[i]) u++; }
|
||||
if (inview) o += snprintf(sys + o, sizeof(sys) - o, "%s%c %s %d/%d", o ? " " : "", GpsSky::sysLetter(k),
|
||||
GpsSky::sysName(k), u, inview);
|
||||
int n = 0, u = 0;
|
||||
for (int i = 0; i < s_n; i++) if (s_sats[i].sys == k) { n++; if (s_used_flag[i]) u++; }
|
||||
if (!n) { lv_obj_add_flag(s_sys_pill[k], LV_OBJ_FLAG_HIDDEN); continue; }
|
||||
lv_label_set_text_fmt(s_sys_pill[k], "%c %s %d/%d", GpsSky::sysLetter(k), GpsSky::sysName(k), u, n);
|
||||
lv_obj_remove_flag(s_sys_pill[k], LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
lv_label_set_text(s_systems, sys);
|
||||
|
||||
int32_t w = s_n * BAR_PITCH;
|
||||
if (w < BAR_PITCH) w = BAR_PITCH;
|
||||
|
||||
@@ -594,21 +594,27 @@ void UITask::navTargetsPopup() {
|
||||
if (live == 0) label(list, "Nobody is sharing their position.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
}
|
||||
|
||||
// A place's coordinates and, with a fix, how far and which way.
|
||||
void UITask::placeCard(lv_obj_t* parent, int32_t lat, int32_t lon) {
|
||||
lv_obj_t* card = infoCard(parent);
|
||||
char t[40];
|
||||
snprintf(t, sizeof(t), "%.5f, %.5f", lat / 1e6, lon / 1e6);
|
||||
infoRow(card, "Coordinates", t);
|
||||
int32_t mlat, mlon;
|
||||
if (!_core->course.currentLocation(mlat, mlon)) return;
|
||||
char d[12];
|
||||
geo::fmtDist(d, sizeof(d), geo::haversineKm(mlat, mlon, lat, lon), _prefs && _prefs->units_imperial);
|
||||
int az = geo::bearingDeg(mlat, mlon, lat, lon);
|
||||
snprintf(t, sizeof(t), "%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az));
|
||||
infoRow(card, "Distance", t);
|
||||
}
|
||||
|
||||
void UITask::navWaypointMenu(int idx) {
|
||||
if (idx < 0 || idx >= _core->waypoints.count()) return;
|
||||
_nav_wp = idx;
|
||||
const Waypoint& w = _core->waypoints.at(idx);
|
||||
lv_obj_t* panel = navPopupPanel(w.label[0] ? w.label : "(unnamed)", false);
|
||||
char info[80];
|
||||
int o = snprintf(info, sizeof(info), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6);
|
||||
int32_t lat, lon;
|
||||
if (_core->course.currentLocation(lat, lon)) {
|
||||
char d[12];
|
||||
geo::fmtDist(d, sizeof(d), geo::haversineKm(lat, lon, w.lat_1e6, w.lon_1e6), _prefs && _prefs->units_imperial);
|
||||
int az = geo::bearingDeg(lat, lon, w.lat_1e6, w.lon_1e6);
|
||||
snprintf(info + o, sizeof(info) - o, "\n%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az));
|
||||
}
|
||||
label(panel, info, THEME_FONT_BODY, theme::TEXT);
|
||||
placeCard(panel, w.lat_1e6, w.lon_1e6);
|
||||
|
||||
lv_obj_t* acts = lv_obj_create(panel);
|
||||
styleSurface(acts, theme::BG);
|
||||
@@ -1387,16 +1393,7 @@ void UITask::navSpotPopup(int32_t lat, int32_t lon) {
|
||||
_nav_spot_lat = lat;
|
||||
_nav_spot_lon = lon;
|
||||
lv_obj_t* panel = navPopupPanel("This spot", false);
|
||||
char info[64];
|
||||
int o = snprintf(info, sizeof(info), "%.5f, %.5f", lat / 1e6, lon / 1e6);
|
||||
int32_t mlat, mlon;
|
||||
if (_core->course.currentLocation(mlat, mlon)) {
|
||||
char d[12];
|
||||
geo::fmtDist(d, sizeof(d), geo::haversineKm(mlat, mlon, lat, lon), _prefs && _prefs->units_imperial);
|
||||
int az = geo::bearingDeg(mlat, mlon, lat, lon);
|
||||
snprintf(info + o, sizeof(info) - o, "\n%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az));
|
||||
}
|
||||
label(panel, info, THEME_FONT_BODY, theme::TEXT);
|
||||
placeCard(panel, lat, lon);
|
||||
lv_obj_t* r = toolRow(panel);
|
||||
toolButton(r, UI_SYMBOL_FLAG " Add waypoint", navmap::TL_SPOT_ADD, true);
|
||||
toolButton(r, UI_SYMBOL_COMPASS " Go here", navmap::TL_SPOT_GO, false);
|
||||
|
||||
@@ -166,6 +166,8 @@ static bool startTask() {
|
||||
|
||||
} // namespace ota
|
||||
|
||||
static lv_obj_t* s_ota_latest = nullptr; // the release found, once checked
|
||||
|
||||
static void onOpenOta(lv_event_t* e) { (void)e; s_ui->showOta(); }
|
||||
static void onOtaButton(lv_event_t* e) { (void)e; s_ui->otaAction(); }
|
||||
|
||||
@@ -181,10 +183,10 @@ bool UITask::otaBusy() const { return ota::s_state == ota::CHECKING || ota::s_st
|
||||
|
||||
void UITask::buildOta() {
|
||||
lv_obj_t* body = newScreen("Firmware update", true);
|
||||
char t[96];
|
||||
snprintf(t, sizeof(t), "Installed: %s", FIRMWARE_VERSION);
|
||||
label(body, t, THEME_FONT_BODY, theme::TEXT);
|
||||
_ota_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* card = infoCard(body);
|
||||
infoRow(card, "Installed", FIRMWARE_VERSION);
|
||||
s_ota_latest = infoRow(card, "Latest release", "");
|
||||
_ota_status = label(body, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_label_set_long_mode(_ota_status, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(_ota_status, LV_PCT(100));
|
||||
_ota_bar = lv_bar_create(body);
|
||||
@@ -258,6 +260,7 @@ void UITask::refreshOta() {
|
||||
break;
|
||||
}
|
||||
lv_label_set_text(_ota_status, st);
|
||||
if (s_ota_latest) infoSet(s_ota_latest, s_latest);
|
||||
lv_label_set_text(_ota_btn_lbl, btn);
|
||||
if (bar) lv_obj_remove_flag(_ota_bar, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_ota_bar, LV_OBJ_FLAG_HIDDEN);
|
||||
if (btn_on) lv_obj_remove_flag(_ota_btn, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_ota_btn, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
@@ -163,19 +163,6 @@ static void showUsage(lv_obj_t* bar, lv_obj_t* lbl, uint64_t total, uint64_t use
|
||||
lv_label_set_text(lbl, line);
|
||||
}
|
||||
|
||||
// Name left, size right.
|
||||
static lv_obj_t* sizeRow(lv_obj_t* parent, const char* name) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), 34);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_align(label(row, name, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_LEFT_MID, theme::PAD, 0);
|
||||
lv_obj_t* v = label(row, "...", THEME_FONT_BODY, theme::ACCENT);
|
||||
lv_obj_align(v, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void refreshSizes() {
|
||||
for (int c = 0; c < C_COUNT; c++) {
|
||||
if (!s_cat_val[c]) continue;
|
||||
@@ -185,7 +172,7 @@ static void refreshSizes() {
|
||||
}
|
||||
if (s_status) {
|
||||
char t[48];
|
||||
snprintf(t, sizeof(t), s_walking ? "Counting files... %lu" : "%lu files", (unsigned long)s_files);
|
||||
snprintf(t, sizeof(t), s_walking ? "counting... %lu" : "%lu", (unsigned long)s_files);
|
||||
lv_label_set_text(s_status, t);
|
||||
}
|
||||
}
|
||||
@@ -220,8 +207,9 @@ void UITask::buildStorage() {
|
||||
} else {
|
||||
s_sd_bar = usageBar(body, &s_sd_lbl);
|
||||
lv_label_set_text(s_sd_lbl, "Reading the card...");
|
||||
for (int c = 0; c < C_COUNT; c++) s_cat_val[c] = sizeRow(body, CAT_NAME[c]);
|
||||
s_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* cats = infoCard(body); // what takes the space
|
||||
for (int c = 0; c < C_COUNT; c++) s_cat_val[c] = infoRow(cats, CAT_NAME[c], "...", theme::ACCENT);
|
||||
s_status = infoRow(cats, "Files", "");
|
||||
s_sd_read = false; // read in pollStorage(): the first read of a big card takes a moment
|
||||
startWalk();
|
||||
}
|
||||
|
||||
@@ -168,6 +168,76 @@ static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint
|
||||
return row;
|
||||
}
|
||||
|
||||
// ── Info cards ──
|
||||
// Label / value rows on a card (Diagnostics, GPS, node detail, About): the
|
||||
// label small and muted on the left, the value on the right -- wrapping,
|
||||
// right-aligned, when it's long -- and a hairline between rows. infoRow()
|
||||
// returns the value label to update in place; infoSet() also hides its row
|
||||
// while there's nothing to show.
|
||||
static lv_obj_t* infoCard(lv_obj_t* parent) {
|
||||
lv_obj_t* c = lv_obj_create(parent);
|
||||
styleSurface(c, theme::SURFACE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(c, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(c, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_hor(c, theme::PAD, 0);
|
||||
lv_obj_set_flex_flow(c, LV_FLEX_FLOW_COLUMN);
|
||||
return c;
|
||||
}
|
||||
|
||||
static lv_obj_t* infoLine(lv_obj_t* card) { // one row's box, the hairline above all but the first
|
||||
lv_obj_t* r = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(r);
|
||||
lv_obj_remove_flag(r, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_ver(r, 5, 0);
|
||||
if (lv_obj_get_index(r) > 0) {
|
||||
lv_obj_set_style_border_side(r, LV_BORDER_SIDE_TOP, 0);
|
||||
lv_obj_set_style_border_width(r, 1, 0);
|
||||
lv_obj_set_style_border_color(r, lv_color_hex(theme::SURFACE_2), 0);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static lv_obj_t* infoRow(lv_obj_t* card, const char* key, const char* value, uint32_t col = theme::TEXT,
|
||||
const lv_font_t* font = THEME_FONT_BODY) {
|
||||
lv_obj_t* r = infoLine(card);
|
||||
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(r, 8, 0);
|
||||
lv_obj_t* k = label(r, key, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(k, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(k, 1);
|
||||
lv_obj_set_flex_grow(k, 1);
|
||||
lv_obj_t* v = label(r, value, font, col);
|
||||
lv_label_set_long_mode(v, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(v, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(v, LV_PCT(66), 0);
|
||||
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
return v;
|
||||
}
|
||||
|
||||
// A row whose text is a sentence (credits, notes): the label over it.
|
||||
static lv_obj_t* infoNote(lv_obj_t* card, const char* key, const char* text) {
|
||||
lv_obj_t* r = infoLine(card);
|
||||
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(r, 2, 0);
|
||||
label(r, key, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* t = label(r, text, THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
return t;
|
||||
}
|
||||
|
||||
static void infoSet(lv_obj_t* value, const char* text) {
|
||||
lv_obj_t* row = lv_obj_get_parent(value);
|
||||
if (!text || !text[0]) { lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN); return; }
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_label_set_text(value, text);
|
||||
}
|
||||
|
||||
// A bare flex row / column sized to its content, taps passing through.
|
||||
static lv_obj_t* flexBox(lv_obj_t* parent, lv_flex_flow_t flow) {
|
||||
lv_obj_t* b = lv_obj_create(parent);
|
||||
@@ -1434,6 +1504,8 @@ void UITask::buildContacts() {
|
||||
// model, since it is a different set: who is in range, not who is known.
|
||||
|
||||
static NearbyModel::Entry s_node; // the node open in SCR_NODE (a copy: the list re-sorts)
|
||||
// Its info card's values (UITask::_node_info is the card).
|
||||
static lv_obj_t *s_nd_type, *s_nd_status, *s_nd_dist, *s_nd_pos, *s_nd_heard, *s_nd_signal, *s_nd_id;
|
||||
enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE, NODE_NAV, NODE_ADMIN, NODE_WAYPOINT, NODE_PIN };
|
||||
|
||||
static void onNearbyChip(lv_event_t* e) { s_ui->setNearbyFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
@@ -1708,16 +1780,16 @@ void UITask::buildNode() {
|
||||
const NearbyModel::Entry& e = s_node;
|
||||
lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true);
|
||||
|
||||
_node_info = label(body, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_label_set_long_mode(_node_info, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(_node_info, LV_PCT(100));
|
||||
lv_obj_set_style_text_line_space(_node_info, 3, 0);
|
||||
_node_ping = label(body, "", THEME_FONT_BODY, theme::ACCENT);
|
||||
|
||||
lv_obj_t* spacer = lv_obj_create(body); // pushes the actions to the bottom
|
||||
lv_obj_remove_style_all(spacer);
|
||||
lv_obj_set_width(spacer, 1);
|
||||
lv_obj_set_flex_grow(spacer, 1);
|
||||
lv_obj_t* info = scrollList(body); // the info, scrolling above the actions at the bottom
|
||||
_node_info = infoCard(info);
|
||||
s_nd_type = infoRow(_node_info, "Type", "");
|
||||
s_nd_status = infoRow(_node_info, "Status", "");
|
||||
s_nd_dist = infoRow(_node_info, "Distance", "");
|
||||
s_nd_pos = infoRow(_node_info, "Position", "");
|
||||
s_nd_heard = infoRow(_node_info, "Heard", "");
|
||||
s_nd_signal = infoRow(_node_info, "Signal", "");
|
||||
s_nd_id = infoRow(_node_info, "ID", "");
|
||||
_node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT);
|
||||
|
||||
lv_obj_t* acts = lv_obj_create(body);
|
||||
styleSurface(acts, theme::BG);
|
||||
@@ -1763,36 +1835,40 @@ void UITask::refreshNode() {
|
||||
if (same) { s_node = m; break; }
|
||||
}
|
||||
|
||||
char buf[320];
|
||||
int o = 0;
|
||||
// A scan row carries no contact index; is_known says whether it's in the contacts.
|
||||
bool known = e.contact_idx >= 0 || (_node_from_scan && e.is_known);
|
||||
o += snprintf(buf + o, sizeof(buf) - o, "%s%s%s%s", NearbyModel::typeName(e.type),
|
||||
known ? "" : " - not a contact",
|
||||
e.is_live ? (e.live_verified ? " - live position" : " - live position (channel)") : "",
|
||||
e.fav ? " - favourite" : "");
|
||||
char status[64] = "", dist[40] = "", pos[32] = "", heard[16] = "", sig[40] = "", id[12] = "";
|
||||
int o = 0;
|
||||
auto tag = [&](const char* t) { o += snprintf(status + o, sizeof(status) - o, "%s%s", o ? ", " : "", t); };
|
||||
if (!known) tag("not a contact");
|
||||
if (e.is_live) tag(e.live_verified ? "live position" : "live position (channel)");
|
||||
if (e.fav) tag("favourite");
|
||||
if (status[0] >= 'a' && status[0] <= 'z') status[0] -= 'a' - 'A';
|
||||
int32_t lat, lon;
|
||||
bool gps = _nearby->ownPosition(lat, lon);
|
||||
if (e.lat_e6 != 0 || e.lon_e6 != 0) {
|
||||
if (gps && e.dist_km >= 0.0f) {
|
||||
if (_nearby->ownPosition(lat, lon) && e.dist_km >= 0.0f) {
|
||||
char d[16];
|
||||
geo::fmtDist(d, sizeof(d), e.dist_km, _prefs && _prefs->units_imperial);
|
||||
int az = geo::bearingDeg(lat, lon, e.lat_e6, e.lon_e6);
|
||||
o += snprintf(buf + o, sizeof(buf) - o, "\n%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az));
|
||||
snprintf(dist, sizeof(dist), "%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az));
|
||||
}
|
||||
o += snprintf(buf + o, sizeof(buf) - o, "\n%.5f, %.5f", e.lat_e6 / 1e6, e.lon_e6 / 1e6);
|
||||
snprintf(pos, sizeof(pos), "%.5f, %.5f", e.lat_e6 / 1e6, e.lon_e6 / 1e6);
|
||||
} else if (!_node_from_scan) {
|
||||
o += snprintf(buf + o, sizeof(buf) - o, "\nNo position shared");
|
||||
snprintf(pos, sizeof(pos), "Not shared");
|
||||
}
|
||||
char age[8];
|
||||
geo::fmtAgeShort(age, sizeof(age), rtc_clock.getCurrentTime(), e.lastmod);
|
||||
if (age[0]) o += snprintf(buf + o, sizeof(buf) - o, "\nHeard %s ago", age);
|
||||
if (age[0]) snprintf(heard, sizeof(heard), "%s ago", age);
|
||||
if (_node_from_scan)
|
||||
o += snprintf(buf + o, sizeof(buf) - o, "\nRSSI %d dBm - SNR %.1f / %.1f dB", e.rssi,
|
||||
e.snr_x4 / 4.0f, e.remote_snr_x4 / 4.0f);
|
||||
if (e.has_prefix)
|
||||
o += snprintf(buf + o, sizeof(buf) - o, "\nID %02X%02X%02X%02X", e.pub_key[0], e.pub_key[1], e.pub_key[2], e.pub_key[3]);
|
||||
lv_label_set_text(_node_info, buf);
|
||||
snprintf(sig, sizeof(sig), "%d dBm, SNR %.1f / %.1f", e.rssi, e.snr_x4 / 4.0f, e.remote_snr_x4 / 4.0f);
|
||||
if (e.has_prefix) snprintf(id, sizeof(id), "%02X%02X%02X%02X", e.pub_key[0], e.pub_key[1], e.pub_key[2], e.pub_key[3]);
|
||||
infoSet(s_nd_type, NearbyModel::typeName(e.type));
|
||||
infoSet(s_nd_status, status);
|
||||
infoSet(s_nd_dist, dist);
|
||||
infoSet(s_nd_pos, pos);
|
||||
infoSet(s_nd_heard, heard);
|
||||
infoSet(s_nd_signal, sig);
|
||||
infoSet(s_nd_id, id);
|
||||
|
||||
// Ping result (PingEngine releases its slot on reply; the view owns the timeout)
|
||||
if (_pinging) {
|
||||
@@ -2721,15 +2797,13 @@ void UITask::buildSettings() {
|
||||
}
|
||||
|
||||
sectionTitle(body, "ABOUT");
|
||||
char about[300], built[24] = "";
|
||||
if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) snprintf(built, sizeof(built), " (%s)", FIRMWARE_BUILD_DATE);
|
||||
snprintf(about, sizeof(about), "%s\nFirmware %s%s\n\nMap data: %s\nEmoji: Twemoji \xC2\xA9 Twitter, Inc. and contributors (CC\xE2\x80\x91" "BY 4.0)",
|
||||
the_mesh.getNodeName(), FIRMWARE_VERSION, built,
|
||||
(lvport::mountStorage() && mapview::s_provider->available()) ? mapview::s_provider->attribution()
|
||||
: "\xC2\xA9 OpenStreetMap contributors");
|
||||
lv_obj_t* a = label(body, about, THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(a, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(a, LV_PCT(100));
|
||||
lv_obj_t* about = infoCard(body);
|
||||
infoRow(about, "Node", the_mesh.getNodeName());
|
||||
infoRow(about, "Firmware", FIRMWARE_VERSION);
|
||||
if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) infoRow(about, "Built", FIRMWARE_BUILD_DATE);
|
||||
infoNote(about, "Map data", (lvport::mountStorage() && mapview::s_provider->available())
|
||||
? mapview::s_provider->attribution() : "\xC2\xA9 OpenStreetMap contributors");
|
||||
infoNote(about, "Emoji", "Twemoji \xC2\xA9 Twitter, Inc. and contributors (CC\xE2\x80\x91" "BY\xC2\xA0" "4.0)");
|
||||
}
|
||||
|
||||
void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
|
||||
@@ -2751,3 +2825,19 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
|
||||
#include "OtaScreen.h"
|
||||
#include "StorageScreen.h"
|
||||
#include "Splash.h"
|
||||
|
||||
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||
// Sim tests: straight to a screen by name.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
if (!s_ui) return;
|
||||
struct { const char* n; void (UITask::*fn)(); } const SCREENS[] = {
|
||||
{ "home", &UITask::showHome }, { "chats", &UITask::showChats }, { "settings", &UITask::showSettings },
|
||||
{ "nodes", &UITask::showNearby }, { "ota", &UITask::showOta }, { "wifi", &UITask::showWifi },
|
||||
{ "clock", &UITask::showClock }, { "radio", &UITask::showRadio }, { "scopes", &UITask::showScopes },
|
||||
{ "repeater", &UITask::showRepeater }, { "bot", &UITask::showBot }, { "storage", &UITask::showStorage },
|
||||
{ "diag", &UITask::showDiag }, { "compass", &UITask::showCompass }, { "admin", &UITask::showAdminPick },
|
||||
{ "quick", &UITask::showQuickMsgs },
|
||||
};
|
||||
for (auto& s : SCREENS) if (!strcmp(s.n, name)) { (s_ui->*s.fn)(); return; }
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -303,6 +303,7 @@ private:
|
||||
void navAddWaypoint(int32_t lat, int32_t lon);
|
||||
void navDropAt(int x, int y);
|
||||
void navSpotPopup(int32_t lat, int32_t lon);
|
||||
void placeCard(lv_obj_t* parent, int32_t lat, int32_t lon);
|
||||
void refreshNavTools();
|
||||
void navPollAveraging();
|
||||
void navPollTrackBack();
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
// Image glyphs (the emoji) are drawn only with this on; lv_imgfont itself is unused.
|
||||
#define LV_USE_IMGFONT 1
|
||||
#define LV_FONT_DEFAULT &ui_font_14
|
||||
// Where a line may break: not after '.', which would split "4.0", "v1.23" or
|
||||
// a coordinate across two lines.
|
||||
#define LV_TXT_BREAK_CHARS " ,;:-_)]}"
|
||||
#define LV_USE_FONT_COMPRESSED 0 // fonts are generated uncompressed: no per-glyph decompression on every draw
|
||||
|
||||
// PNG decoder for raster map tiles (map/TileProvider.h calls lodepng directly).
|
||||
|
||||
Reference in New Issue
Block a user