refactor(ui-lvgl): review -- shared button bar, confirms, notes, local time

- Dead code gone: anim::fadeHide, radioctl::presetCount, the L2 board's
  setSdPower / setGrovePower / expanderOK (awRead, gnssReset now private).
- buttonBar() / barButton() replace 7 copies of the popup button row.
- confirmBody(): one red-button confirm for reboot / power off, reset
  counters, admin commands, preset and scope delete.
- tapConfirmed(): the 11 "tap again" buttons share one mechanism, 3 s
  everywhere, the label restored when left alone.
- noteLabel() for ~30 wrapped notes.
- localTm() in NodePrefs.h and one MONTHS table: lvgl, MsgExpand, the L1
  clock and lock screen.
- The sim keeps UI settings in an in-memory nvs::, so one set of load /
  save helpers serves both.
- Asleep, the loop wakes every 50 ms (the button / touch poll), not 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-28 22:35:26 +02:00
co-authored by Claude Opus 5.5
parent 1def29335c
commit ece849db3b
25 changed files with 308 additions and 563 deletions
+10
View File
@@ -76,6 +76,16 @@ for a second radio). They are L2-specific and block the loop for ~90 s.
places to speed things up or save RAM, flash and battery.
First a list of findings to accept, then fixes in batches, each checked
in the sim and on the device.
Done 2026-09-28 (-265 lines): dead code (anim::fadeHide,
presetCount, the L2 board's setSdPower / setGrovePower / expanderOK);
one buttonBar() / barButton() for 7 copies of the button row;
confirmBody() for 5 red-button popups; tapConfirmed() for 11
"tap again" buttons, all 3 s and the label restored; noteLabel() for
~30 wrapped notes; localTm() in NodePrefs.h and one MONTHS table
(lvgl, MsgExpand, the L1 clock); the sim keeps UI settings in an
in-memory nvs::, one set of load / save helpers; asleep, the loop
wakes every 50 ms, not 20. Left: map tables to PSRAM only if the
internal heap gets tight; emoji 1.25 MB of flash (fits).
## G. Core parity with L1 SOLO
+4 -5
View File
@@ -4,6 +4,7 @@
#include <ctime>
#include <cstdint>
#include <helpers/SensorManager.h>
#include "NodePrefs.h" // localTm()
#include <helpers/sensors/LPPDataHelpers.h>
// Expands placeholders in tmpl into out (out_len bytes).
@@ -85,12 +86,10 @@ inline void expandMsg(const char* tmpl, char* out, int out_len,
else strcpy(lb, "no GPS");
APPEND(lb, strlen(lb)); p += 5;
} else if (strncmp(p, "{time}", 6) == 0) {
if (utc_ts > 1000000000UL) {
uint32_t local_ts = utc_ts + (int32_t)tz_hours * 3600;
time_t t = (time_t)local_ts;
struct tm* ti = gmtime(&t);
struct tm ti;
if (localTm(utc_ts, tz_hours, ti)) {
char tb[8];
snprintf(tb, sizeof(tb), "%02d:%02d", ti->tm_hour, ti->tm_min);
snprintf(tb, sizeof(tb), "%02d:%02d", ti.tm_hour, ti.tm_min);
APPEND(tb, strlen(tb));
}
p += 6;
+8
View File
@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <stdio.h>
#include <time.h>
// Firmware's own boot-default radio params — used both as the companion's
// initial freq/sf/bw/cr (MyMesh.cpp) and, here, as the seed for a never-
@@ -724,6 +725,13 @@ static inline bool localHour(uint32_t utc, int8_t tz_hours, int& h) {
h = (int)(((int64_t)utc + (int64_t)tz_hours * 3600) / 3600 % 24);
return true;
}
// A UTC time as the local calendar date and time; false while the clock isn't set.
static inline bool localTm(uint32_t utc, int8_t tz_hours, struct tm& out) {
if (utc < 1000000000UL) return false;
time_t t = (time_t)((int64_t)utc + (int64_t)tz_hours * 3600);
out = *gmtime(&t);
return true;
}
// Settings > Sound > Quiet hours in force at `utc` (never with the clock unset).
static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) {
int h;
@@ -27,12 +27,6 @@ static void applyApc() { the_mesh.applyApc(); } // (re)initialise Adaptive Pow
// ── Presets ─────────────────────────────────────────────────────────────────
// One list: built-ins 0..RADIO_PRESET_COUNT-1, then the non-empty user slots.
static int presetCount(const NodePrefs* p) {
int n = RADIO_PRESET_COUNT;
for (int i = 0; p && i < NodePrefs::USER_RADIO_PRESET_MAX; i++) if (p->user_radio_presets[i].name[0]) n++;
return n;
}
// Preset `idx` of the list; false past the end.
static bool presetAt(const NodePrefs* p, int idx, const char*& name, float& freq, float& bw, uint8_t& sf, uint8_t& cr) {
if (idx < 0) return false;
+5 -22
View File
@@ -96,10 +96,8 @@ void UITask::buildAdminPick() {
}
}
if (!rows) {
lv_obj_t* t = label(body, "No repeaters or room servers yet. They show up here once their advert is heard.",
THEME_FONT_BODY, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
noteLabel(body, "No repeaters or room servers yet. They show up here once their advert is heard.",
THEME_FONT_BODY, theme::TEXT_MUTED);
}
}
@@ -219,18 +217,7 @@ void UITask::adminRow(int row) {
const char* prompt = admin::confirmPrompt(f);
if (prompt) { // takes the node out of service for a while: ask first
s_confirm = &f;
lv_obj_t* panel = navPopupPanel(prompt, false);
lv_obj_t* t = label(panel, "It's offline until it comes back.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
lv_obj_t* b = lv_button_create(panel);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0);
lv_obj_add_event_cb(b, onAdminValue, LV_EVENT_CLICKED, (void*)(uintptr_t)V_CONFIRM);
lv_obj_center(label(b, f.label, THEME_FONT_BODY, theme::TEXT));
confirmBody(navPopupPanel(prompt, false), "It's offline until it comes back.", f.label, onAdminValue, V_CONFIRM);
return;
}
S.run(f);
@@ -337,9 +324,7 @@ void UITask::adminValuePopup() {
lv_obj_add_event_cb(s_choice, onAdminChoice, LV_EVENT_VALUE_CHANGED, NULL);
}
if (f->isRadio()) {
lv_obj_t* note = label(panel, "A mismatch cuts the node off.", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP);
lv_obj_set_width(note, LV_PCT(100));
noteLabel(panel, "A mismatch cuts the node off.");
}
lv_obj_t* r = adminRowBox(panel);
adminPopupButton(r, LV_SYMBOL_OK " Save", V_SAVE, true);
@@ -395,9 +380,7 @@ void UITask::adminReplyPopup(const char* text) {
lv_obj_set_flex_grow(box, 1);
lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_ACTIVE);
}
lv_obj_t* l = label(box, text[0] ? text : "(empty)", THEME_FONT_BODY, theme::TEXT);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
noteLabel(box, text[0] ? text : "(empty)", THEME_FONT_BODY, theme::TEXT);
lv_obj_t* r = adminRowBox(panel);
adminPopupButton(r, "OK", adminview::V_CLOSE, false);
}
-11
View File
@@ -75,15 +75,4 @@ namespace anim {
lv_obj_set_style_translate_x(o, 0, 0);
run(o, setOpa, LV_OPA_TRANSP, LV_OPA_COVER, ms);
}
static void hideDone(lv_anim_t* a) {
lv_obj_t* o = (lv_obj_t*)a->var;
lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_opa(o, LV_OPA_COVER, 0);
}
// Fade out, then hide (the object stays for reuse).
static void fadeHide(lv_obj_t* o) {
lv_anim_delete(o, setOpa);
run(o, setOpa, lv_obj_get_style_opa(o, LV_PART_MAIN), LV_OPA_TRANSP, OUT_MS, hideDone);
}
}
@@ -17,7 +17,6 @@ enum : uint8_t { C_NOTIF, C_SCOPE, C_MELODY };
static int s_idx = -1; // channel the popup / form is about (-1: adding)
static uint8_t s_type = T_HASHTAG;
static bool s_hex = false;
static uint32_t s_del_armed_ms = 0;
static lv_obj_t* s_del_lbl = nullptr;
static lv_obj_t* s_fav_btn = nullptr;
// Add / Edit form
@@ -69,7 +68,6 @@ void UITask::channelMenu(int idx) {
ChannelDetails ch;
if (!chanctl::exists(idx) || !the_mesh.getChannel(idx, ch)) return;
s_idx = idx;
s_del_armed_ms = 0;
s_del_lbl = s_fav_btn = nullptr;
char title[40];
snprintf(title, sizeof(title), "%s", ch.name); // as named: "#" marks a hashtag channel
@@ -89,12 +87,7 @@ void UITask::channelMenu(int idx) {
(void*)(uintptr_t)C_SCOPE);
}
lv_obj_t* acts = lv_obj_create(panel);
styleSurface(acts, theme::BG);
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
lv_obj_t* acts = buttonBar(panel);
bool unread = _core->history.chUnread(idx) > 0;
struct { const char* text; uint8_t act; } btns[] = {
{ UI_SYMBOL_STAR " Fav", A_FAV },
@@ -105,21 +98,10 @@ void UITask::channelMenu(int idx) {
};
for (auto& b : btns) {
if (b.act == A_READ && !unread) continue;
lv_obj_t* bt = lv_button_create(acts);
lv_obj_set_height(bt, 40);
lv_obj_set_flex_grow(bt, 1);
lv_obj_set_style_pad_hor(bt, 4, 0);
lv_obj_set_style_radius(bt, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(bt, 0, 0);
lv_obj_set_style_bg_color(bt, lv_color_hex(theme::SURFACE), 0);
lv_obj_add_event_cb(bt, onChanAction, LV_EVENT_CLICKED, (void*)(uintptr_t)b.act);
lv_obj_t* l = label(bt, b.text, THEME_FONT_SMALL, theme::TEXT);
lv_obj_center(l);
if (b.act == A_DELETE) s_del_lbl = l;
lv_obj_t* bt = barButton(acts, b.text, onChanAction, b.act, b.act == A_FAV && chanctl::favourite(_prefs, idx));
if (b.act == A_DELETE) s_del_lbl = lv_obj_get_child(bt, 0);
if (b.act == A_FAV) s_fav_btn = bt;
}
if (s_fav_btn && chanctl::favourite(_prefs, idx))
lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(theme::ACCENT_DIM), 0);
}
void UITask::channelSet(uint8_t which, int v) {
@@ -140,7 +122,7 @@ void UITask::channelAction(uint8_t act) {
bool on = !chanctl::favourite(_prefs, idx);
chanctl::setFavourite(_prefs, idx, on);
prefsSave();
if (s_fav_btn) lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0);
if (s_fav_btn) barButtonOn(s_fav_btn, on);
showToast(on ? "Added to favourites" : "Removed from favourites", 1200);
if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star
break;
@@ -158,11 +140,7 @@ void UITask::channelAction(uint8_t act) {
pinPopup(true, (uint8_t)idx, nullptr); // DeviceScreen.h
break;
case A_DELETE:
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // second tap within 3 s confirms
s_del_armed_ms = millis() | 1;
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
break;
}
if (!tapConfirmed(s_del_lbl, "Delete?")) break;
chanctl::remove(idx);
navClosePopup();
showToast("Channel deleted");
@@ -201,9 +179,7 @@ void UITask::buildChannelEdit() {
lv_obj_add_event_cb(s_types, onChanType, LV_EVENT_VALUE_CHANGED, NULL);
}
s_hint = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s_hint, LV_PCT(100));
s_hint = noteLabel(body, "");
s_name = chanField(body, "Name");
lv_textarea_set_max_length(s_name, sizeof(((ChannelDetails*)0)->name) - 1);
s_secret = chanField(body, "Passphrase");
@@ -223,9 +199,7 @@ void UITask::buildChannelEdit() {
lv_obj_add_event_cb(save, onChanSave, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
stylePrimary(save);
s_status = label(body, "", THEME_FONT_SMALL, theme::FAIL);
lv_label_set_long_mode(s_status, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s_status, LV_PCT(100));
s_status = noteLabel(body, "", THEME_FONT_SMALL, theme::FAIL);
if (edit) {
ChannelDetails ch;
@@ -105,9 +105,7 @@ void UITask::buildCompass() {
s_deg = label(num, "", THEME_FONT_CLOCK, theme::TEXT);
s_degsign = label(num, "", THEME_FONT_LARGE, theme::TEXT);
s_card = label(col, "", THEME_FONT_LARGE, theme::ACCENT);
s_hint = label(col, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s_hint, LV_PCT(100));
s_hint = noteLabel(col, "");
lv_obj_set_style_text_align(s_hint, LV_TEXT_ALIGN_CENTER, 0);
refreshCompass();
}
@@ -25,28 +25,6 @@ static bool s_login_wait = false; // open the room once this login answers
static lv_obj_t* s_fav_btn = nullptr;
static int s_msg = -1; // s_msg_meta index the message popup is about
static lv_obj_t* actionRow(lv_obj_t* panel) {
lv_obj_t* acts = lv_obj_create(panel);
styleSurface(acts, theme::BG);
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
return acts;
}
static lv_obj_t* actionButton(lv_obj_t* acts, const char* text, lv_event_cb_t cb, uint8_t act, bool on) {
lv_obj_t* bt = lv_button_create(acts);
lv_obj_set_height(bt, 40);
lv_obj_set_flex_grow(bt, 1);
lv_obj_set_style_pad_hor(bt, 4, 0);
lv_obj_set_style_radius(bt, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(bt, 0, 0);
lv_obj_set_style_bg_color(bt, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0);
lv_obj_add_event_cb(bt, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
lv_obj_center(label(bt, text, THEME_FONT_SMALL, theme::TEXT));
return bt;
}
} // namespace convview
@@ -172,13 +150,13 @@ void UITask::conversationMenu(const uint8_t* pub_key) {
lv_obj_set_width(st, LV_PCT(100));
}
lv_obj_t* acts = actionRow(panel);
s_fav_btn = actionButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV, contactctl::favourite(ci));
if (!room && _core->dmUnread(ci.id.pub_key) > 0) actionButton(acts, LV_SYMBOL_OK " Read", onConvAction, A_READ, false);
actionButton(acts, UI_SYMBOL_PIN, onConvAction, A_PIN, favslots::findContact(_prefs, ci.id.pub_key) >= 0);
lv_obj_t* acts = buttonBar(panel);
s_fav_btn = barButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV, contactctl::favourite(ci));
if (!room && _core->dmUnread(ci.id.pub_key) > 0) barButton(acts, LV_SYMBOL_OK " Read", onConvAction, A_READ, false);
barButton(acts, UI_SYMBOL_PIN, onConvAction, A_PIN, favslots::findContact(_prefs, ci.id.pub_key) >= 0);
if (room) {
actionButton(acts, LV_SYMBOL_EDIT " Login", onConvAction, A_LOGIN, false);
if (_core->rooms.isLoggedIn(ci.id.pub_key)) actionButton(acts, LV_SYMBOL_CLOSE " Logout", onConvAction, A_LOGOUT, false);
barButton(acts, LV_SYMBOL_EDIT " Login", onConvAction, A_LOGIN, false);
if (_core->rooms.isLoggedIn(ci.id.pub_key)) barButton(acts, LV_SYMBOL_CLOSE " Logout", onConvAction, A_LOGOUT, false);
}
}
@@ -196,7 +174,7 @@ void UITask::conversationAction(uint8_t act) {
case A_FAV: {
bool on = !contactctl::favourite(ci);
if (!contactctl::setFavourite(ci.id.pub_key, on)) break;
if (s_fav_btn) lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0);
if (s_fav_btn) barButtonOn(s_fav_btn, on);
showToast(on ? "Added to favourites" : "Removed from favourites", 1200);
if (_screen == SCR_CHATS) { buildChats(); conversationMenu(s_key); } // star / filter
break;
@@ -314,9 +292,8 @@ void UITask::messageMenu(int idx) {
char when[64], age[16];
uint32_t now = rtc_clock.getCurrentTime();
geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now);
if (ts > 1000000000UL) {
time_t t = (time_t)((int64_t)ts + (int64_t)(_prefs ? _prefs->tz_offset_hours : 0) * 3600);
struct tm ti = *gmtime(&t);
struct tm ti;
if (localTime(_prefs, ti, ts)) {
char clk[12];
fmtClock(clk, sizeof(clk), ti, _prefs, true);
if (clockBehind(now, ts)) { // the clock isn't set yet: the date instead of an age
@@ -372,9 +349,9 @@ void UITask::messageMenu(int idx) {
bool can_reply = !m.own && m.from[0] && _compose_ta;
if (!can_reply && m.loc < 0) return;
lv_obj_t* acts = actionRow(panel);
if (can_reply) actionButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false);
if (m.loc >= 0) actionButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false);
lv_obj_t* acts = buttonBar(panel);
if (can_reply) barButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false);
if (m.loc >= 0) barButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false);
}
void UITask::messageAction(uint8_t act) {
@@ -147,19 +147,10 @@ void UITask::nodeNameDone(bool ok) {
}
void UITask::powerPopup(bool restart) {
lv_obj_t* panel = navPopupPanel(restart ? "Reboot?" : "Power off?", false);
lv_obj_t* t = label(panel, restart ? "Restarts the device. Messages and settings are kept."
: "Turns the device off. Messages and settings are kept; no messages arrive while it is off.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
lv_obj_t* b = lv_button_create(panel);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0);
lv_obj_add_event_cb(b, onPowerGo, LV_EVENT_CLICKED, (void*)(uintptr_t)(restart ? 1 : 0));
lv_obj_center(label(b, restart ? LV_SYMBOL_REFRESH " Reboot" : LV_SYMBOL_POWER " Power off", THEME_FONT_BODY, theme::TEXT));
confirmBody(navPopupPanel(restart ? "Reboot?" : "Power off?", false),
restart ? "Restarts the device. Messages and settings are kept."
: "Turns the device off. Messages and settings are kept; no messages arrive while it is off.",
restart ? LV_SYMBOL_REFRESH " Reboot" : LV_SYMBOL_POWER " Power off", onPowerGo, restart ? 1 : 0);
}
// ── Lock screen ───────────────────────────────────────────────────────────────
@@ -503,9 +494,9 @@ void UITask::favHold(int slot) {
if (!contactctl::favName(_prefs, slot, name, sizeof(name))) { favPickPopup(slot); return; }
devview::s_fav_slot = slot;
lv_obj_t* panel = navPopupPanel(name, false);
lv_obj_t* acts = convview::actionRow(panel);
convview::actionButton(acts, LV_SYMBOL_EDIT " Change", onFavAction, devview::F_CHANGE, false);
convview::actionButton(acts, LV_SYMBOL_TRASH " Remove", onFavAction, devview::F_REMOVE, false);
lv_obj_t* acts = buttonBar(panel);
barButton(acts, LV_SYMBOL_EDIT " Change", onFavAction, devview::F_CHANGE, false);
barButton(acts, LV_SYMBOL_TRASH " Remove", onFavAction, devview::F_REMOVE, false);
}
void UITask::favAction(uint8_t act) {
+4 -16
View File
@@ -284,10 +284,8 @@ void UITask::buildDiag() {
s_noise_status = nullptr;
if (s_tab == TAB_NOISE) {
lv_obj_t* t = label(s_list, "What the radio hears with nothing on air, on the mesh frequency and 1.1 MHz "
"either side. Takes about 12 seconds.", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
noteLabel(s_list, "What the radio hears with nothing on air, on the mesh frequency and 1.1 MHz "
"either side. Takes about 12 seconds.");
lv_obj_t* b = lv_button_create(s_list);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
@@ -363,18 +361,8 @@ void UITask::refreshDiag() {
}
void UITask::diagResetPopup() {
lv_obj_t* panel = navPopupPanel("Reset counters?", false);
lv_obj_t* t = label(panel, "Zeroes packet and error counts.", THEME_FONT_SMALL,
theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
lv_obj_t* b = lv_button_create(panel);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0);
lv_obj_add_event_cb(b, onDiagResetGo, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(b, LV_SYMBOL_REFRESH " Reset", THEME_FONT_BODY, theme::TEXT));
confirmBody(navPopupPanel("Reset counters?", false), "Zeroes packet and error counts.", LV_SYMBOL_REFRESH " Reset",
onDiagResetGo, 0);
}
void UITask::diagReset() {
+1 -3
View File
@@ -245,9 +245,7 @@ void UITask::buildGps() {
lv_obj_set_style_pad_row(col, 2, 0);
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);
lv_label_set_long_mode(s_used, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s_used, LV_PCT(100));
s_used = noteLabel(col, "", THEME_FONT_BODY, theme::TEXT);
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);
+69 -74
View File
@@ -387,47 +387,6 @@ static void wifiStoreWrite() {
}
}
}
// Settings > WiFi's switch: off keeps the radio off for everything (scan, map
// download). Kept with the credentials.
static int8_t s_wifi_allowed = -1; // read once (the status bar asks every second)
static bool wifiAllowed() {
if (s_wifi_allowed < 0) s_wifi_allowed = nvs::getBool("mc_wifi", "on", true);
return s_wifi_allowed;
}
static void setWifiAllowed(bool on) { s_wifi_allowed = on; nvs::putBool("mc_wifi", "on", on); }
// Map tools > Live tiles: missing tiles fetched over WiFi while the map is open.
static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); }
static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); }
// Map tools > Hiking trails: the Waymarked Trails overlay over the map.
static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); }
static void setTrailsOn(bool on) { nvs::putBool("mc_ui", "trails", on); }
// Map tools > Vector map (test).
static bool vectorOn() { return nvs::getBool("mc_ui", "vector", false); }
static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", on); }
// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none.
static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); }
static void savePin(const char* pin) { nvs::putStr("mc_lock", "pin", pin); }
// Accent colour (Settings > Display & power): an index into theme::ACCENTS.
static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); }
static void saveAccent(int idx) { nvs::putU8("mc_ui", "accent", idx); }
// Settings > Storage > Kept per conversation (an index into histstore::KEEP).
static int loadHistKeep() { return nvs::getI8("mc_ui", "hkeep", -1); }
static void saveHistKeep(int idx) { nvs::putI8("mc_ui", "hkeep", idx); }
// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default.
static int loadLiveCap() { return nvs::getI8("mc_ui", "ltcap", -1); }
static void saveLiveCap(int idx) { nvs::putI8("mc_ui", "ltcap", idx); }
// Settings > Display & power > Tap to wake: a touch turns the dark screen on
// (off: only the top button does).
static bool loadTapWake() { return nvs::getBool("mc_ui", "tapwake", true); }
static void saveTapWake(bool on) { nvs::putBool("mc_ui", "tapwake", on); }
// Messages: which sections are folded (bit per section).
static int loadChatFold() { return nvs::getU8("mc_ui", "chfold", 0); }
static void saveChatFold(int bits) { nvs::putU8("mc_ui", "chfold", bits); }
// Home's apps: their letters in the user's order, a hidden one in lower case.
static void loadHomeApps(char* out, size_t n) { nvs::getStr("mc_ui", "apps", out, n); }
static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); }
// Filesystem size and space in use (Settings > Storage), plus the card's own
// size -- a card whose FAT partition is small (e.g. written by a Raspberry Pi
// imager) shows both. The first free-space count on a big card takes a moment.
@@ -697,6 +656,32 @@ static const char* fetchError() { return s_ferr; }
// Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to
// a <canvas>, the host page feeds the mouse in as touch.
// The device's NVS (Preferences) as a table for the session: the UI settings
// below read and write it by the same names.
namespace nvs {
struct Kv { char ns[10]; char key[10]; char val[40]; };
static Kv s_kv[24];
static int s_kv_n = 0;
static Kv* find(const char* ns, const char* key, bool add) {
for (int i = 0; i < s_kv_n; i++) if (!strcmp(s_kv[i].ns, ns) && !strcmp(s_kv[i].key, key)) return &s_kv[i];
if (!add || s_kv_n >= (int)(sizeof(s_kv) / sizeof(s_kv[0]))) return nullptr;
Kv* k = &s_kv[s_kv_n++];
snprintf(k->ns, sizeof(k->ns), "%s", ns);
snprintf(k->key, sizeof(k->key), "%s", key);
return k;
}
static int getInt(const char* ns, const char* key, int def) { Kv* k = find(ns, key, false); return k ? atoi(k->val) : def; }
static void putInt(const char* ns, const char* key, int v) { if (Kv* k = find(ns, key, true)) snprintf(k->val, sizeof(k->val), "%d", v); }
static bool getBool(const char* ns, const char* key, bool def) { return getInt(ns, key, def) != 0; }
static void putBool(const char* ns, const char* key, bool v) { putInt(ns, key, v); }
static int getI8(const char* ns, const char* key, int def) { return getInt(ns, key, def); }
static void putI8(const char* ns, const char* key, int v) { putInt(ns, key, v); }
static int getU8(const char* ns, const char* key, int def) { return getInt(ns, key, def); }
static void putU8(const char* ns, const char* key, int v) { putInt(ns, key, v); }
static void getStr(const char* ns, const char* key, char* out, size_t n) { Kv* k = find(ns, key, false); snprintf(out, n, "%s", k ? k->val : ""); }
static void putStr(const char* ns, const char* key, const char* v) { if (Kv* k = find(ns, key, true)) snprintf(k->val, sizeof(k->val), "%s", v); }
} // namespace nvs
static bool s_swallow = false;
static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
@@ -763,27 +748,6 @@ static void restart() {} // the page's Reset does that
// The browser is always online; saved WiFi networks only live for the session.
static void wifiStoreLoad() { s_net_count = 0; }
static void wifiStoreWrite() {}
static char s_pin[9] = ""; // the screen PIN, for the session
static void loadPin(char* out, size_t n) { snprintf(out, n, "%s", s_pin); }
static void savePin(const char* pin) { snprintf(s_pin, sizeof(s_pin), "%s", pin); }
static int s_accent = 0;
static int loadAccent() { return s_accent; }
static void saveAccent(int idx) { s_accent = idx; }
static int s_hist_keep = -1;
static int loadHistKeep() { return s_hist_keep; }
static void saveHistKeep(int idx) { s_hist_keep = idx; }
static int s_live_cap = -1;
static int loadLiveCap() { return s_live_cap; }
static void saveLiveCap(int idx) { s_live_cap = idx; }
static bool s_tap_wake = true;
static bool loadTapWake() { return s_tap_wake; }
static void saveTapWake(bool on) { s_tap_wake = on; }
static int s_chat_fold = 0;
static int loadChatFold() { return s_chat_fold; }
static void saveChatFold(int bits) { s_chat_fold = bits; }
static char s_home_apps[24] = "";
static void loadHomeApps(char* out, size_t n) { snprintf(out, n, "%s", s_home_apps); }
static void saveHomeApps(const char* v) { snprintf(s_home_apps, sizeof(s_home_apps), "%s", v); }
// The browser has no card: a nominal 32 GB, used = what the files add up to
// (the storage screen counts them anyway; it passes that in).
static bool sdInfo(uint64_t& total, uint64_t& used, uint64_t& card) { total = card = 32ULL << 30; used = 0; return true; }
@@ -791,18 +755,6 @@ static const char* const FLASH_ROOT = "/sim_data";
static const char* resetReason() { return "Power on"; }
static bool crashSummary(char*, size_t) { return false; }
static bool flashInfo(uint64_t& total, uint64_t& used) { total = 1536ULL << 10; used = 0; return true; }
static bool s_wifi_on = true;
static bool wifiAllowed() { return s_wifi_on; }
static void setWifiAllowed(bool on) { s_wifi_on = on; }
static bool s_live_tiles = true;
static bool liveTiles() { return s_live_tiles; }
static void setLiveTiles(bool on) { s_live_tiles = on; }
static bool s_trails = false;
static bool trailsOn() { return s_trails; }
static void setTrailsOn(bool on) { s_trails = on; }
static bool s_vector = false;
static bool vectorOn() { return s_vector; }
static void setVectorOn(bool on) { s_vector = on; }
static bool s_net_on = false;
static void netBegin(const char*, const char*) { s_net_on = true; }
static void netJoin(const char*, const char*) { s_net_on = true; }
@@ -876,4 +828,47 @@ static const char* fetchError() { return s_fcode > 0 && s_fcode != 200 ? "HTTP e
#error "ui-lvgl: no LVGL port for this board (see LvglPort.h)"
#endif
// ── UI settings kept outside NodePrefs (nvs:: above: NVS, or the session) ──
// Settings > WiFi's switch: off keeps the radio off for everything (scan, map
// download). Kept with the credentials.
static int8_t s_wifi_allowed = -1; // read once (the status bar asks every second)
static bool wifiAllowed() {
if (s_wifi_allowed < 0) s_wifi_allowed = nvs::getBool("mc_wifi", "on", true);
return s_wifi_allowed;
}
static void setWifiAllowed(bool on) { s_wifi_allowed = on; nvs::putBool("mc_wifi", "on", on); }
// Map tools > Live tiles: missing tiles fetched over WiFi while the map is open.
static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); }
static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); }
// Map tools > Hiking trails: the Waymarked Trails overlay over the map.
static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); }
static void setTrailsOn(bool on) { nvs::putBool("mc_ui", "trails", on); }
// Map tools > Vector map (test).
static bool vectorOn() { return nvs::getBool("mc_ui", "vector", false); }
static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", on); }
// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none.
static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); }
static void savePin(const char* pin) { nvs::putStr("mc_lock", "pin", pin); }
// Accent colour (Settings > Display & power): an index into theme::ACCENTS.
static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); }
static void saveAccent(int idx) { nvs::putU8("mc_ui", "accent", idx); }
// Settings > Storage > Kept per conversation (an index into histstore::KEEP).
static int loadHistKeep() { return nvs::getI8("mc_ui", "hkeep", -1); }
static void saveHistKeep(int idx) { nvs::putI8("mc_ui", "hkeep", idx); }
// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default.
static int loadLiveCap() { return nvs::getI8("mc_ui", "ltcap", -1); }
static void saveLiveCap(int idx) { nvs::putI8("mc_ui", "ltcap", idx); }
// Settings > Display & power > Tap to wake: a touch turns the dark screen on
// (off: only the top button does).
static bool loadTapWake() { return nvs::getBool("mc_ui", "tapwake", true); }
static void saveTapWake(bool on) { nvs::putBool("mc_ui", "tapwake", on); }
// Messages: which sections are folded (bit per section).
static int loadChatFold() { return nvs::getU8("mc_ui", "chfold", 0); }
static void saveChatFold(int bits) { nvs::putU8("mc_ui", "chfold", bits); }
// Home's apps: their letters in the user's order, a hidden one in lower case.
static void loadHomeApps(char* out, size_t n) { nvs::getStr("mc_ui", "apps", out, n); }
static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); }
} // namespace lvport
+7 -19
View File
@@ -34,7 +34,6 @@ static int s_prev_z;
static bool s_prev_follow;
static int s_idx = -1; // area in the open popup
static lv_obj_t* s_del_lbl = nullptr;
static uint32_t s_del_armed_ms = 0;
static const int BAR_H = 84;
static const int HANDLE = 26;
@@ -69,11 +68,9 @@ static void boxWorld(const mapview::TileArea& b, double& x0, double& y0, double&
}
static void fmtDate(char* out, size_t n, uint32_t t, const NodePrefs* p) {
if (t < 1000000000UL) { out[0] = '\0'; return; }
time_t tt = (time_t)((int64_t)t + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
struct tm tm = *gmtime(&tt);
static const char* const MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
snprintf(out, n, "%d %s %d", tm.tm_mday, MON[tm.tm_mon], tm.tm_year + 1900);
struct tm tm;
if (!localTime(p, tm, t)) { out[0] = '\0'; return; }
snprintf(out, n, "%d %s %d", tm.tm_mday, MONTHS[tm.tm_mon], tm.tm_year + 1900);
}
static void onHandle(lv_event_t* e) {
@@ -387,9 +384,7 @@ void UITask::mapAreasPopup() {
listRow(g, m.name, sub, onArea, (void*)(intptr_t)i);
}
} else {
lv_obj_t* l = label(list, "No areas yet. Maps downloaded before this list are still on the card.", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
noteLabel(list, "No areas yet. Maps downloaded before this list are still on the card.");
}
lv_obj_t* add = lv_button_create(list);
lv_obj_set_size(add, LV_PCT(100), 38);
@@ -412,16 +407,13 @@ void UITask::mapAreaPopup(int idx) {
snprintf(info, sizeof(info), "Zoom %d-%d, %lu tiles%s%s%s%s", m.box.zmin, m.box.zmax, (unsigned long)n,
(m.flags & mapview::AreaStore::F_TRAILS) ? ", trails" : "", date[0] ? " - " : "", date,
(m.flags & mapview::AreaStore::F_COMPLETE) ? "" : "\nUnfinished - Fill gaps completes it");
lv_obj_t* l = label(panel, info, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* r = toolRow(panel);
noteLabel(panel, info);
lv_obj_t* r = buttonBar(panel);
actButton(r, LV_SYMBOL_EDIT, "Rename", A_RENAME);
actButton(r, LV_SYMBOL_DOWNLOAD, "Fill", A_FILL);
actButton(r, LV_SYMBOL_REFRESH, "Refresh", A_REFRESH);
actButton(r, (m.flags & mapview::AreaStore::F_TRAILS) ? LV_SYMBOL_MINUS : LV_SYMBOL_PLUS, "Trails", A_TRAILS);
s_del_lbl = actButton(r, LV_SYMBOL_TRASH, "Delete", A_DELETE);
s_del_armed_ms = 0;
// Framed in the map above the sheet: the view is kept to go back to.
lv_obj_update_layout(panel);
@@ -492,11 +484,7 @@ void UITask::mapAreaAction(uint8_t act) {
showToast("Fetching its trails");
return;
case A_DELETE:
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms
s_del_armed_ms = millis() | 1;
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
return;
}
if (!tapConfirmed(s_del_lbl, "Delete?")) return;
if (mapview::s_areas.deleting()) { showToast("Still deleting - try again shortly"); return; }
if (mapview::s_dl.active() && mapview::s_areas.find(mapview::s_dl.area()) == s_idx) mapview::s_dl.cancel();
if (s_show == s_idx) s_show = -1;
+7 -29
View File
@@ -8,7 +8,6 @@
namespace regions {
static int s_sel = -1; // the pack a popup is about
static uint32_t s_del_armed_ms = 0;
static lv_obj_t* s_del_lbl = nullptr;
enum : uint8_t { A_SHOW, A_DELETE };
@@ -17,19 +16,8 @@ static void onDeleted(lv_event_t* e) { if (lv_event_get_target(e) == s_del_lbl)
static void onPack(lv_event_t* e) { s_ui->mapRegionPopup((int)(intptr_t)lv_event_get_user_data(e)); }
static void onAct(lv_event_t* e) { s_ui->mapRegionAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
static lv_obj_t* actButton(lv_obj_t* row, const char* text, uint8_t act, bool accent) {
lv_obj_t* b = lv_button_create(row);
lv_obj_set_height(b, 36);
lv_obj_set_flex_grow(b, 1);
lv_obj_set_style_pad_hor(b, 4, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
lv_obj_add_event_cb(b, onAct, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT);
lv_obj_center(l);
return l;
static lv_obj_t* actButton(lv_obj_t* bar, const char* text, uint8_t act, bool accent) {
return lv_obj_get_child(barButton(bar, text, onAct, act, accent), 0);
}
static void fmtMB(char* out, size_t n, uint32_t bytes) {
@@ -55,11 +43,8 @@ void UITask::mapRegionsPopup() {
listRow(g, p.name[0] ? p.name : p.file, sub, onPack, (void*)(intptr_t)i);
}
}
lv_obj_t* l = label(list, np ? "Packs in /vmap on the SD card, made with tools/maps/osm_vector.py --pack."
: "No regions yet. Make a pack with tools/maps/osm_vector.py --pack and copy it to /vmap on the SD card.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
noteLabel(list, np ? "Packs in /vmap on the SD card, made with tools/maps/osm_vector.py --pack."
: "No regions yet. Make a pack with tools/maps/osm_vector.py --pack and copy it to /vmap on the SD card.");
}
void UITask::mapRegionPopup(int idx) {
@@ -71,14 +56,11 @@ void UITask::mapRegionPopup(int idx) {
char info[128], mb[16];
fmtMB(mb, sizeof(mb), p.size);
snprintf(info, sizeof(info), "%s, zoom %d-18%s\n%s", mb, p.zmin, (p.flags & mapview::VectorPacks::F_CONTOURS) ? ", contours" : "", p.file);
lv_obj_t* l = label(panel, info, THEME_FONT_SMALL, theme::TEXT);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* r = toolRow(panel);
noteLabel(panel, info, THEME_FONT_SMALL, theme::TEXT);
lv_obj_t* r = buttonBar(panel);
actButton(r, UI_SYMBOL_MAP " Show", A_SHOW, true);
s_del_lbl = actButton(r, LV_SYMBOL_TRASH " Delete", A_DELETE, false);
lv_obj_add_event_cb(s_del_lbl, onDeleted, LV_EVENT_DELETE, NULL);
s_del_armed_ms = 0;
}
void UITask::mapRegionAction(uint8_t act) {
@@ -100,11 +82,7 @@ void UITask::mapRegionAction(uint8_t act) {
return;
}
case A_DELETE: {
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms
s_del_armed_ms = millis() | 1;
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
return;
}
if (!tapConfirmed(s_del_lbl, "Delete?")) return;
char path[80];
snprintf(path, sizeof(path), "%s/%s", mapview::VECTOR_ROOT, p.file);
mapview::s_vpacks.closeOpen();
+3 -18
View File
@@ -744,9 +744,7 @@ void UITask::mapDownloadPopup() {
headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL);
lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
_dl_info = label(panel, "", THEME_FONT_SMALL, theme::TEXT);
lv_label_set_long_mode(_dl_info, LV_LABEL_LONG_WRAP);
lv_obj_set_width(_dl_info, LV_PCT(100));
_dl_info = noteLabel(panel, "", THEME_FONT_SMALL, theme::TEXT);
lv_obj_set_style_text_line_space(_dl_info, 2, 0);
_dl_bar = lv_bar_create(panel);
lv_obj_set_size(_dl_bar, LV_PCT(100), 6);
@@ -758,21 +756,8 @@ void UITask::mapDownloadPopup() {
lv_obj_set_width(_dl_err, LV_PCT(100));
lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* acts = lv_obj_create(panel);
styleSurface(acts, theme::BG);
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
lv_obj_t* go = lv_button_create(acts);
lv_obj_set_height(go, 38);
lv_obj_set_flex_grow(go, 1);
lv_obj_set_style_shadow_width(go, 0, 0);
lv_obj_set_style_radius(go, theme::RADIUS, 0);
lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0);
lv_obj_add_event_cb(go, onDlStart, LV_EVENT_CLICKED, NULL);
_dl_start_lbl = label(go, "", THEME_FONT_SMALL, theme::TEXT);
lv_obj_center(_dl_start_lbl);
lv_obj_t* go = barButton(buttonBar(panel), "", onDlStart, 0, true);
_dl_start_lbl = lv_obj_get_child(go, 0);
stylePrimary(go);
refreshDownloadPopup();
+23 -88
View File
@@ -496,7 +496,6 @@ void UITask::navClosePopup() {
if (_nav_overlay) lv_obj_delete_async(_nav_overlay); // may be closing from its own button
_nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = nullptr;
_nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr;
_nav_del_armed_ms = 0;
}
static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right) {
@@ -543,10 +542,7 @@ void UITask::navTargetsPopup() {
if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, 52);
}
if (wp.count() == 0) {
lv_obj_t* l = label(list, "None yet. Hold the map to drop one, or tap the pin to mark where you are.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
noteLabel(list, "None yet. Hold the map to drop one, or tap the pin to mark where you are.");
}
TrailStore& ts = _core->trail.store();
if (!ts.empty()) {
@@ -598,12 +594,7 @@ void UITask::navWaypointMenu(int idx) {
lv_obj_t* panel = navPopupPanel(w.label[0] ? w.label : "(unnamed)", false);
placeCard(panel, w.lat_1e6, w.lon_1e6);
lv_obj_t* acts = lv_obj_create(panel);
styleSurface(acts, theme::BG);
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
lv_obj_t* acts = buttonBar(panel);
struct { const char* text; uint8_t act; bool accent; } btns[] = {
{ UI_SYMBOL_COMPASS " Go", navmap::WP_NAV, true },
{ LV_SYMBOL_EDIT " Name", navmap::WP_RENAME, false },
@@ -611,17 +602,8 @@ void UITask::navWaypointMenu(int idx) {
{ LV_SYMBOL_TRASH, navmap::WP_DELETE, false },
};
for (auto& b : btns) {
lv_obj_t* bt = lv_button_create(acts);
lv_obj_set_height(bt, 40);
lv_obj_set_flex_grow(bt, 1);
lv_obj_set_style_pad_hor(bt, 4, 0);
lv_obj_set_style_radius(bt, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(bt, 0, 0);
lv_obj_set_style_bg_color(bt, lv_color_hex(b.accent ? theme::ACCENT_DIM : theme::SURFACE), 0);
lv_obj_add_event_cb(bt, onNavWpAction, LV_EVENT_CLICKED, (void*)(uintptr_t)b.act);
lv_obj_t* l = label(bt, b.text, THEME_FONT_SMALL, theme::TEXT);
lv_obj_center(l);
if (b.act == navmap::WP_DELETE) _nav_del_lbl = l;
lv_obj_t* bt = barButton(acts, b.text, onNavWpAction, b.act, b.accent);
if (b.act == navmap::WP_DELETE) _nav_del_lbl = lv_obj_get_child(bt, 0);
}
}
@@ -643,11 +625,7 @@ void UITask::navWaypointAction(uint8_t act) {
break;
}
case navmap::WP_DELETE:
if (!_nav_del_armed_ms || millis() - _nav_del_armed_ms > 3000) { // second tap within 3 s confirms
_nav_del_armed_ms = millis() | 1;
if (_nav_del_lbl) lv_label_set_text(_nav_del_lbl, "Delete?");
break;
}
if (!tapConfirmed(_nav_del_lbl, "Delete?")) break;
_core->waypoints.remove(i);
navClosePopup();
showToast("Waypoint deleted");
@@ -805,15 +783,13 @@ static const int ST_MAX = 40;
static char s_st_names[ST_MAX][28]; // newest first
static int s_st_n = 0;
static int s_st_sel = -2; // the one the popup is about; -1 = the internal slot
static uint32_t s_st_del_armed_ms = 0;
static lv_obj_t* s_st_del_lbl = nullptr;
// "trail-20260926-1405.trl" -> "26 Sep 2026 14:05" (else the name).
static void trailTitle(const char* name, char* out, size_t n) {
static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
int y, mo, d, h, mi;
if (sscanf(name, "trail-%4d%2d%2d-%2d%2d", &y, &mo, &d, &h, &mi) == 5 && mo >= 1 && mo <= 12)
snprintf(out, n, "%d %s %d %02d:%02d", d, MON[mo - 1], y, h, mi);
snprintf(out, n, "%d %s %d %02d:%02d", d, MONTHS[mo - 1], y, h, mi);
else snprintf(out, n, "%s", name);
}
@@ -871,29 +847,9 @@ static void onNavShareTarget(lv_event_t* e) {
s_ui->navSetShareTarget(choiceSelected((lv_obj_t*)lv_event_get_target(e)));
}
static lv_obj_t* toolRow(lv_obj_t* parent) {
lv_obj_t* r = lv_obj_create(parent);
styleSurface(r, theme::BG);
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(r, theme::GAP, 0);
return r;
}
static lv_obj_t* toolButton(lv_obj_t* row, const char* text, uint8_t act, bool accent) {
lv_obj_t* b = lv_button_create(row);
lv_obj_set_height(b, 36);
lv_obj_set_flex_grow(b, 1);
lv_obj_set_style_pad_hor(b, 4, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
lv_obj_add_event_cb(b, onNavTool, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT);
lv_obj_center(l);
return l;
// A map tool's button (UITask.cpp's barButton); returns its label.
static lv_obj_t* toolButton(lv_obj_t* bar, const char* text, uint8_t act, bool accent) {
return lv_obj_get_child(barButton(bar, text, onNavTool, act, accent), 0);
}
void UITask::navToolsPopup() {
@@ -902,11 +858,11 @@ void UITask::navToolsPopup() {
sectionTitle(list, "TRAIL");
_nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT);
lv_obj_t* r = toolRow(list);
lv_obj_t* r = buttonBar(list);
_nav_trail_btn = toolButton(r, "", navmap::TL_TRAIL_TOGGLE, true);
toolButton(r, LV_SYMBOL_SAVE " Save", navmap::TL_TRAIL_SAVE, false);
toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_TRAIL_LOAD, false);
r = toolRow(list);
r = buttonBar(list);
_nav_tb_btn = toolButton(r, "", navmap::TL_TRACKBACK, false);
toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_TRAIL_GPX, false);
_nav_reset_lbl = toolButton(r, LV_SYMBOL_TRASH " Reset", navmap::TL_TRAIL_RESET, false);
@@ -914,9 +870,7 @@ void UITask::navToolsPopup() {
(void*)(uintptr_t)navmap::TL_OPT_TRAIL);
sectionTitle(list, "LIVE SHARE");
_nav_share_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT);
lv_label_set_long_mode(_nav_share_lbl, LV_LABEL_LONG_WRAP);
lv_obj_set_width(_nav_share_lbl, LV_PCT(100));
_nav_share_lbl = noteLabel(list, "", THEME_FONT_SMALL, theme::TEXT);
// Target: channels (as named -- "#" marks a hashtag channel), then
// favourite contacts (starred, so the two stay apart).
char opts[MAX_GROUP_CHANNELS * 24 + 16 * 40];
@@ -943,7 +897,7 @@ void UITask::navToolsPopup() {
favs++;
}
choiceRow(group(list, nullptr), "Send to", nullptr, o ? opts : "(no channels)", sel, onNavShareTarget);
r = toolRow(list);
r = buttonBar(list);
_nav_share_btn = toolButton(r, "", navmap::TL_SHARE_TOGGLE, true);
toolButton(r, LV_SYMBOL_UPLOAD " Send once", navmap::TL_SHARE_ONCE, false);
@@ -978,7 +932,6 @@ void UITask::navToolsPopup() {
snprintf(vr, sizeof(vr), nv ? "%d on the card" : "None - packs go in /vmap", nv);
listRow(g, "Vector regions", vr, onNavTool, (void*)(uintptr_t)navmap::TL_REGIONS);
_nav_reset_armed_ms = 0;
refreshNavTools();
}
@@ -995,10 +948,6 @@ void UITask::refreshNavTools() {
else lv_label_set_text_fmt(_nav_trail_lbl, "%s - %s, %s, %d points", state, dist, dur, ts.count());
lv_label_set_text(_nav_trail_btn, ts.isActive() ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Record");
lv_label_set_text(_nav_tb_btn, navmap::s_tb.active() ? LV_SYMBOL_STOP " Stop back" : LV_SYMBOL_LOOP " Track back");
if (_nav_reset_armed_ms && millis() - _nav_reset_armed_ms > 3000) {
_nav_reset_armed_ms = 0;
lv_label_set_text(_nav_reset_lbl, LV_SYMBOL_TRASH " Reset");
}
if (_prefs->loc_share_enabled) {
char left[12];
@@ -1024,11 +973,10 @@ void UITask::navSetShareTarget(int sel) {
static void trailFilePath(char* path, size_t n, const char* ext, const NodePrefs* p) {
char stem[40];
uint32_t now = rtc_clock.getCurrentTime();
if (now > 1000000000UL) {
time_t t = (time_t)((int64_t)now + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
struct tm* ti = gmtime(&t);
snprintf(stem, sizeof(stem), "%s/trail-%04d%02d%02d-%02d%02d", navmap::TRAILS_DIR, ti->tm_year + 1900,
ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min);
struct tm ti;
if (localTime(p, ti, now)) {
snprintf(stem, sizeof(stem), "%s/trail-%04d%02d%02d-%02d%02d", navmap::TRAILS_DIR, ti.tm_year + 1900,
ti.tm_mon + 1, ti.tm_mday, ti.tm_hour, ti.tm_min);
} else {
snprintf(stem, sizeof(stem), "%s/trail-%lu", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000));
}
@@ -1090,7 +1038,6 @@ void UITask::savedTrailsPopup() {
void UITask::savedTrailPopup(int idx) {
if (idx >= navmap::s_st_n) return;
navmap::s_st_sel = idx;
navmap::s_st_del_armed_ms = 0;
char title[32];
if (idx < 0) snprintf(title, sizeof(title), "Saved on the device");
else navmap::trailTitle(navmap::s_st_names[idx], title, sizeof(title));
@@ -1113,7 +1060,7 @@ void UITask::savedTrailPopup(int idx) {
label(panel, info, THEME_FONT_BODY, theme::TEXT);
if (!_core->trail.store().empty())
label(panel, "Loading replaces the trail on the map.", THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_obj_t* r = toolRow(panel);
lv_obj_t* r = buttonBar(panel);
lv_obj_t* lb = toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_ST_LOAD, true);
stylePrimary(lv_obj_get_parent(lb));
if (idx >= 0) toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_ST_GPX, false);
@@ -1126,11 +1073,7 @@ void UITask::savedTrailAction(uint8_t act) {
char path[64] = "";
if (idx >= 0) snprintf(path, sizeof(path), "%s/%s", navmap::TRAILS_DIR, navmap::s_st_names[idx]);
if (act == navmap::TL_ST_DELETE) {
if (!navmap::s_st_del_armed_ms || millis() - navmap::s_st_del_armed_ms > 3000) { // second tap confirms
navmap::s_st_del_armed_ms = millis() | 1;
if (navmap::s_st_del_lbl) lv_label_set_text(navmap::s_st_del_lbl, "Delete?");
return;
}
if (!tapConfirmed(navmap::s_st_del_lbl, "Delete?")) return;
bool ok = idx >= 0 ? remove(path) == 0
: (the_mesh.getDataStore() && the_mesh.getDataStore()->removeFile(TrailEngine::TRAIL_FILE));
showToast(ok ? "Trail deleted" : "Delete failed");
@@ -1209,13 +1152,8 @@ void UITask::navToolAction(uint8_t act) {
savedTrailAction(act);
return;
case navmap::TL_TRAIL_RESET:
if (!_nav_reset_armed_ms || millis() - _nav_reset_armed_ms > 3000) { // second tap within 3 s confirms
_nav_reset_armed_ms = millis() | 1;
lv_label_set_text(_nav_reset_lbl, "Reset?");
return;
}
if (!tapConfirmed(_nav_reset_lbl, "Reset?")) return;
tr.reset();
_nav_reset_armed_ms = 0;
showToast("Trail cleared");
rebuildMapMarkers();
layoutMap();
@@ -1312,7 +1250,7 @@ void UITask::navWaypointsPopup() {
char title[32];
snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY);
lv_obj_t* panel = navPopupPanel(title, true);
lv_obj_t* r = toolRow(panel);
lv_obj_t* r = buttonBar(panel);
toolButton(r, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true);
toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false);
@@ -1332,10 +1270,7 @@ void UITask::navWaypointsPopup() {
}
}
if (wp.count() == 0) {
lv_obj_t* l = label(list, "None yet. Add one here, type its coordinates, or hold the map on a spot.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
noteLabel(list, "None yet. Add one here, type its coordinates, or hold the map on a spot.");
}
}
@@ -1398,7 +1333,7 @@ void UITask::navSpotPopup(int32_t lat, int32_t lon) {
_nav_spot_lon = lon;
lv_obj_t* panel = navPopupPanel("This spot", false);
placeCard(panel, lat, lon);
lv_obj_t* r = toolRow(panel);
lv_obj_t* r = buttonBar(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);
}
+1 -3
View File
@@ -186,9 +186,7 @@ void UITask::buildOta() {
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_status = noteLabel(body, "", THEME_FONT_BODY, theme::TEXT);
_ota_bar = lv_bar_create(body);
lv_obj_set_size(_ota_bar, LV_PCT(100), 10);
lv_bar_set_range(_ota_bar, 0, 1000);
+13 -32
View File
@@ -22,24 +22,6 @@ static uint8_t s_naming = N_PRESET;
static int s_slot = -1; // user preset slot the popup is about
static int s_scope = -1; // scope list index the popup is about
static lv_obj_t* redButton(lv_obj_t* panel, const char* text, lv_event_cb_t cb, uint8_t act) {
lv_obj_t* b = lv_button_create(panel);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
lv_obj_center(label(b, text, THEME_FONT_BODY, theme::TEXT));
return b;
}
static lv_obj_t* note(lv_obj_t* parent, const char* text) {
lv_obj_t* t = label(parent, text, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
return t;
}
} // namespace radiox
static void onPresetRow(lv_event_t* e) { s_ui->presetMenu((int)(uintptr_t)lv_event_get_user_data(e)); }
@@ -86,9 +68,9 @@ void UITask::presetMenu(int slot) {
char info[64];
snprintf(info, sizeof(info), "%.3f MHz, SF%u, BW %g kHz, CR 4/%u", u.freq, (unsigned)u.sf, (double)u.bw, (unsigned)u.cr);
label(panel, info, THEME_FONT_BODY, theme::TEXT);
lv_obj_t* acts = convview::actionRow(panel);
convview::actionButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true);
convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false);
lv_obj_t* acts = buttonBar(panel);
barButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true);
barButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false);
}
void UITask::presetAction(uint8_t act) {
@@ -108,9 +90,8 @@ void UITask::presetAction(uint8_t act) {
case P_DELETE: {
char t[40];
snprintf(t, sizeof(t), "Delete %s?", u.name);
lv_obj_t* panel = navPopupPanel(t, false);
note(panel, "Removes the saved preset. The radio settings stay as they are.");
redButton(panel, LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO);
confirmBody(navPopupPanel(t, false), "Removes the saved preset. The radio settings stay as they are.",
LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO);
break;
}
case P_DELETE_GO:
@@ -199,13 +180,13 @@ void UITask::scopeMenu(int idx) {
radiox::s_scope = idx;
bool def = idx == sl.default_idx;
lv_obj_t* panel = navPopupPanel(idx == 0 ? "* (no scope)" : sl.name((uint8_t)idx), false);
if (def) radiox::note(panel, "This is the default: direct messages and relaying use it.");
if (def) noteLabel(panel, "This is the default: direct messages and relaying use it.");
if (idx == 0 && def) return; // nothing else to do with "*"
lv_obj_t* acts = convview::actionRow(panel);
if (!def) convview::actionButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true);
lv_obj_t* acts = buttonBar(panel);
if (!def) barButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true);
if (idx > 0) {
convview::actionButton(acts, LV_SYMBOL_EDIT " Rename", onScopeAction, radiox::S_RENAME, false);
convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onScopeAction, radiox::S_DELETE, false);
barButton(acts, LV_SYMBOL_EDIT " Rename", onScopeAction, radiox::S_RENAME, false);
barButton(acts, LV_SYMBOL_TRASH " Delete", onScopeAction, radiox::S_DELETE, false);
}
}
@@ -227,9 +208,9 @@ void UITask::scopeAction(uint8_t act) {
if (s_scope == 0) break;
char t[40];
snprintf(t, sizeof(t), "Delete %s?", sl.name((uint8_t)s_scope));
lv_obj_t* panel = navPopupPanel(t, false);
note(panel, "Channels using it go back to no scope. If it is the default, * becomes the default.");
redButton(panel, LV_SYMBOL_TRASH " Delete", onScopeAction, S_DELETE_GO);
confirmBody(navPopupPanel(t, false),
"Channels using it go back to no scope. If it is the default, * becomes the default.",
LV_SYMBOL_TRASH " Delete", onScopeAction, S_DELETE_GO);
break;
}
case S_DELETE_GO:
@@ -133,10 +133,7 @@ void UITask::repeaterScopesPopup() {
if (sl.count == 0) { showToast("Set up scopes in Settings > Radio first"); return; }
lv_obj_t* panel = navPopupPanel("Extra scopes", sl.count > 2);
if (sl.count > 2) lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); // up to 8 rows
lv_obj_t* t = label(panel, "Relayed besides the default scope.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
noteLabel(panel, "Relayed besides the default scope.");
for (uint8_t i = 0; i < sl.count; i++) {
lv_obj_t* sw = switchRow(panel, sl.name((uint8_t)(i + 1)), (i + 1) == sl.default_idx ? "Default - always relayed" : nullptr,
nullptr);
@@ -37,14 +37,11 @@ static lv_obj_t* s_sd_lbl = nullptr;
static lv_obj_t* s_cat_val[C_COUNT];
static lv_obj_t* s_status = nullptr;
static lv_obj_t* s_clear_lbl = nullptr;
static uint32_t s_clear_armed_ms = 0;
static lv_obj_t* s_live_lbl = nullptr; // "Delete live map tiles" row label
static lv_obj_t* s_live_info = nullptr; // count / size from the index
static uint32_t s_live_armed_ms = 0;
static uint32_t s_shown_ms = 0;
static lv_obj_t* s_fmt_overlay = nullptr; // Format card: the popup
static lv_obj_t* s_fmt_go_lbl = nullptr; // its Format button's label
static bool s_fmt_armed = false; // Format tapped once
static void fmtBytes(char* out, size_t n, uint64_t b) {
if (b < 1024) snprintf(out, n, "%u B", (unsigned)b);
@@ -264,9 +261,7 @@ void UITask::buildStorage() {
lv_obj_t* body = newScreen("Storage", true);
s_sd_bar = s_sd_lbl = s_status = s_clear_lbl = s_live_lbl = s_live_info = nullptr;
s_fmt_overlay = s_fmt_go_lbl = nullptr; // went with the old screen
s_fmt_armed = false;
memset(s_cat_val, 0, sizeof(s_cat_val));
s_clear_armed_ms = 0;
lv_obj_t* g = group(body, "SD CARD");
if (!lvport::mountStorage()) {
@@ -310,7 +305,6 @@ void UITask::buildStorage() {
choiceRow(g, "Keep up to", "Past it, the oldest go first", mapview::LIVE_CAP_OPTS, cap_idx, onLiveCap);
s_live_lbl = actionRow(g, LV_SYMBOL_TRASH " Delete live map tiles", onLiveClear, NULL, theme::FAIL);
groupNote(body, "Downloaded areas stay.");
s_live_armed_ms = 0;
s_shown_ms = 0;
}
@@ -326,9 +320,7 @@ void UITask::pollStorage() {
fmtBytes(t, sizeof(t), s_sd_total);
snprintf(line, sizeof(line), "The card is %s but its FAT partition only %s. "
"Format it as one FAT32 partition on a computer to use all of it.", c, t);
lv_obj_t* n = label(lv_obj_get_parent(s_sd_bar), line, THEME_FONT_SMALL, theme::ACCENT);
lv_label_set_long_mode(n, LV_LABEL_LONG_WRAP);
lv_obj_set_width(n, LV_PCT(100));
noteLabel(lv_obj_get_parent(s_sd_bar), line, THEME_FONT_SMALL, theme::ACCENT);
}
}
if (s_walking) stepWalk(12);
@@ -347,14 +339,6 @@ void UITask::pollStorage() {
else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b);
lv_label_set_text(s_live_info, t);
}
if (s_live_armed_ms && millis() - s_live_armed_ms > 4000) {
s_live_armed_ms = 0;
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles");
}
if (s_clear_armed_ms && millis() - s_clear_armed_ms > 4000) { // the confirm tap timed out
s_clear_armed_ms = 0;
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history");
}
}
void UITask::storageKeep(int idx) {
@@ -367,16 +351,10 @@ void UITask::storageKeep(int idx) {
void UITask::storageClearHistory() {
using namespace storeview;
if (!s_clear_armed_ms) { // first tap: ask
s_clear_armed_ms = millis();
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Tap again to delete");
return;
}
s_clear_armed_ms = 0;
if (!tapConfirmed(s_clear_lbl, LV_SYMBOL_TRASH " Tap again to delete")) return;
s_archive.clearAll();
_core->history.clearAll();
_core->markAllRead();
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history");
showToast("Message history deleted");
startWalk();
}
@@ -389,14 +367,8 @@ void UITask::storageLiveCap(int idx) {
void UITask::storageClearLive() {
using namespace storeview;
if (!s_live_armed_ms) { // first tap: ask
s_live_armed_ms = millis();
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete");
return;
}
s_live_armed_ms = 0;
if (!tapConfirmed(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete")) return;
mapview::s_live_cache.clearAll(); // a few files per loop (mapDownloadTick)
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles");
showToast("Deleting live map tiles");
}
@@ -419,15 +391,11 @@ static lv_obj_t* fmtButton(lv_obj_t* row, const char* text, uint32_t bg, bool go
void UITask::storageFormatAsk() {
using namespace storeview;
if (s_fmt_overlay) return;
s_fmt_armed = false;
lv_obj_t* panel = popupOpen(screen(), POP_FIT, s_fmt_overlay);
lv_obj_set_style_pad_row(panel, 8, 0);
label(panel, "Format the SD card?", THEME_FONT_TITLE, theme::TEXT);
lv_obj_t* t = label(panel, "Everything on it is erased: map areas, live map tiles, message history, "
"trails and screenshots. The device restarts afterwards.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
noteLabel(panel, "Everything on it is erased: map areas, live map tiles, message history, "
"trails and screenshots. The device restarts afterwards.");
lv_obj_t* row = flexBox(panel, LV_FLEX_FLOW_ROW);
lv_obj_set_width(row, LV_PCT(100));
lv_obj_set_style_pad_column(row, theme::GAP, 0);
@@ -442,11 +410,7 @@ void UITask::storageFormatTap(bool go) {
s_fmt_overlay = s_fmt_go_lbl = nullptr;
return;
}
if (!s_fmt_armed) { // the second confirmation
s_fmt_armed = true;
if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Tap again");
return;
}
if (!tapConfirmed(s_fmt_go_lbl, "Tap again")) return; // the second confirmation
if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Formatting...");
lv_refr_now(NULL); // shown before the loop blocks
stopWalk();
@@ -485,9 +449,7 @@ static void usbPanel(const char* title, const char* text, bool buttons) {
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
label(row, LV_SYMBOL_USB, THEME_FONT_TITLE, theme::ACCENT);
label(row, title, THEME_FONT_TITLE, theme::TEXT);
lv_obj_t* t = label(panel, text, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
noteLabel(panel, text);
if (!buttons) return;
lv_obj_t* br = flexBox(panel, LV_FLEX_FLOW_ROW);
lv_obj_set_width(br, LV_PCT(100));
+103 -74
View File
@@ -132,6 +132,14 @@ static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font
lv_obj_set_style_text_color(l, lv_color_hex(color), 0);
return l;
}
// A sentence across the parent's width, wrapping: a hint, a note, a status.
static lv_obj_t* noteLabel(lv_obj_t* parent, const char* text, const lv_font_t* font = THEME_FONT_SMALL,
uint32_t color = theme::TEXT_MUTED) {
lv_obj_t* l = label(parent, text, font, color);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
return l;
}
// A list filling the rest of a flex column, scrolling on its own (the
// scrollbar only while it moves).
@@ -277,9 +285,7 @@ static lv_obj_t* infoNote(lv_obj_t* card, const char* key, const char* text) {
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));
lv_obj_t* t = noteLabel(r, text, THEME_FONT_SMALL, theme::TEXT);
return t;
}
@@ -322,9 +328,7 @@ static lv_obj_t* group(lv_obj_t* parent, const char* title) {
}
static lv_obj_t* groupNote(lv_obj_t* parent, const char* text) {
lv_obj_t* l = label(parent, text, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* l = noteLabel(parent, text);
lv_obj_set_style_pad_hor(l, theme::PAD, 0);
return l;
}
@@ -357,13 +361,9 @@ static lv_obj_t* groupText(lv_obj_t* row, const char* text, const char* hint, ui
// after its flex layout, which then sits everything at the top.
lv_obj_set_style_min_height(t, theme::ROW_H - 12, 0);
lv_obj_set_flex_align(t, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_t* l = label(t, text, THEME_FONT_BODY, col);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* l = noteLabel(t, text, THEME_FONT_BODY, col);
if (hint) {
lv_obj_t* h = label(t, hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP);
lv_obj_set_width(h, LV_PCT(100));
noteLabel(t, hint);
}
lv_obj_set_user_data(row, l); // rowTitle()
return l;
@@ -656,6 +656,77 @@ static void stylePrimary(lv_obj_t* b) {
lv_obj_set_style_text_color(lv_obj_get_child(b, i), lv_color_hex(theme::BG), 0);
}
// A row of equal buttons at the foot of a popup or screen: buttonBar(), then
// barButton() for each -- `accent` for the main action or one that's on
// (barButtonOn() flips it later). Returns the button; its label is child 0.
static lv_obj_t* buttonBar(lv_obj_t* parent) {
lv_obj_t* bar = lv_obj_create(parent);
styleSurface(bar, theme::BG);
lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(bar, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(bar, theme::GAP, 0);
return bar;
}
static void barButtonOn(lv_obj_t* b, bool on) {
lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0);
}
static lv_obj_t* barButton(lv_obj_t* bar, const char* text, lv_event_cb_t cb, uintptr_t user, bool accent = false) {
lv_obj_t* b = lv_button_create(bar);
lv_obj_set_height(b, 40);
lv_obj_set_flex_grow(b, 1);
lv_obj_set_style_pad_hor(b, 4, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
barButtonOn(b, accent);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)user);
lv_obj_center(label(b, text, THEME_FONT_SMALL, theme::TEXT));
return b;
}
// Asking before something that can't be undone: under navPopupPanel("...?"),
// what happens in a sentence and the red button that does it.
static lv_obj_t* confirmBody(lv_obj_t* panel, const char* text, const char* button, lv_event_cb_t cb, uintptr_t user) {
noteLabel(panel, text);
lv_obj_t* b = lv_button_create(panel);
lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)user);
lv_obj_center(label(b, button, THEME_FONT_BODY, theme::TEXT));
return b;
}
// A button that asks by itself before it destroys: the first tap puts `ask`
// on its label, a second tap within 3 s is the yes. Left alone, the label
// comes back and the next tap asks again. One button asks at a time.
static lv_obj_t* s_ask_lbl = nullptr;
static char s_ask_text[64];
static lv_timer_t* s_ask_timer = nullptr;
static void onAskDeleted(lv_event_t* e);
static void askReset() {
if (s_ask_timer) { lv_timer_delete(s_ask_timer); s_ask_timer = nullptr; }
if (!s_ask_lbl) return;
lv_obj_remove_event_cb(s_ask_lbl, onAskDeleted);
lv_label_set_text(s_ask_lbl, s_ask_text);
s_ask_lbl = nullptr;
}
static void onAskDeleted(lv_event_t* e) { (void)e; s_ask_lbl = nullptr; askReset(); }
static bool tapConfirmed(lv_obj_t* lbl, const char* ask) {
bool yes = lbl && lbl == s_ask_lbl;
askReset();
if (yes || !lbl) return yes;
s_ask_lbl = lbl;
snprintf(s_ask_text, sizeof(s_ask_text), "%s", lv_label_get_text(lbl));
lv_label_set_text(lbl, ask);
lv_obj_add_event_cb(lbl, onAskDeleted, LV_EVENT_DELETE, NULL);
s_ask_timer = lv_timer_create([](lv_timer_t*) { s_ask_timer = nullptr; askReset(); }, 3000, NULL);
lv_timer_set_repeat_count(s_ask_timer, 1);
return false;
}
// "@[nick]" -> "@nick" in place, for one-line text (previews, quotes) where
// the bubble's highlight doesn't reach.
static void plainMentions(char* t) {
@@ -698,14 +769,12 @@ static void tileBadge(lv_obj_t* tile, int n) {
lv_obj_align(lv_obj_get_child(tile, -1), LV_ALIGN_TOP_RIGHT, -2, 2);
}
// Local time (NodePrefs::tz_offset_hours); false before the clock is set.
static bool localTime(const NodePrefs* p, struct tm& out) {
uint32_t now = rtc_clock.getCurrentTime();
if (now <= 1000000000UL) return false;
time_t t = (time_t)((int64_t)now + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
out = *gmtime(&t);
return true;
// Local time (NodePrefs::tz_offset_hours), now or at `utc`; false before the clock is set.
static bool localTime(const NodePrefs* p, struct tm& out, uint32_t utc = rtc_clock.getCurrentTime()) {
return localTm(utc, p ? p->tz_offset_hours : 0, out);
}
static const char* const MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
// "14:05", or "2:05" (+ " PM" with suffix) with Settings > 12-hour clock;
// with `seconds`, ":09" follows unless Settings > Clock seconds is off.
static void fmtClock(char* b, size_t n, const struct tm& ti, const NodePrefs* p, bool suffix, bool seconds = false) {
@@ -721,9 +790,7 @@ static void fmtClock(char* b, size_t n, const struct tm& ti, const NodePrefs* p,
// "Thu 25 Sep 2026".
static void fmtDate(char* b, size_t n, const struct tm& ti) {
static const char* DOW[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
snprintf(b, n, "%s %d %s %d", DOW[ti.tm_wday], ti.tm_mday, MON[ti.tm_mon], ti.tm_year + 1900);
snprintf(b, n, "%s %d %s %d", DOW[ti.tm_wday], ti.tm_mday, MONTHS[ti.tm_mon], ti.tm_year + 1900);
}
// Settings changed here reach flash once their screen is left (as on the L1:
@@ -777,13 +844,11 @@ static const NodePrefs* s_prefs = nullptr; // for the free helpers below; set
static bool clockBehind(uint32_t now, uint32_t ts) { return ts > 1000000000UL && ts > now + 120; }
static void fmtMsgAge(char* b, size_t n, uint32_t now, uint32_t ts, const NodePrefs* p) {
if (!clockBehind(now, ts)) { geo::fmtAgeShort(b, (int)n, now, ts ? ts : now); return; }
static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
time_t t = (time_t)((int64_t)ts + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
struct tm ti = *gmtime(&t);
struct tm ti;
localTime(p, ti, ts); // set: clockBehind() says ts is
char clk[12];
fmtClock(clk, sizeof(clk), ti, p, true);
snprintf(b, n, "%d %s %s", ti.tm_mday, MON[ti.tm_mon], clk);
snprintf(b, n, "%d %s %s", ti.tm_mday, MONTHS[ti.tm_mon], clk);
}
static void contactName(const uint8_t* prefix, char* out, size_t n) {
@@ -1056,7 +1121,7 @@ uint32_t UITask::idleMillis(uint32_t lv_next) {
if (_buzzer.isPlaying()) return 1;
#endif
if (isClientConnected()) return 2;
if (_asleep) return 20; // the buttons and tap to wake are polled every 50 ms
if (_asleep) return 50; // as often as the buttons and tap to wake are polled
return lv_next < 10 ? lv_next : 10; // LVGL's next timer: a refresh, an animation, input
}
@@ -2204,11 +2269,9 @@ void UITask::refreshScanPopup() {
lv_obj_align(label(row, right, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
}
if (n == 0) {
lv_obj_t* l = label(_scan_list, _scanning ? "Repeaters and rooms in range will answer."
: "Nobody answered. Try again later or move.",
THEME_FONT_BODY, theme::TEXT_MUTED);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* l = noteLabel(_scan_list, _scanning ? "Repeaters and rooms in range will answer."
: "Nobody answered. Try again later or move.",
THEME_FONT_BODY, theme::TEXT_MUTED);
lv_obj_set_style_pad_top(l, 8, 0);
}
}
@@ -2220,7 +2283,6 @@ void UITask::openScanNode(int row) {
_node_from_map = false;
_scanning = false;
_pinging = false;
_delete_armed_ms = 0;
_screen = SCR_NODE;
buildNode();
}
@@ -2292,26 +2354,10 @@ void UITask::openNode(int row) {
_node_from_scan = false;
_node_from_map = false;
_pinging = false;
_delete_armed_ms = 0;
_screen = SCR_NODE;
buildNode();
}
static lv_obj_t* actionButton(lv_obj_t* parent, const char* text, uint8_t action, bool accent) {
lv_obj_t* b = lv_button_create(parent);
lv_obj_set_height(b, 40);
lv_obj_set_flex_grow(b, 1);
lv_obj_set_style_pad_hor(b, 4, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
lv_obj_add_event_cb(b, onNodeAction, LV_EVENT_CLICKED, (void*)(uintptr_t)action);
lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT);
lv_obj_center(l);
return l;
}
void UITask::buildNode() {
const NearbyModel::Entry& e = s_node;
lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true);
@@ -2327,12 +2373,7 @@ void UITask::buildNode() {
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);
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
lv_obj_t* acts = buttonBar(body);
bool contact = e.contact_idx >= 0;
bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM);
bool pos = e.lat_e6 != 0 || e.lon_e6 != 0;
@@ -2350,8 +2391,8 @@ void UITask::buildNode() {
for (int i = 0; i < n; i++) { // five or more: icons only, so every button fits one row
char t[24];
snprintf(t, sizeof(t), "%s%s", list[i].icon, n >= 5 ? "" : list[i].text);
lv_obj_t* l = actionButton(acts, t, list[i].action, list[i].accent);
if (list[i].action == NODE_DELETE) _node_delete_lbl = l;
lv_obj_t* b = barButton(acts, t, onNodeAction, list[i].action, list[i].accent);
if (list[i].action == NODE_DELETE) _node_delete_lbl = lv_obj_get_child(b, 0);
}
refreshNode();
@@ -2420,10 +2461,6 @@ void UITask::refreshNode() {
_pinging = false;
}
}
if (_node_delete_lbl && _delete_armed_ms && millis() - _delete_armed_ms > 3000) {
_delete_armed_ms = 0;
lv_label_set_text(_node_delete_lbl, LV_SYMBOL_TRASH);
}
}
void UITask::nodeAction(uint8_t action) {
@@ -2488,10 +2525,7 @@ void UITask::nodeAction(uint8_t action) {
pinPopup(false, 0, e.pub_key);
break;
case NODE_DELETE:
if (!_delete_armed_ms) { // destructive: second tap within 3 s confirms
_delete_armed_ms = millis();
if (_delete_armed_ms == 0) _delete_armed_ms = 1;
lv_label_set_text(_node_delete_lbl, LV_SYMBOL_TRASH "?"); // fits an icon-only button
if (!tapConfirmed(_node_delete_lbl, LV_SYMBOL_TRASH "?")) { // "?" fits an icon-only button
showToast("Tap again to delete the contact", 2500);
break;
}
@@ -3121,17 +3155,13 @@ void UITask::pruneContacts() {
showToast(_prefs && _prefs->contact_expiry_idx == 0 ? "Contact expiry is off" : "No inactive contacts");
return;
}
if (!_prune_armed_ms || millis() - _prune_armed_ms > 3000) {
_prune_armed_ms = millis() | 1;
if (_prune_lbl) lv_label_set_text_fmt(_prune_lbl, LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s");
return;
}
_prune_armed_ms = 0;
char ask[40];
snprintf(ask, sizeof(ask), LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s");
if (!tapConfirmed(_prune_lbl, ask)) return;
int removed = the_mesh.pruneStaleContacts();
char t[40];
snprintf(t, sizeof(t), "Removed %d contact%s", removed, removed == 1 ? "" : "s");
showToast(t);
if (_prune_lbl) lv_label_set_text(_prune_lbl, LV_SYMBOL_TRASH " Remove inactive contacts now");
}
void UITask::applyDisplayPrefs() {
@@ -3171,7 +3201,6 @@ void UITask::buildSchemaSettings() {
: s_nav_section == settings::SEC_LIVE_SHARE ? "Share options" : "Alert options";
lv_obj_t* body = newScreen(title, true);
_prune_lbl = nullptr;
_prune_armed_ms = 0;
switch (_settings_page) {
case PG_KEYBOARD: buildKeyboardPage(body); return;
case PG_ABOUT: buildAboutPage(body); return;
@@ -455,7 +455,6 @@ private:
lv_obj_t* _ota_btn_lbl = nullptr;
uint32_t _map_left_ms = 0; // when the map was left, for dropping live tiles' WiFi
bool _admin_from_pick = false; // Admin opened from its Home tile (back returns there)
uint32_t _prune_armed_ms = 0;
uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown
uint32_t _next_batt_ms = 0;
lv_obj_t* _prune_lbl = nullptr;
@@ -508,7 +507,6 @@ private:
uint32_t _next_nearby_ms = 0;
bool _pinging = false;
uint32_t _ping_started_ms = 0;
uint32_t _delete_armed_ms = 0;
lv_obj_t* _nearby_list = nullptr;
lv_obj_t* _nearby_status = nullptr;
lv_obj_t* _nearby_sort_lbl = nullptr;
@@ -553,7 +551,6 @@ private:
lv_obj_t* _nav_ta = nullptr;
lv_obj_t* _nav_kb = nullptr;
lv_obj_t* _nav_del_lbl = nullptr;
uint32_t _nav_del_armed_ms = 0;
int _nav_wp = -1; // waypoint the menu / rename is about
lv_obj_t* _nav_rec = nullptr; // "REC 1.2 km LIVE 58m" pill
lv_obj_t* _nav_avg_pill = nullptr; // GPS averaging progress (tap: cancel)
@@ -563,7 +560,6 @@ private:
lv_obj_t* _nav_share_lbl = nullptr;
lv_obj_t* _nav_share_btn = nullptr;
lv_obj_t* _nav_tb_btn = nullptr;
uint32_t _nav_reset_armed_ms = 0;
int32_t _nav_spot_lat = 0, _nav_spot_lon = 0; // the spot a long-press picked
char _share_text[96] = ""; // waiting for a conversation to be picked (shareToMessage)
+6 -8
View File
@@ -807,10 +807,9 @@ public:
display.drawTextCentered(display.width() / 2, mid_y + step, "Enable GPS or");
display.drawTextCentered(display.width() / 2, mid_y + step * 2, "connect app");
} else {
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
unix_ts += (int32_t)tz * 3600;
time_t t = (time_t)unix_ts;
struct tm* ti = gmtime(&t);
struct tm lt;
localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, lt);
struct tm* ti = &lt;
char buf[24];
display.setColor(DisplayDriver::LIGHT);
@@ -954,10 +953,9 @@ public:
if (unix_ts < 1000000000UL) {
display.drawTextCentered(display.width() / 2, display.height() / 2 - step, "No time sync");
} else {
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
unix_ts += (int32_t)tz * 3600;
time_t t = (time_t)unix_ts;
struct tm* ti = gmtime(&t);
struct tm lt;
localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, lt);
struct tm* ti = &lt;
char buf[12];
bool h12 = _node_prefs && _node_prefs->clock_12h;
int date_y = drawClockTime(display, 0, ti, h12, /*show_sec*/false);
+9 -15
View File
@@ -68,21 +68,9 @@ public:
expWritePin(EXP_PIN_GNSS_EN, LOW);
}
}
// microSD rail (unmount first)
void setSdPower(bool on) { expWritePin(EXP_PIN_TF_EN, on); }
// Grove expansion port rail (nothing on-board depends on it)
void setGrovePower(bool on) { expWritePin(EXP_PIN_GROVE_EN, on); }
bool gnssPowered() const { return (out_shadow[EXP_PIN_GNSS_EN >> 3] >> (EXP_PIN_GNSS_EN & 7)) & 1; }
// hardware reset pulse to the L76K GNSS (active HIGH on this board); the
// module keeps almanac/ephemeris across it, so this is a warm restart
void gnssReset() {
expWritePin(EXP_PIN_GNSS_RST, HIGH);
delay(10);
expWritePin(EXP_PIN_GNSS_RST, LOW);
}
// WAKE button on expander P00: pressed = level differs from boot baseline
bool readWakeButton();
@@ -93,9 +81,6 @@ public:
// VBUS presence via the AW35615 USB-C controller (I2C 0x22)
bool isExternalPowered() override;
int awRead(uint8_t reg); // AW35615 register, -1 on error
bool expanderOK() const { return expander_ok; }
private:
uint8_t out_shadow[2] = { 0xFF, 0xFF }; // TCA9535 output regs default high
@@ -107,6 +92,15 @@ private:
uint32_t batt_read_ms = 0; // when it was taken (reads closer together reuse it)
int expReadInputs(); // 16-bit input register pair, -1 on error
int awRead(uint8_t reg); // AW35615 register, -1 on error
// hardware reset pulse to the L76K GNSS (active HIGH on this board); the
// module keeps almanac/ephemeris across it, so this is a warm restart
void gnssReset() {
expWritePin(EXP_PIN_GNSS_RST, HIGH);
delay(10);
expWritePin(EXP_PIN_GNSS_RST, LOW);
}
bool expWriteReg(uint8_t reg, uint8_t val);
void expSetOutput(uint8_t pin, bool initial_level);