From 4ab9d949ec34639615cb27b6f2bc804da04b5ae0 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:51:18 +0200 Subject: [PATCH] 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 --- .../companion_radio/ui-lvgl/ClockScreen.h | 8 +-- .../companion_radio/ui-lvgl/CompassScreen.h | 16 ++--- .../companion_radio/ui-lvgl/DeviceScreen.h | 16 ++--- examples/companion_radio/ui-lvgl/DiagScreen.h | 6 +- examples/companion_radio/ui-lvgl/GpsScreen.h | 14 ++--- examples/companion_radio/ui-lvgl/HomeScreen.h | 8 +-- examples/companion_radio/ui-lvgl/LvglPort.h | 51 +++++++++++++--- examples/companion_radio/ui-lvgl/MapScreen.h | 46 +++++++++----- examples/companion_radio/ui-lvgl/NavMap.h | 16 ++--- .../companion_radio/ui-lvgl/SoundScreen.h | 2 +- .../companion_radio/ui-lvgl/StorageScreen.h | 4 +- examples/companion_radio/ui-lvgl/UITask.cpp | 60 ++++++++++++++----- examples/companion_radio/ui-lvgl/UITask.h | 1 + examples/companion_radio/ui-lvgl/WifiScreen.h | 2 +- src/helpers/BaseSerialInterface.h | 4 ++ src/helpers/esp32/DualSerialInterface.h | 5 ++ src/helpers/esp32/SerialBLEInterface.cpp | 3 + src/helpers/esp32/SerialBLEInterface.h | 5 ++ variants/wio-tracker-l2/WioTrackerL2Display.h | 3 + 19 files changed, 188 insertions(+), 82 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/ClockScreen.h b/examples/companion_radio/ui-lvgl/ClockScreen.h index 72c24dd2..a0612220 100644 --- a/examples/companion_radio/ui-lvgl/ClockScreen.h +++ b/examples/companion_radio/ui-lvgl/ClockScreen.h @@ -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"); } } diff --git a/examples/companion_radio/ui-lvgl/CompassScreen.h b/examples/companion_radio/ui-lvgl/CompassScreen.h index e88c1e3a..b94dbd88 100644 --- a/examples/companion_radio/ui-lvgl/CompassScreen.h +++ b/examples/companion_radio/ui-lvgl/CompassScreen.h @@ -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); } } diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index 24b0b6a2..5df20e1c 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -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 ──────────────────────────────────────────────────────────────── diff --git a/examples/companion_radio/ui-lvgl/DiagScreen.h b/examples/companion_radio/ui-lvgl/DiagScreen.h index d9b8c728..10cb2b0b 100644 --- a/examples/companion_radio/ui-lvgl/DiagScreen.h +++ b/examples/companion_radio/ui-lvgl/DiagScreen.h @@ -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); } diff --git a/examples/companion_radio/ui-lvgl/GpsScreen.h b/examples/companion_radio/ui-lvgl/GpsScreen.h index 2f829076..9dc49cf5 100644 --- a/examples/companion_radio/ui-lvgl/GpsScreen.h +++ b/examples/companion_radio/ui-lvgl/GpsScreen.h @@ -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); } diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 6eb00afe..5d2c4e0c 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -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); } } diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index afe0e741..ccdc2909 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -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 diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 2035d0c6..d3d5df39 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -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(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); diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index 211aab2e..976b9954 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -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 ────────────────────────────────────────────────────────── diff --git a/examples/companion_radio/ui-lvgl/SoundScreen.h b/examples/companion_radio/ui-lvgl/SoundScreen.h index 4f494393..62ea5e9a 100644 --- a/examples/companion_radio/ui-lvgl/SoundScreen.h +++ b/examples/companion_radio/ui-lvgl/SoundScreen.h @@ -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() { diff --git a/examples/companion_radio/ui-lvgl/StorageScreen.h b/examples/companion_radio/ui-lvgl/StorageScreen.h index e67b03f5..3420fe4c 100644 --- a/examples/companion_radio/ui-lvgl/StorageScreen.h +++ b/examples/companion_radio/ui-lvgl/StorageScreen.h @@ -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); } } diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 2d78d28c..9c921697 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -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) { diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index a1478971..341d7cbe 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -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 diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index 73d98249..cdd6724f 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -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 } diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index 266fbf2c..d1e7a685 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -25,6 +25,10 @@ public: // isConnected() so single-transport interfaces are unchanged. virtual bool isClientConnected() const { return isConnected(); } + // For a loop that sleeps between passes: frames waiting to be read or sent. + // An interface that can't tell says true (poll it often). + virtual bool hasPendingFrames() const { return true; } + virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/esp32/DualSerialInterface.h b/src/helpers/esp32/DualSerialInterface.h index 5f6f17b2..76f44be4 100644 --- a/src/helpers/esp32/DualSerialInterface.h +++ b/src/helpers/esp32/DualSerialInterface.h @@ -52,6 +52,11 @@ public: bool isClientConnected() const override { return isBLEConnected(); } #endif + // USB can't tell without reading it. + bool hasPendingFrames() const override { + return (_ble_enabled && _ble.isConnected()) ? _ble.hasPendingFrames() : true; + } + bool isWriteBusy() const override { return (_ble_enabled && _ble.isConnected()) ? _ble.isWriteBusy() : _usb.isWriteBusy(); } diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index d0dcc92a..75f70f7f 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -10,6 +10,8 @@ #define ADVERT_RESTART_DELAY 1000 // millis +__attribute__((weak)) void serialFrameArrived() {} + // As the nRF52 interface: 20-30 ms for the first 30 s (the app finds it at // once), then 152.5 ms. Left alone, the library advertises every 30-60 ms // for as long as nothing is connected. Units of 0.625 ms. @@ -187,6 +189,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } + serialFrameArrived(); } } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 6032eadc..fcbe3c80 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -8,6 +8,10 @@ #include #include +// A frame from the app was queued (the BLE stack's task). Weak, empty by +// default: a firmware whose loop blocks between passes defines it to wake it. +void serialFrameArrived(); + class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; BLEService *pService; @@ -102,6 +106,7 @@ public: bool isConnected() const override; + bool hasPendingFrames() const override { return send_queue_len > 0 || uxQueueMessagesWaiting(recv_queue) > 0; } bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/variants/wio-tracker-l2/WioTrackerL2Display.h b/variants/wio-tracker-l2/WioTrackerL2Display.h index 63cc40ee..df2617f8 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Display.h +++ b/variants/wio-tracker-l2/WioTrackerL2Display.h @@ -182,6 +182,9 @@ public: // direct access to the LGFX device (LVGL flush/touch glue) lgfx::LGFX_Device* lgfxDevice() { return &disp; } + // LVGL draws straight to the panel: the frame sprite (76 KB of PSRAM) only + // carried the boot "Loading..." -- given back once LVGL takes over. + void releaseFrameBuffer() { buffer.deleteSprite(); } // reliable touch read in logical (UI_ZOOM-scaled) coords; returns false when // not touched (LGFXDisplay::getTouch reads an uninitialized point on release)