perf(ui-lvgl): lower clock when idle, fewer redraws, wake on BLE frames

- CPU at 80 MHz with the screen on after 2 s without touch or animation;
  a touch restores the full clock before its frame is drawn
- setText(): periodic refreshes no longer redraw unchanged labels
  (status bar, Home, lock, Diag, GPS, compass, clock, map, nav, nearby)
- map markers kept across the 3 s rebuild when unchanged
- app connected: the loop blocks until a BLE frame arrives
  (serialFrameArrived / hasPendingFrames) instead of polling every 2 ms
- PSRAM: boot frame sprite and the HTTP response buffer released

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-29 11:51:18 +02:00
co-authored by Claude Opus 5.5
parent bc266a4f5c
commit 4ab9d949ec
19 changed files with 188 additions and 82 deletions
+46 -14
View File
@@ -133,6 +133,23 @@ 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 label's text, set only when it differs: LVGL redraws a label on every
// set_text, the same text or not, and the refreshes that run every second
// mostly write back what's already there.
static void setText(lv_obj_t* l, const char* text) {
const char* cur = lv_label_get_text(l);
if (cur && strcmp(cur, text) == 0) return;
lv_label_set_text(l, text);
}
static void setTextFmt(lv_obj_t* l, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
static void setTextFmt(lv_obj_t* l, const char* fmt, ...) {
char b[160];
va_list ap;
va_start(ap, fmt);
vsnprintf(b, sizeof(b), fmt, ap);
va_end(ap);
setText(l, b);
}
// 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) {
@@ -296,7 +313,7 @@ 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);
setText(value, text);
}
// A bare flex row / column sized to its content, taps passing through.
@@ -828,9 +845,9 @@ static void clockFaceSet(lv_obj_t* f, const struct tm* ti, const NodePrefs* p) {
lv_obj_t* ap = lv_obj_get_child(f, 1);
char clk[12] = "--:--";
if (ti) fmtClock(clk, sizeof(clk), *ti, p, false, true);
lv_label_set_text(lv_obj_get_child(f, 0), clk);
setText(lv_obj_get_child(f, 0), clk);
bool h12 = ti && p && p->clock_12h;
if (h12) lv_label_set_text(ap, ti->tm_hour < 12 ? "AM" : "PM");
if (h12) setText(ap, ti->tm_hour < 12 ? "AM" : "PM");
if (h12 != !lv_obj_has_flag(ap, LV_OBJ_FLAG_HIDDEN)) {
if (h12) lv_obj_remove_flag(ap, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(ap, LV_OBJ_FLAG_HIDDEN);
@@ -1110,6 +1127,7 @@ void UITask::loop() {
uint32_t sig = threadSignature();
if (_thread_dirty || sig != _thread_sig) refreshThread();
}
lvport::cpuSlow(!cpuNeeded());
lv_next = lv_timer_handler();
}
lvport::idle(idleMillis(lv_next));
@@ -1117,13 +1135,14 @@ void UITask::loop() {
// How long loop() may sleep: nothing is due before then. Not at all while the
// mesh has packets queued; briefly while a melody plays (its notes are timed
// here) or the app is connected (its frames are read one a pass).
// here) or frames to or from the app wait (read one a pass, sent spaced out).
// A frame arriving from the app over BLE wakes the loop (serialFrameArrived).
uint32_t UITask::idleMillis(uint32_t lv_next) {
if (the_mesh.hasPendingWork()) return 0;
#ifdef PIN_BUZZER
if (_buzzer.isPlaying()) return 1;
#endif
if (isClientConnected()) return 2;
if (isClientConnected() && _serial->hasPendingFrames()) return 2;
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
}
@@ -1429,9 +1448,9 @@ void UITask::refreshStatusBar() {
if (localTime(_prefs, ti)) {
char clk[12];
fmtClock(clk, sizeof(clk), ti, _prefs, true);
lv_label_set_text(_status_time, clk);
setText(_status_time, clk);
} else {
lv_label_set_text(_status_time, "--:--");
setText(_status_time, "--:--");
}
// Battery: the icon, then % or volts per Settings > Battery display. The
@@ -1446,7 +1465,7 @@ void UITask::refreshStatusBar() {
case battery::VOLTAGE: snprintf(level, sizeof(level), " %u.%02u V", mv / 1000, (mv % 1000) / 10); break;
default: break;
}
lv_label_set_text_fmt(_status_batt, "%s%s", batt, level);
setTextFmt(_status_batt, "%s%s", batt, level);
lv_obj_t* left_of = _status_batt; // the icons pack up to this
if (_board->isExternalPowered()) {
lv_obj_remove_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
@@ -2246,8 +2265,8 @@ void UITask::refreshScanPopup() {
if (!_scan_list) return;
_scan->refreshScan();
int n = _scan->count();
if (_scanning) lv_label_set_text_fmt(_scan_status, LV_SYMBOL_REFRESH " Listening for replies... %d", n);
else lv_label_set_text_fmt(_scan_status, "%d node%s answered the discover request", n, n == 1 ? "" : "s");
if (_scanning) setTextFmt(_scan_status, LV_SYMBOL_REFRESH " Listening for replies... %d", n);
else setTextFmt(_scan_status, "%d node%s answered the discover request", n, n == 1 ? "" : "s");
uint32_t sig = (uint32_t)n + (_scanning ? 0x10000u : 0);
for (int i = 0; i < n; i++) {
@@ -2309,10 +2328,10 @@ void UITask::refreshNearbyList() {
int n = _nearby->count();
if (_nearby_sort_lbl)
lv_label_set_text(_nearby_sort_lbl, _nearby->sortMode() == NearbyModel::SORT_TIME ? "Recent" : "Dist");
setText(_nearby_sort_lbl, _nearby->sortMode() == NearbyModel::SORT_TIME ? "Recent" : "Dist");
int32_t lat, lon;
bool gps = _nearby->ownPosition(lat, lon);
lv_label_set_text_fmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s",
setTextFmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s",
gps ? "" : " - no GPS fix, distances unknown");
if (sig == _nearby_sig) return;
_nearby_sig = sig;
@@ -2455,12 +2474,12 @@ void UITask::refreshNode() {
int16_t out = 0, back = 0; uint32_t rtt = 0;
_core->ping.getResult(out, back, rtt);
if (!_core->ping.isActive() && (out || back || rtt)) {
lv_label_set_text_fmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt,
setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt,
out / 4.0f, back / 4.0f);
_pinging = false;
} else if (millis() - _ping_started_ms > 3000) {
_core->ping.clear();
lv_label_set_text(_node_ping, "Ping: no reply");
setText(_node_ping, "Ping: no reply");
_pinging = false;
}
}
@@ -3459,6 +3478,19 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
#include "StorageScreen.h"
#include "Splash.h"
// The screen on, the full CPU clock while it's worth having: touched in the
// last CPU_IDLE_MS, something moving, map tiles to decode, a download or an
// update on WiFi. Otherwise 80 MHz (lvport::cpuSlow) -- a clock ticking over,
// a message being read.
static constexpr uint32_t CPU_IDLE_MS = 2000;
bool UITask::cpuNeeded() {
if (lv_display_get_inactive_time(NULL) < CPU_IDLE_MS || lv_anim_count_running() > 0) return true;
if (wifiInUse() || mapview::s_dl.liveQueued() > 0) return true;
if (_screen == SCR_MAP && _map_pending) return true;
if (_screen == SCR_HOME && home::mini::s_area && home::mini::s_pending) return true;
return false;
}
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
// Sim tests: straight to a screen by name.
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {