feat(ui-lvgl): quick panel from the status bar; L2 fixes from rc1

The quick panel, as a phone's: pulled down from the status bar (or a tap
on it). Bluetooth, WiFi, GPS, sound, trail and live share switch at a
tap; Advert opens the advert popup (moved here from the Nodes header);
Lock locks the screen; a brightness slider. Holding a tile opens its
settings. Lock and sun glyphs added to the fonts.

Fixes from rc1 testing on the L2:
- GPS status icon: none when GPS is off (the module kept its last fix
  valid, so it stayed green); grey on, green with a fix. ensureGps()
  turns GPS on before trusting that stale fix.
- Screen wake: the panel wakes dark, a frame is drawn, then the
  backlight fades in -- no white flash. Off fades out first.
- Power off / flat battery: the chip wakes every 30 s to look for USB
  power and starts when plugged in (it slept with no wake source before,
  until the reset button). Plugged in when switched off: stays off until
  re-plugged.
- Message ages in a conversation refresh every 2 s, as on ui-new; the
  message popup keeps its age, repeaters heard and delivery up to date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-05 09:10:06 +02:00
co-authored by Claude Opus 5.5
parent d66f04ba94
commit b5e4845dae
17 changed files with 940 additions and 333 deletions
@@ -24,6 +24,37 @@ static uint8_t s_login_key[PUB_KEY_SIZE];
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
// The message popup kept up to date (messagePopupTick): its overlay while
// open, the message (by time), its "when" line and what else it shows.
static lv_obj_t* s_msg_overlay = nullptr;
static lv_obj_t* s_msg_when = nullptr;
static uint32_t s_msg_ts = 0;
static uint32_t s_msg_state = 0;
// "14:05 - 12s ago - 2 hops": when it came and, incoming, how far.
static void whenText(char* when, size_t n, uint32_t ts, uint8_t packed, bool own, const NodePrefs* prefs) {
char age[16];
uint32_t now = rtc_clock.getCurrentTime();
geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now);
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
char date[32];
fmtDate(date, sizeof(date), ti);
snprintf(when, n, "%s %s", date, clk);
} else snprintf(when, n, "%s - %s ago", clk, age);
} else {
snprintf(when, n, "%s ago", age);
}
if (!own) { // incoming: how far it came, on the same line
uint8_t hops = contactctl::hopCount(packed);
size_t o = strlen(when);
if (hops > 0) snprintf(when + o, n - o, " - %u hop%s", hops, hops == 1 ? "" : "s");
else snprintf(when + o, n - o, " - direct");
}
}
} // namespace convview
@@ -296,28 +327,13 @@ void UITask::messageMenu(int idx) {
lv_obj_set_style_border_width(q, 2, 0);
lv_obj_set_style_border_color(q, lv_color_hex(m.own ? theme::ACCENT : theme::SURFACE_2), 0);
lv_obj_set_style_pad_left(q, 8, 0);
char when[64], age[16];
uint32_t now = rtc_clock.getCurrentTime();
geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now);
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
char date[32];
fmtDate(date, sizeof(date), ti);
snprintf(when, sizeof(when), "%s %s", date, clk);
} else snprintf(when, sizeof(when), "%s - %s ago", clk, age);
} else {
snprintf(when, sizeof(when), "%s ago", age);
}
char when[64];
whenText(when, sizeof(when), ts, packed, m.own, _prefs);
uint8_t hops = contactctl::hopCount(packed);
if (!m.own) { // incoming: how far it came, on the same line
size_t o = strlen(when);
if (hops > 0) snprintf(when + o, sizeof(when) - o, " - %u hop%s", hops, hops == 1 ? "" : "s");
else snprintf(when + o, sizeof(when) - o, " - direct");
}
label(panel, when, THEME_FONT_SMALL, theme::TEXT_MUTED);
s_msg_when = label(panel, when, THEME_FONT_SMALL, theme::TEXT_MUTED);
s_msg_overlay = _nav_overlay;
s_msg_ts = ts;
s_msg_state = packed | (uint32_t)dm_st << 8;
if (m.own && !m.channel) { // a DM we sent: its end-to-end delivery, in words
const char* st = "Sent"; uint32_t col = theme::TEXT_MUTED;
@@ -361,6 +377,35 @@ void UITask::messageMenu(int idx) {
if (m.loc >= 0) barButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false);
}
// The open message popup, every half second on a conversation: the age in
// its "when" line, and opened anew when the repeaters heard passing it on or
// its delivery change. Found again by its time (the page may have moved).
void UITask::messagePopupTick() {
using namespace convview;
if (!_nav_overlay || _nav_overlay != s_msg_overlay) return;
const MessageHistory& h = _core->history;
for (int i = 0; i < s_msg_meta_n; i++) {
const MsgMeta& m = s_msg_meta[i];
if (m.pos < 0) continue;
uint32_t ts, state;
uint8_t packed;
if (m.channel) {
const ChHistEntry& e = s_th_ch[m.pos];
ts = e.timestamp; packed = e.path_len; state = packed | (uint32_t)ACK_NONE << 8;
} else {
const DmHistEntry& e = s_th_dm[m.pos];
ts = e.timestamp; packed = e.path_len;
state = packed | (uint32_t)(e.outgoing ? h.dmEffectiveStatus(e) : ACK_NONE) << 8;
}
if (ts != s_msg_ts) continue;
if (state != s_msg_state) { messageMenu(i); return; }
char when[64];
whenText(when, sizeof(when), ts, packed, m.own, _prefs);
setText(s_msg_when, when);
return;
}
}
void UITask::messageAction(uint8_t act) {
using namespace convview;
if (s_msg < 0 || s_msg >= s_msg_meta_n) { navClosePopup(); return; }