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
@@ -226,16 +226,16 @@ void UITask::refreshClock() {
if (s_big) {
char b[16];
fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs());
lv_label_set_text(s_big, b);
setText(s_big, b);
}
if (s_go_lbl) lv_label_set_text(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
if (s_go_lbl) setText(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
} else if (s_tab == TAB_STOPWATCH) {
if (s_big) {
char b[16];
fmtStopwatch(b, sizeof(b), swElapsed());
lv_label_set_text(s_big, b);
setText(s_big, b);
}
if (s_go_lbl) lv_label_set_text(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start");
if (s_go_lbl) setText(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start");
}
}
@@ -127,14 +127,14 @@ void UITask::refreshCompass() {
lv_obj_invalidate(s_dial);
}
if (have) {
lv_label_set_text_fmt(s_deg, "%03d", cog);
lv_label_set_text(s_degsign, "\xC2\xB0");
lv_label_set_text(s_card, geo::bearingCardinal(cog));
lv_label_set_text(s_hint, "Course over ground");
setTextFmt(s_deg, "%03d", cog);
setText(s_degsign, "\xC2\xB0");
setText(s_card, geo::bearingCardinal(cog));
setText(s_hint, "Course over ground");
} else {
lv_label_set_text(s_deg, "");
lv_label_set_text(s_degsign, "");
lv_label_set_text(s_card, "--");
lv_label_set_text(s_hint, hint);
setText(s_deg, "");
setText(s_degsign, "");
setText(s_card, "--");
setText(s_hint, hint);
}
}
@@ -288,21 +288,21 @@ void UITask::refreshLock() {
clockFaceSet(s_lock_clock, &ti, _prefs);
char date[48];
fmtDate(date, sizeof(date), ti);
if (s_lock_date) lv_label_set_text(s_lock_date, date);
if (s_lock_date) setText(s_lock_date, date);
}
int unread = unreadTotal();
if (!s_lock_date) { // PIN card: the count next to the clock, the keypad's line
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
else lv_label_set_text(s_lock_unread, "");
if (unread > 0) setTextFmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
else setText(s_lock_unread, "");
if (!s_pin_msg) return;
bool was = _pin_tries.block_until != 0;
if (_pin_tries.blocked()) lv_label_set_text_fmt(s_pin_msg, "Too many tries - wait %lu s", (unsigned long)_pin_tries.secondsLeft());
else if (was) lv_label_set_text(s_pin_msg, "Enter PIN");
else if (!lv_label_get_text(s_pin_msg)[0]) lv_label_set_text(s_pin_msg, "Enter PIN");
if (_pin_tries.blocked()) setTextFmt(s_pin_msg, "Too many tries - wait %lu s", (unsigned long)_pin_tries.secondsLeft());
else if (was) setText(s_pin_msg, "Enter PIN");
else if (!lv_label_get_text(s_pin_msg)[0]) setText(s_pin_msg, "Enter PIN");
return;
}
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d new message%s", unread, unread == 1 ? "" : "s");
else lv_label_set_text(s_lock_unread, "");
if (unread > 0) setTextFmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d new message%s", unread, unread == 1 ? "" : "s");
else setText(s_lock_unread, "");
}
// ── Screen PIN ────────────────────────────────────────────────────────────────
@@ -91,13 +91,13 @@ static void setLive(int i, const diag::Row& r) {
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 : "");
setText(lv.in, a);
setText(lv.out, slash ? slash + 1 : "");
return;
}
char v[40];
showValue(r.label, r.value, v, sizeof(v));
lv_label_set_text(lv.val, v);
setText(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);
}
+7 -7
View File
@@ -324,13 +324,13 @@ void UITask::refreshGps() {
char pos[32] = "", alt[16] = "", speed[16] = "", dop[32] = "";
bool fix = false;
if (!g) {
lv_label_set_text(s_status, "Not available");
setText(s_status, "Not available");
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
} else if (off) {
lv_label_set_text(s_status, "GPS is off");
setText(s_status, "GPS is off");
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
} else if (!data) {
lv_label_set_text(s_status, "No data");
setText(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(used, sizeof(used), "Nothing from the receiver (%lu B)", (unsigned long)gps.rxChars());
@@ -342,10 +342,10 @@ void UITask::refreshGps() {
for (int i = 0; i < s_n; i++) { if (s_sats[i].snr >= 0) tracked++; if (s_used_flag[i]) used_n++; }
fix = g->hasFix();
if (fix) {
lv_label_set_text(s_status, g->fix_mode == 2 ? "2D fix" : "3D fix");
setText(s_status, g->fix_mode == 2 ? "2D fix" : "3D fix");
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::OK), 0);
} else {
lv_label_set_text(s_status, "Searching");
setText(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", fix ? (used_n ? used_n : g->sats_used) : 0, s_n);
@@ -362,7 +362,7 @@ void UITask::refreshGps() {
}
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);
setText(s_used, used);
infoSet(s_inview, inview);
infoSet(s_search, search);
infoSet(s_ttff, ttff);
@@ -378,7 +378,7 @@ void UITask::refreshGps() {
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);
setTextFmt(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);
}
@@ -729,7 +729,7 @@ void UITask::homeMapLayout() {
}
const char* hint = !mapview::s_available ? "No map on the SD card"
: !shown && !s_pending ? "No map tiles for this area" : nullptr;
if (hint) { lv_label_set_text(s_hint, hint); lv_obj_remove_flag(s_hint, LV_OBJ_FLAG_HIDDEN); }
if (hint) { setText(s_hint, hint); lv_obj_remove_flag(s_hint, LV_OBJ_FLAG_HIDDEN); }
else lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN);
}
@@ -786,14 +786,14 @@ void UITask::refreshHome() {
if (known) {
char date[48];
fmtDate(date, sizeof(date), ti);
lv_label_set_text(_home_date, date);
setText(_home_date, date);
} else {
lv_label_set_text(_home_date, "time not synced");
setText(_home_date, "time not synced");
}
for (int i = 0; i < FIELDS; i++) {
if (!s_field_val[i] || !_prefs) continue;
char v[24];
homeFieldText(_prefs->dashboard_fields[i], v, sizeof(v));
lv_label_set_text(s_field_val[i], v);
setText(s_field_val[i], v);
}
}
+44 -7
View File
@@ -101,6 +101,19 @@ static lgfx::LGFX_Device* s_gfx = nullptr;
static bool s_swallow = false; // ignore the touch that woke the display until it lifts
static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb
// The CPU clock down to 80 MHz (LoRa, BLE and WiFi all run at that) with the
// screen off (powerSave()), and with it on while nobody touches it and nothing
// moves (the loop decides); a touch brings the full clock back before the
// frame it causes is drawn.
static uint32_t s_full_mhz = 0;
static bool s_cpu_slow = false;
static void cpuSlow(bool on) {
if (on == s_cpu_slow) return;
if (!s_full_mhz) s_full_mhz = getCpuFrequencyMhz();
s_cpu_slow = on;
setCpuFrequencyMhz(on ? 80 : s_full_mhz);
}
static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
#ifdef UI_PERF_TEST
uint32_t t = micros();
@@ -128,6 +141,7 @@ static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
return;
}
if (down) {
cpuSlow(false); // the frame this touch causes: at the full clock
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = tp.x;
data->point.y = tp.y;
@@ -139,6 +153,7 @@ static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
static bool begin() {
s_gfx = display.lgfxDevice();
s_gfx->setColorDepth(16);
display.releaseFrameBuffer();
lv_display_t* disp = lv_display_create(s_gfx->width(), s_gfx->height());
lv_display_set_flush_cb(disp, flushCb);
@@ -169,19 +184,22 @@ static bool touched() {
static void swallowTouch() { s_swallow = true; }
// ── Power ────────────────────────────────────────────────────────────────────
// Screen off: the CPU clock down (LoRa, BLE and WiFi all run at 80 MHz) and
// the touch panel asleep unless a tap is to wake the screen. Screen on: back.
// Screen off: the clock down and the touch panel asleep unless a tap is to
// wake the screen. Screen on: back.
static bool s_touch_asleep = false;
static void powerSave(bool on, bool keep_touch) {
static uint32_t full_mhz = 0;
if (!full_mhz) full_mhz = getCpuFrequencyMhz();
setCpuFrequencyMhz(on ? 80 : full_mhz);
cpuSlow(on);
if (on && !keep_touch) { board.touchSleep(); s_touch_asleep = true; }
else if (!on && s_touch_asleep) { board.touchWake(); s_touch_asleep = false; }
}
// Nothing due for `ms`: the loop task blocks and the core idles, clock-gated,
// instead of spinning.
static void idle(uint32_t ms) { if (ms) vTaskDelay(pdMS_TO_TICKS(ms)); }
// instead of spinning -- woken early by a frame from the app (below).
static TaskHandle_t s_loop_task = nullptr;
static void idle(uint32_t ms) {
if (!ms) return;
if (!s_loop_task) s_loop_task = xTaskGetCurrentTaskHandle();
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms));
}
// Backlight 1-100 % (the brightness slider), on the LP5814 PWM.
static void setBacklightPct(uint8_t pct) {
@@ -496,10 +514,12 @@ static int netState() {
default: return NET_CONNECTING;
}
}
static void fetchFreeBuffer();
static void netEnd() {
if (s_net_scanning) { s_net_scanning = false; WiFi.scanDelete(); }
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
fetchFreeBuffer();
}
// The network joined (Settings > WiFi marks it), "" when not connected.
static void netSsid(char* out, size_t n) {
@@ -651,6 +671,14 @@ static void fetchAbandon() {
else fetchRelease();
}
static const char* fetchError() { return s_ferr; }
// WiFi off: the response buffer (grown to the biggest tile or update chunk,
// up to 512 KB of PSRAM) given back, unless a request is still finishing.
static void fetchFreeBuffer() {
if (s_fstate != F_IDLE) return;
heap_caps_free(s_fbuf);
s_fbuf = nullptr;
s_fcap = s_flen = 0;
}
#elif defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
// Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to
@@ -719,6 +747,7 @@ static bool begin() {
static bool touched() { return SimLcdDisplay::touchState().down; }
static void swallowTouch() { s_swallow = true; }
static void powerSave(bool, bool) {} // the browser's page loop runs the sim
static void cpuSlow(bool) {}
static void idle(uint32_t) {}
static void setBacklightPct(uint8_t pct) { (void)pct; } // the browser canvas has no backlight
@@ -870,3 +899,11 @@ static void loadHomeApps(char* out, size_t n) { nvs::getStr("mc_ui", "apps", out
static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); }
} // namespace lvport
#if defined(SEEED_WIO_TRACKER_L2)
// A frame from the app (SerialBLEInterface, the BLE stack's task): the loop,
// blocked in lvport::idle(), reads it now rather than at the next timeout.
void serialFrameArrived() {
if (TaskHandle_t t = lvport::s_loop_task) xTaskNotifyGive(t);
}
#endif
+31 -15
View File
@@ -116,11 +116,12 @@ static void drawGrid(lv_event_t* e) {
// Markers, remembered so layout can reposition them. `idx` is the Nearby row
// (MK_NODE), the waypoint index (MK_WAYPOINT) or the live-share slot (MK_LIVE).
enum : uint8_t { MK_NODE, MK_WAYPOINT, MK_LIVE };
struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; lv_obj_t* obj; lv_obj_t* dot; };
struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; uint32_t col, text_hash; lv_obj_t* obj; lv_obj_t* dot; };
static const int MAX_MARKS = NearbyModel::MAX_NEARBY > WaypointStore::CAPACITY + LiveTrackStore::CAPACITY
? NearbyModel::MAX_NEARBY : WaypointStore::CAPACITY + LiveTrackStore::CAPACITY; // Nearby map, or waypoints + live shares
static Mark* s_marks = psramBuf<Mark>(MAX_MARKS);
static int s_mark_count = 0;
static int s_mark_prev = 0; // during a rebuild: how many the last one left, reused where unchanged
static int s_drag = 0; // px moved in the current press: a drag isn't a tap
static bool s_available = false; // provider has data; checked when the map opens, not per frame
static double s_left = 0, s_top = 0; // world px (at the current zoom) of the view's top-left
@@ -419,7 +420,7 @@ void UITask::layoutMap() {
double lat = atan(sinh(M_PI * (1 - 2 * _map_cy / (double)(1 << _map_z)))) * 180.0 / M_PI;
mapview::s_grid = { left, top, mapview::gridStep(_map_z, lat, _prefs && _prefs->units_imperial, 56, sl, sizeof(sl)) };
if (mapview::s_scale_lbl) {
lv_label_set_text(mapview::s_scale_lbl, sl);
setText(mapview::s_scale_lbl, sl);
lv_obj_set_width(mapview::s_scale_bar, (int)lround(mapview::s_grid.step_px));
}
lv_obj_invalidate(_map_area);
@@ -494,20 +495,20 @@ void UITask::layoutMap() {
}
if (_map_nav) layoutNav();
if (over) lv_label_set_text_fmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
if (over) setTextFmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
else if (mapview::s_provider == &mapview::s_vector) // spike: how long a tile takes
{
uint32_t ld, fl, ln;
mapview::s_vector.lastSplit(ld, fl, ln);
lv_label_set_text_fmt(_map_zoom_lbl, "z%d vt %lu ms (r%lu a%lu l%lu)", _map_z, (unsigned long)mapview::s_vector.lastMs(),
setTextFmt(_map_zoom_lbl, "z%d vt %lu ms (r%lu a%lu l%lu)", _map_z, (unsigned long)mapview::s_vector.lastMs(),
(unsigned long)ld, (unsigned long)fl, (unsigned long)ln);
}
else lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z);
else setTextFmt(_map_zoom_lbl, "z%d", _map_z);
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
: (!_map_pending && shown == 0 && missing > 0)
? "No map here at this zoom -\nzoom out or download the area" : nullptr;
if (hint && !areaSelecting()) { lv_label_set_text(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
if (hint && !areaSelecting()) { setText(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
else lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN);
areaLayout();
}
@@ -585,10 +586,12 @@ void UITask::mapLoop() {
void UITask::rebuildMapMarkers() {
if (!_map_marks) return;
for (int k = 0; k < mapview::s_mark_count; k++) lv_obj_delete(mapview::s_marks[k].obj);
mapview::s_mark_prev = mapview::s_mark_count;
mapview::s_mark_count = 0;
if (_map_nav) rebuildNavMarkers();
else rebuildNodeMarkers();
for (int k = mapview::s_mark_count; k < mapview::s_mark_prev; k++) lv_obj_delete(mapview::s_marks[k].obj);
mapview::s_mark_prev = 0;
lv_obj_update_layout(_map_marks); // so layoutMap() knows where each dot sits in its marker
}
@@ -604,9 +607,22 @@ void UITask::rebuildNodeMarkers() {
// One clickable object per marker: a dot plus the name beside it. The object
// is placed so the dot's centre is on the spot, from where layout actually put
// the dot inside it (Mark::dot) -- the name may be taller than the dot.
// the dot inside it (Mark::dot) -- the name may be taller than the dot. The
// markers are rebuilt every few seconds: one the same as the last time in its
// slot is kept as it is, so an unchanged map isn't redrawn under it.
void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text) {
if (!_map_marks || mapview::s_mark_count >= mapview::MAX_MARKS) return;
uint32_t h = 2166136261u; // FNV-1a of the name
for (const char* c = text; *c; c++) h = (h ^ (uint8_t)*c) * 16777619u;
int k = mapview::s_mark_count;
if (k < mapview::s_mark_prev) {
mapview::Mark& old = mapview::s_marks[k];
if (old.kind == kind && old.idx == idx && old.lat_e6 == lat_e6 && old.lon_e6 == lon_e6 && old.col == col && old.text_hash == h) {
mapview::s_mark_count++;
return;
}
lv_obj_delete(old.obj);
}
lv_obj_t* m = lv_obj_create(_map_marks);
lv_obj_remove_style_all(m);
lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
@@ -628,7 +644,7 @@ void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, u
lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE);
mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++];
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.obj = m; mk.dot = dot;
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.col = col; mk.text_hash = h; mk.obj = m; mk.dot = dot;
}
void UITask::mapPan(int dx, int dy) {
@@ -968,24 +984,24 @@ void UITask::mapDownloadTick() {
next_ui = millis() + 500;
if (_map_dl_pill) {
if (dl.state() == mapview::TileDownloader::CONNECTING) {
lv_label_set_text(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
setText(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (dl.active()) {
if (dl.failed())
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu " LV_SYMBOL_WARNING " %lu",
setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu " LV_SYMBOL_WARNING " %lu",
(unsigned long)dl.processed(), (unsigned long)dl.total(), (unsigned long)dl.failed());
else
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(),
setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(),
(unsigned long)dl.total());
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (dl.liveConnecting()) {
lv_label_set_text(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
setText(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (dl.liveQueued() > 0) {
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " live %d", dl.liveQueued());
setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " live %d", dl.liveQueued());
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else if (mapview::s_available && dl.savedJob(s_job_tmp)) {
lv_label_set_text(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?");
setText(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?");
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
+8 -8
View File
@@ -290,7 +290,7 @@ void UITask::refreshNavBar() {
navmap::fmtDuration(left, sizeof(left), _core->live_share.remainingSecs());
o += snprintf(t + o, sizeof(t) - o, "%sLIVE %s", o ? " " : "", left);
}
lv_label_set_text(_nav_rec, t);
setText(_nav_rec, t);
if (o) lv_obj_remove_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN);
}
int32_t tlat, tlon;
@@ -298,23 +298,23 @@ void UITask::refreshNavBar() {
bool person = false, set = false;
bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set);
if (!set) {
lv_label_set_text(_nav_title, "No target");
setText(_nav_title, "No target");
lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::TEXT_MUTED), 0);
lv_label_set_text(_nav_info, "Tap here to pick a target, or hold the map on a spot");
setText(_nav_info, "Tap here to pick a target, or hold the map on a spot");
lv_obj_add_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
return;
}
lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::ACCENT), 0);
lv_obj_remove_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
lv_label_set_text_fmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG,
setTextFmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG,
name);
if (!pos) {
lv_label_set_text(_nav_info, "Position unknown (not shared recently)");
setText(_nav_info, "Position unknown (not shared recently)");
return;
}
int32_t lat, lon;
if (!_core->course.currentLocation(lat, lon)) {
lv_label_set_text(_nav_info, _core->gpsEnabled() || !_core->gpsAvailable()
setText(_nav_info, _core->gpsEnabled() || !_core->gpsAvailable()
? "Waiting for a GPS fix..." : "GPS is off - tap the crosshair to turn it on");
return;
}
@@ -327,9 +327,9 @@ void UITask::refreshNavBar() {
int cog;
if (_core->course.currentCourse(cog)) snprintf(hdg, sizeof(hdg), " - you %d\xC2\xB0", cog);
if (navmap::s_eta.eta(km, eta, sizeof(eta)))
lv_label_set_text_fmt(_nav_info, "%s %d\xC2\xB0 %s%s - ETA %s", dist, to, geo::bearingCardinal(to), hdg, eta);
setTextFmt(_nav_info, "%s %d\xC2\xB0 %s%s - ETA %s", dist, to, geo::bearingCardinal(to), hdg, eta);
else
lv_label_set_text_fmt(_nav_info, "%s %d\xC2\xB0 %s%s", dist, to, geo::bearingCardinal(to), hdg);
setTextFmt(_nav_info, "%s %d\xC2\xB0 %s%s", dist, to, geo::bearingCardinal(to), hdg);
}
// ── Target selection ──────────────────────────────────────────────────────────
@@ -350,7 +350,7 @@ void UITask::refreshMelody() {
lightNote(s_play_all ? melodyNote() : -1);
if (!s_play_lbl || playing == s_playing_shown) return;
s_playing_shown = playing;
lv_label_set_text(s_play_lbl, playing ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Play");
setText(s_play_lbl, playing ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Play");
}
void UITask::melodySave() {
@@ -313,7 +313,7 @@ void UITask::pollStorage() {
if (s_sd_bar && !s_sd_read) {
s_sd_read = true;
s_sd_ok = lvport::sdInfo(s_sd_total, s_sd_used, s_sd_card);
if (!s_sd_ok) lv_label_set_text(s_sd_lbl, "Can't read the card");
if (!s_sd_ok) setText(s_sd_lbl, "Can't read the card");
else if (s_sd_card > s_sd_total + s_sd_total / 4) { // most of the card sits outside the partition
char c[16], t[16], line[160];
fmtBytes(c, sizeof(c), s_sd_card);
@@ -337,7 +337,7 @@ void UITask::pollStorage() {
fmtBytes(b, sizeof(b), mapview::s_live_cache.bytes());
if (mapview::s_live_cache.clearing()) snprintf(t, sizeof(t), "Deleting... %lu left", (unsigned long)mapview::s_live_cache.count());
else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b);
lv_label_set_text(s_live_info, t);
setText(s_live_info, t);
}
}
+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) {
@@ -419,6 +419,7 @@ private:
bool _wake_down = false;
uint32_t autoOffMillis() const;
uint32_t idleMillis(uint32_t lv_next);
bool cpuNeeded();
void checkLowBattery();
#ifdef PIN_BUZZER
@@ -269,7 +269,7 @@ void UITask::pollWifiScan() {
lv_obj_center(label(b, text, THEME_FONT_SMALL, known ? theme::ACCENT : theme::TEXT));
}
if (n > 0) lv_obj_remove_flag(_wifi_list, LV_OBJ_FLAG_HIDDEN);
lv_label_set_text(_wifi_status, n > 0 ? "Tap a network, then enter its password." : "No networks found.");
setText(_wifi_status, n > 0 ? "Tap a network, then enter its password." : "No networks found.");
if (!wifiInUse()) lvport::netEnd(); // the scan switched the radio on
}