From 23caa11f517013efca806695424a5b4de65bc7cd Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:12:08 +0200 Subject: [PATCH] feat(ui-lvgl): power saving, review fixes, shared helpers Power: the UI loop sleeps until something is due (never with packets queued); screen off puts the CPU at 80 MHz, the GT911 to sleep (unless a tap wakes the screen) and the LP5814 in standby. GPS off now cuts the L76K's rail and UART on the L2 (L2GpsProvider). The battery divider is powered only for a reading (measured: settles at once); readings within 2 s are reused. The SD mount is retried at most every 5 s. Fixes: WiFi left on after leaving a scan, live tiles' WiFi kept with the screen off on the map, an ADC read every second for the status bar, touch polled on every pass while dark, a scan cutting an update's WiFi, relay count not refreshing in a channel thread, raw @[nick] in room previews. Labels skip unchanged text (no redraw). Helpers: scrollList, dimOverlay, settingRow, flexBox, nvs::get/put, trailFilePath (a second save in the same minute no longer overwrites), unreadTotal. Dead code removed. Co-Authored-By: Claude Opus 5.5 --- docs/development/l2-roadmap.md | 10 +- .../companion_radio/ui-lvgl/AdminScreen.h | 8 +- examples/companion_radio/ui-lvgl/BotScreen.h | 10 +- .../companion_radio/ui-lvgl/ChannelScreen.h | 8 +- .../ui-lvgl/ConversationScreen.h | 4 +- .../companion_radio/ui-lvgl/DeviceScreen.h | 9 +- examples/companion_radio/ui-lvgl/DiagScreen.h | 2 +- .../companion_radio/ui-lvgl/HistoryStore.h | 3 +- examples/companion_radio/ui-lvgl/LvglPort.h | 178 ++++++--------- examples/companion_radio/ui-lvgl/MapScreen.h | 13 +- examples/companion_radio/ui-lvgl/NavMap.h | 82 +++---- examples/companion_radio/ui-lvgl/OtaScreen.h | 3 + .../companion_radio/ui-lvgl/RadioScreen.h | 22 +- .../companion_radio/ui-lvgl/RepeaterScreen.h | 1 - .../companion_radio/ui-lvgl/SoundScreen.h | 2 +- examples/companion_radio/ui-lvgl/UITask.cpp | 205 +++++++++--------- examples/companion_radio/ui-lvgl/UITask.h | 10 +- examples/companion_radio/ui-lvgl/WifiScreen.h | 2 +- variants/wio-tracker-l2/WioTrackerL2Board.cpp | 34 ++- variants/wio-tracker-l2/WioTrackerL2Board.h | 14 +- variants/wio-tracker-l2/WioTrackerL2Display.h | 25 ++- variants/wio-tracker-l2/target.cpp | 24 +- variants/wio-tracker-l2/target.h | 15 +- 23 files changed, 337 insertions(+), 347 deletions(-) diff --git a/docs/development/l2-roadmap.md b/docs/development/l2-roadmap.md index d85eb9dc..e7dc5350 100644 --- a/docs/development/l2-roadmap.md +++ b/docs/development/l2-roadmap.md @@ -226,7 +226,15 @@ Ideas to come back to (2026-09-26): - [ ] Speaker click: would keeping the amplifier on at minimum volume remove it? - [ ] Import routes from the SD card (optional extra). -- [ ] Battery life without losing features, above all CPU sleep. +- [x] Battery life without losing features (2026-09-27): the UI loop sleeps + until something is due (none while packets are queued; 1-2 ms with a + melody / the app connected; up to 10 ms awake, 20 ms dark); screen off: + CPU at 80 MHz, touch controller asleep unless it wakes the screen, + backlight driver in standby. GPS off really cuts the L76K's rail (and + its UART). Battery divider powered only for a reading (measured: settles + at once). Also fixed: WiFi left on after a scan cut short, live tiles' + WiFi kept with the screen off on the map, a battery ADC read every + second. Not measured yet: the current before / after. - [x] Live tiles: bounded cache on the card (2026-09-26) -- they go to /sdcard/maps-live, apart from downloaded maps; an index keeps their order and the oldest are deleted past the limit (Settings > Storage > diff --git a/examples/companion_radio/ui-lvgl/AdminScreen.h b/examples/companion_radio/ui-lvgl/AdminScreen.h index e90c7ca1..bd0ef886 100644 --- a/examples/companion_radio/ui-lvgl/AdminScreen.h +++ b/examples/companion_radio/ui-lvgl/AdminScreen.h @@ -165,13 +165,7 @@ void UITask::buildAdmin() { lv_obj_add_event_cb(s_status, onAdminCancelWait, LV_EVENT_CLICKED, NULL); lv_obj_add_flag(s_status, LV_OBJ_FLAG_HIDDEN); - s_list = lv_obj_create(body); - styleSurface(s_list, theme::BG); - lv_obj_set_width(s_list, LV_PCT(100)); - lv_obj_set_flex_grow(s_list, 1); - lv_obj_set_flex_flow(s_list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(s_list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(s_list, LV_SCROLLBAR_MODE_ACTIVE); + s_list = scrollList(body); for (int i = 0; i < admin::rowCount(s_tab); i++) { const admin::Field& f = admin::field(s_tab, i); const char* hint = f.isCustom() ? "Any CLI command" : f.isAction() ? nullptr diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index dcd20a92..9b4640a9 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -89,18 +89,12 @@ void UITask::buildBot() { lv_obj_set_style_bg_color(tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS); lv_obj_add_event_cb(tabs, onBotTab, LV_EVENT_VALUE_CHANGED, NULL); - lv_obj_t* list = lv_obj_create(body); - styleSurface(list, theme::BG); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_t* list = scrollList(body); for (int i = 0; i < botcfg::rowCount(s_tab); i++) { const botcfg::Row& r = botcfg::row(s_tab, i); uint8_t* fl = botcfg::flag(p, r.kind); - lv_obj_t* row = radioview::settingRow(list, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr); + lv_obj_t* row = settingRow(list, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr); if (fl && r.kind == botcfg::DM_SCOPE) { static const char* SCOPE[] = { "All", "Fav", "" }; lv_obj_t* seg = segmented(row, SCOPE, *fl ? 1 : 0, 110, 32); diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index 99752c17..d1cb9223 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -83,13 +83,13 @@ void UITask::channelMenu(int idx) { // bottom of the screen from a popup this low). static const char* NOTIF[] = { "Default", "Muted", "Always", "" }; static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" }; - lv_obj_t* row = radioview::settingRow(panel, "Alerts", NULL); + lv_obj_t* row = settingRow(panel, "Alerts", NULL); radioview::rowSegmented(row, NOTIF, chanctl::notif(_prefs, idx), 200, onChanSegment, C_NOTIF); - row = radioview::settingRow(panel, "Sound", NULL); + row = settingRow(panel, "Sound", NULL); radioview::rowSegmented(row, MELODY, chanctl::melody(_prefs, idx), 200, onChanSegment, C_MELODY); const ScopeList& sl = the_mesh.scopeList(); if (sl.count > 0) { // only once regions are set up (Settings in the app) - row = radioview::settingRow(panel, "Scope", "Region it's sent in"); + row = settingRow(panel, "Scope", "Region it's sent in"); int o = 0; for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 24; i++) o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "%s%s", i ? "\n" : "", sl.name(i)); @@ -217,7 +217,7 @@ void UITask::buildChannelEdit() { s_secret = chanField(body, "Passphrase"); lv_textarea_set_max_length(s_secret, 32); - lv_obj_t* row = radioview::settingRow(body, "Hex key", "32 characters"); + lv_obj_t* row = settingRow(body, "Hex key", "32 characters"); s_hex_sw = lv_switch_create(row); lv_obj_set_size(s_hex_sw, 46, 24); lv_obj_align(s_hex_sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0); diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index a187bae2..cc61c6d6 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -163,10 +163,10 @@ void UITask::conversationMenu(const uint8_t* pub_key) { if (!room) { // alerts: three segments (a dropdown's list runs off screen this low) static const char* NOTIF[] = { "Default", "Muted", "Always", "" }; - lv_obj_t* row = radioview::settingRow(panel, "Alerts", NULL); + lv_obj_t* row = settingRow(panel, "Alerts", NULL); radioview::rowSegmented(row, NOTIF, contactctl::notif(_prefs, ci.id.pub_key), 200, onConvNotif, 0); static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" }; - row = radioview::settingRow(panel, "Sound", NULL); + row = settingRow(panel, "Sound", NULL); radioview::rowSegmented(row, MELODY, contactctl::melody(_prefs, ci.id.pub_key), 200, onConvMelody, 0); } else { bool in = _core->rooms.isLoggedIn(ci.id.pub_key); diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index f94d9ded..17e188c3 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -248,7 +248,7 @@ void UITask::unlockScreen() { anim::run(fading, anim::setBgOpa, LV_OPA_COVER, LV_OPA_TRANSP, anim::UNLOCK_MS, anim::coverDone); if (_body) anim::run(_body, anim::setTy, 12, 0, anim::UNLOCK_MS); } - s_lock =s_lock_clock = s_lock_date = s_lock_unread = s_lock_slider = s_pin_dots = s_pin_msg = nullptr; + s_lock = s_lock_clock = s_lock_date = s_lock_unread = s_lock_slider = s_pin_dots = s_pin_msg = nullptr; _pin_entry[0] = '\0'; if (_screen == SCR_HOME) refreshHome(); else if (_screen == SCR_CHATS) buildChats(); // counts moved on while locked @@ -302,7 +302,7 @@ void UITask::refreshLock() { lv_label_set_text(s_lock_clock, clk); if (s_lock_date) lv_label_set_text(s_lock_date, date); } - int unread = _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread(); + 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, ""); @@ -449,11 +449,6 @@ void UITask::setAccent(int idx) { // ── Favourites card (Home, left of the main page) ───────────────────────────── -void UITask::showFavourites() { - home::s_page = home::FAVS; - showHome(); -} - // The six slots, three to a row, into `grid` (a wrapping row). void UITask::favGrid(lv_obj_t* grid) { int w = (lv_display_get_horizontal_resolution(NULL) - 2 * theme::PAD - 2 * theme::GAP) / 3; diff --git a/examples/companion_radio/ui-lvgl/DiagScreen.h b/examples/companion_radio/ui-lvgl/DiagScreen.h index beec19e7..5a7a4b6c 100644 --- a/examples/companion_radio/ui-lvgl/DiagScreen.h +++ b/examples/companion_radio/ui-lvgl/DiagScreen.h @@ -117,7 +117,7 @@ void UITask::refreshDiag() { int n = allRows(rows, _core->gpsEnabled()); if (n != s_rows) { buildDiag(); return; } for (int i = 0; i < n; i++) - if (strcmp(lv_label_get_text(s_vals[i]), rows[i].value) != 0) lv_label_set_text(s_vals[i], rows[i].value); + lv_label_set_text(s_vals[i], rows[i].value); // unchanged text isn't redrawn (UITask.cpp) } void UITask::diagResetPopup() { diff --git a/examples/companion_radio/ui-lvgl/HistoryStore.h b/examples/companion_radio/ui-lvgl/HistoryStore.h index 47659810..c6c87537 100644 --- a/examples/companion_radio/ui-lvgl/HistoryStore.h +++ b/examples/companion_radio/ui-lvgl/HistoryStore.h @@ -245,8 +245,7 @@ private: snprintf(tmp, sizeof(tmp), "%s.tmp", path); FILE* o = fopen(tmp, "wb"); if (o) { - Hdr nh = { MAGIC, VERSION, (uint16_t)sizeof(E), cap, 0, got, h.next_id }; - nh.next_id = h.next_id; // ids of the kept records stay as they were + Hdr nh = { MAGIC, VERSION, (uint16_t)sizeof(E), cap, 0, got, h.next_id }; // the kept records keep their ids bool ok = fwrite(&nh, sizeof(nh), 1, o) == 1 && (got == 0 || fwrite(buf, sizeof(E), got, o) == got); fclose(o); if (ok) { remove(path); rename(tmp, path); } // FAT's rename doesn't replace diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index 90660d4c..a161e0d3 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -103,6 +103,21 @@ 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. +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); + 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)); } + // Backlight 1-100 % (the brightness slider), on the LP5814 PWM. static void setBacklightPct(uint8_t pct) { if (pct > 100) pct = 100; @@ -110,141 +125,84 @@ static void setBacklightPct(uint8_t pct) { } // microSD over SDMMC, 1-bit (CLK 2, CMD 3, D0 1); its power rail (expander -// P14) is switched on in WioTrackerL2Board::begin(). Retried on every call -// until it works, so a card inserted later is picked up the next time. +// P14) is switched on in WioTrackerL2Board::begin(). Retried until it works -- +// at most every 5 s, a failed mount takes a while -- so a card inserted later +// is picked up. static bool mountStorage() { static bool mounted = false; + static uint32_t last_try = 0; if (mounted) return true; + if (last_try && millis() - last_try < 5000) return false; + last_try = millis() | 1; SD_MMC.setPins(2, 3, 1); mounted = SD_MMC.begin("/sdcard", true /* 1-bit */); return mounted; } -// ── WiFi (station, only while a map download runs) ─────────────────────────── -// Credentials in NVS, not in NodePrefs (whose on-flash layout stays fixed) and -// not on the removable card. -static bool loadWifi(char* ssid, size_t ssid_n, char* pass, size_t pass_n) { +// ── Settings kept in NVS ───────────────────────────────────────────────────── +// Not in NodePrefs (whose on-flash layout stays fixed) and not on the +// removable card. One value per call: the namespace is opened and closed +// around it; a namespace that won't open reads as the default. +namespace nvs { +struct Ns { Preferences p; - if (!p.begin("mc_wifi", true)) { ssid[0] = pass[0] = '\0'; return false; } - p.getString("ssid", ssid, ssid_n); - p.getString("pass", pass, pass_n); - p.end(); + bool ok; + Ns(const char* ns, bool ro) { ok = p.begin(ns, ro); } + ~Ns() { if (ok) p.end(); } +}; +static bool getBool(const char* ns, const char* key, bool def) { Ns n(ns, true); return n.ok ? n.p.getBool(key, def) : def; } +static void putBool(const char* ns, const char* key, bool v) { Ns n(ns, false); if (n.ok) n.p.putBool(key, v); } +static int getI8(const char* ns, const char* key, int def) { Ns n(ns, true); return n.ok ? n.p.getChar(key, (int8_t)def) : def; } +static void putI8(const char* ns, const char* key, int v) { Ns n(ns, false); if (n.ok) n.p.putChar(key, (int8_t)v); } +static int getU8(const char* ns, const char* key, int def) { Ns n(ns, true); return n.ok ? n.p.getUChar(key, (uint8_t)def) : def; } +static void putU8(const char* ns, const char* key, int v) { Ns n(ns, false); if (n.ok) n.p.putUChar(key, (uint8_t)v); } +static void getStr(const char* ns, const char* key, char* out, size_t n_out) { + out[0] = '\0'; + Ns n(ns, true); + if (n.ok) n.p.getString(key, out, n_out); +} +static void putStr(const char* ns, const char* key, const char* v) { Ns n(ns, false); if (n.ok) n.p.putString(key, v); } +} // namespace nvs + +// ── WiFi (station, only while a map download runs) ─────────────────────────── +static bool loadWifi(char* ssid, size_t ssid_n, char* pass, size_t pass_n) { + nvs::getStr("mc_wifi", "ssid", ssid, ssid_n); + nvs::getStr("mc_wifi", "pass", pass, pass_n); return ssid[0] != '\0'; } static void saveWifi(const char* ssid, const char* pass) { - Preferences p; - if (!p.begin("mc_wifi", false)) return; - p.putString("ssid", ssid); - p.putString("pass", pass); - p.end(); + nvs::putStr("mc_wifi", "ssid", ssid); + nvs::putStr("mc_wifi", "pass", pass); } // 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) { - Preferences p; - bool on = true; - if (p.begin("mc_wifi", true)) { on = p.getBool("on", true); p.end(); } - s_wifi_allowed = on; - } + 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; - Preferences p; - if (!p.begin("mc_wifi", false)) return; - p.putBool("on", on); - p.end(); -} +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() { - Preferences p; - if (!p.begin("mc_wifi", true)) return true; - bool on = p.getBool("live", true); - p.end(); - return on; -} -static void setLiveTiles(bool on) { - Preferences p; - if (!p.begin("mc_wifi", false)) return; - p.putBool("live", on); - p.end(); -} +static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); } +static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); } // Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none. -// In NVS for the same reason as the WiFi credentials. -static void loadPin(char* out, size_t n) { - Preferences p; - out[0] = '\0'; - if (!p.begin("mc_lock", true)) return; - p.getString("pin", out, n); - p.end(); -} -static void savePin(const char* pin) { - Preferences p; - if (!p.begin("mc_lock", false)) return; - p.putString("pin", pin); - p.end(); -} +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() { - Preferences p; - if (!p.begin("mc_ui", true)) return 0; - int v = p.getUChar("accent", 0); - p.end(); - return v; -} -static void saveAccent(int idx) { - Preferences p; - if (!p.begin("mc_ui", false)) return; - p.putUChar("accent", (uint8_t)idx); - p.end(); -} +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() { - Preferences p; - if (!p.begin("mc_ui", true)) return -1; - int v = p.getChar("hkeep", -1); - p.end(); - return v; -} -static void saveHistKeep(int idx) { - Preferences p; - if (!p.begin("mc_ui", false)) return; - p.putChar("hkeep", (int8_t)idx); - p.end(); -} +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() { - Preferences p; - if (!p.begin("mc_ui", true)) return -1; - int v = p.getChar("ltcap", -1); - p.end(); - return v; -} -static void saveLiveCap(int idx) { - Preferences p; - if (!p.begin("mc_ui", false)) return; - p.putChar("ltcap", (int8_t)idx); - p.end(); -} +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 side button does). -static bool loadTapWake() { - Preferences p; - if (!p.begin("mc_ui", true)) return true; - bool on = p.getBool("tapwake", true); - p.end(); - return on; -} -static void saveTapWake(bool on) { - Preferences p; - if (!p.begin("mc_ui", false)) return; - p.putBool("tapwake", on); - p.end(); -} +// (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); } // 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. @@ -495,7 +453,7 @@ static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) { static bool begin() { lv_display_t* disp = lv_display_create(SimLcdDisplay::W, SimLcdDisplay::H); lv_display_set_flush_cb(disp, flushCb); - static uint8_t buf[SimLcdDisplay::W * 40 * 2]; // same 40-line partial buffer as the board + static uint8_t buf[SimLcdDisplay::W * 40 * 2]; // 40 lines (the board: two half screens) lv_display_set_buffers(disp, buf, nullptr, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL); lv_indev_t* indev = lv_indev_create(); @@ -506,6 +464,8 @@ 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 idle(uint32_t) {} static void setBacklightPct(uint8_t pct) { (void)pct; } // the browser canvas has no backlight // The host page preloads map tiles into the in-memory FS under /sdcard/maps. diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 0a27f963..28d000fc 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -583,13 +583,7 @@ void UITask::mapDownloadPopup() { if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3; if (_dl_zmax > src_max) _dl_zmax = src_max; // the server has nothing finer - _dl_overlay = lv_obj_create(screen()); - lv_obj_remove_style_all(_dl_overlay); - lv_obj_set_size(_dl_overlay, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_bg_color(_dl_overlay, lv_color_hex(0x000000), 0); - lv_obj_set_style_bg_opa(_dl_overlay, LV_OPA_60, 0); - lv_obj_add_flag(_dl_overlay, LV_OBJ_FLAG_CLICKABLE); - lv_obj_remove_flag(_dl_overlay, LV_OBJ_FLAG_SCROLLABLE); + _dl_overlay = dimOverlay(screen()); lv_obj_t* panel = lv_obj_create(_dl_overlay); anim::popup(_dl_overlay); @@ -828,8 +822,9 @@ void UITask::mapDownloadTick() { bool got = false; while (dl.liveTake(z, x, y)) { mapview::s_cache.forgetMissing(z, x, y); got = true; } if (got && _screen == SCR_MAP) layoutMap(); - // The WiFi goes 30 s after the map was left (a quick look elsewhere keeps it). - if (_screen != SCR_MAP) { + // The WiFi goes 30 s after the map was left or the screen went off (a + // quick look elsewhere keeps it). + if (_screen != SCR_MAP || _asleep) { if (!_map_left_ms) _map_left_ms = millis() | 1; else if (millis() - _map_left_ms > 30000) dl.liveEnd(); } diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index badd7eb7..abdbab8a 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -476,13 +476,7 @@ void UITask::navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char* lv_obj_t* UITask::navPopupPanel(const char* title, bool full) { navClosePopup(); - _nav_overlay = lv_obj_create(screen()); - lv_obj_remove_style_all(_nav_overlay); - lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0); - lv_obj_set_style_bg_opa(_nav_overlay, LV_OPA_60, 0); - lv_obj_add_flag(_nav_overlay, LV_OBJ_FLAG_CLICKABLE); - lv_obj_remove_flag(_nav_overlay, LV_OBJ_FLAG_SCROLLABLE); + _nav_overlay = dimOverlay(screen()); lv_obj_t* panel = lv_obj_create(_nav_overlay); anim::popup(_nav_overlay); @@ -529,13 +523,7 @@ static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right void UITask::navTargetsPopup() { lv_obj_t* panel = navPopupPanel("Navigate to", true); - lv_obj_t* list = lv_obj_create(panel); - styleSurface(list, theme::BG); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_t* list = scrollList(panel); int32_t lat = 0, lon = 0; bool gps = _core->course.currentLocation(lat, lon); @@ -914,13 +902,7 @@ static lv_obj_t* toolButton(lv_obj_t* row, const char* text, uint8_t act, bool a void UITask::navToolsPopup() { lv_obj_t* panel = navPopupPanel("Map tools", true); - lv_obj_t* list = lv_obj_create(panel); - styleSurface(list, theme::BG); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_t* list = scrollList(panel); sectionTitle(list, "TRAIL"); _nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); @@ -1035,20 +1017,31 @@ void UITask::navSetShareTarget(int sel) { the_mesh.savePrefs(); } +// A new file in /sdcard/trails named by the local time, "trail-YYYYMMDD-HHMM" +// (seconds since boot before the clock is set), with `ext`; a second save in +// the same minute gets "-2", "-3"... +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); + } else { + snprintf(stem, sizeof(stem), "%s/trail-%lu", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000)); + } + snprintf(path, n, "%s%s", stem, ext); + struct stat st; + for (int k = 2; k < 100 && stat(path, &st) == 0; k++) snprintf(path, n, "%s-%d%s", stem, k, ext); + mapview::makeParents(path); +} + // GPX of the live trail (with the waypoints) to /sdcard/trails/. bool UITask::exportTrailGpx(char* name_out, size_t n) { if (!lvport::mountStorage()) return false; char path[64]; - uint32_t now = rtc_clock.getCurrentTime(); - if (now > 1000000000UL) { - time_t t = (time_t)((int64_t)now + (int64_t)_prefs->tz_offset_hours * 3600); - struct tm* ti = gmtime(&t); - snprintf(path, sizeof(path), "/sdcard/trails/trail-%04d%02d%02d-%02d%02d.gpx", ti->tm_year + 1900, - ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min); - } else { - snprintf(path, sizeof(path), "/sdcard/trails/trail-%lu.gpx", (unsigned long)(millis() / 1000)); - } - mapview::makeParents(path); + trailFilePath(path, sizeof(path), ".gpx", _prefs); FILE* f = fopen(path, "w"); if (!f) return false; navmap::FilePrint out(f); @@ -1062,16 +1055,7 @@ bool UITask::exportTrailGpx(char* name_out, size_t n) { // The live trail to a new file in /sdcard/trails (named by the local time). bool UITask::saveTrailToCard(char* name_out, size_t n) { char path[64]; - uint32_t now = rtc_clock.getCurrentTime(); - if (now > 1000000000UL) { - time_t t = (time_t)((int64_t)now + (int64_t)_prefs->tz_offset_hours * 3600); - struct tm* ti = gmtime(&t); - snprintf(path, sizeof(path), "%s/trail-%04d%02d%02d-%02d%02d.trl", navmap::TRAILS_DIR, ti->tm_year + 1900, - ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min); - } else { - snprintf(path, sizeof(path), "%s/trail-%lu.trl", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000)); - } - mapview::makeParents(path); + trailFilePath(path, sizeof(path), ".trl", _prefs); FILE* f = fopen(path, "wb"); if (!f) return false; navmap::FileRW io{ f }; @@ -1086,13 +1070,7 @@ bool UITask::saveTrailToCard(char* name_out, size_t n) { // device's own slot (manual saves without a card, the low-battery auto-save). void UITask::savedTrailsPopup() { lv_obj_t* panel = navPopupPanel("Saved trails", true); - lv_obj_t* list = lv_obj_create(panel); - styleSurface(list, theme::BG); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_t* list = scrollList(panel); navmap::scanTrails(); for (int i = 0; i < navmap::s_st_n; i++) { char title[32], sub[48], path[64]; @@ -1328,13 +1306,7 @@ void UITask::navWaypointsPopup() { toolButton(r, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true); toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false); - lv_obj_t* list = lv_obj_create(panel); - styleSurface(list, theme::BG); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_t* list = scrollList(panel); int32_t lat = 0, lon = 0; bool gps = _core->course.currentLocation(lat, lon); for (int i = 0; i < wp.count(); i++) { diff --git a/examples/companion_radio/ui-lvgl/OtaScreen.h b/examples/companion_radio/ui-lvgl/OtaScreen.h index 93ceebbd..ce5a5b74 100644 --- a/examples/companion_radio/ui-lvgl/OtaScreen.h +++ b/examples/companion_radio/ui-lvgl/OtaScreen.h @@ -174,6 +174,9 @@ void UITask::showOta() { buildOta(); } +// Something else holds the WiFi: a scan leaves it up. +bool UITask::wifiInUse() const { return mapview::s_dl.active() || otaBusy() || ota::s_state == ota::CONNECTING; } + bool UITask::otaBusy() const { return ota::s_state == ota::CHECKING || ota::s_state == ota::INSTALLING || ota::s_state == ota::DONE; } void UITask::buildOta() { diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h index 9143f97b..efe0da68 100644 --- a/examples/companion_radio/ui-lvgl/RadioScreen.h +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -24,21 +24,6 @@ static const int OPTS_LEN = 1024; static char* s_opts = psramBuf(OPTS_LEN); // dropdown options (LVGL copies them) // Label + optional hint on the left of a settings row; the control goes on the right. -static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint) { - lv_obj_t* row = lv_obj_create(parent); - styleSurface(row, theme::SURFACE); - lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); - lv_obj_set_style_radius(row, theme::RADIUS, 0); - lv_obj_align(label(row, text, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13); - if (hint) { - lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); - lv_obj_set_size(h, 150, 15); - lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); - } - return row; -} static lv_obj_t* rowDropdown(lv_obj_t* row, const char* opts, int sel, int width, lv_event_cb_t cb, uintptr_t which) { lv_obj_t* dd = lv_dropdown_create(row); @@ -170,12 +155,7 @@ void UITask::radioFreqPopup(bool repeater) { using namespace radioview; if (s_overlay) return; s_freq_rpt = repeater; - s_overlay = lv_obj_create(screen()); - lv_obj_remove_style_all(s_overlay); - lv_obj_set_size(s_overlay, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_bg_color(s_overlay, lv_color_hex(0x000000), 0); - lv_obj_set_style_bg_opa(s_overlay, LV_OPA_60, 0); - lv_obj_add_flag(s_overlay, LV_OBJ_FLAG_CLICKABLE); + s_overlay = dimOverlay(screen()); lv_obj_t* panel = lv_obj_create(s_overlay); anim::popup(s_overlay); lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT); diff --git a/examples/companion_radio/ui-lvgl/RepeaterScreen.h b/examples/companion_radio/ui-lvgl/RepeaterScreen.h index 7d9b6ec6..8a13b3c9 100644 --- a/examples/companion_radio/ui-lvgl/RepeaterScreen.h +++ b/examples/companion_radio/ui-lvgl/RepeaterScreen.h @@ -48,7 +48,6 @@ void UITask::showRepeater() { void UITask::buildRepeater() { using namespace rptview; - using radioview::settingRow; using radioview::rowDropdown; lv_obj_t* body = newScreen("Repeater", true); NodePrefs* p = _prefs; diff --git a/examples/companion_radio/ui-lvgl/SoundScreen.h b/examples/companion_radio/ui-lvgl/SoundScreen.h index 08d4b8ba..18ff583f 100644 --- a/examples/companion_radio/ui-lvgl/SoundScreen.h +++ b/examples/companion_radio/ui-lvgl/SoundScreen.h @@ -268,7 +268,7 @@ void UITask::buildSoundRows(lv_obj_t* body, bool top) { using namespace sndview; if (top) { static const char* MODES[] = { "On", "Off", "Auto", "" }; - lv_obj_t* row = radioview::settingRow(body, "Sound", "Auto: off with the app"); + lv_obj_t* row = settingRow(body, "Sound", "Auto: off with the app"); radioview::rowSegmented(row, MODES, soundctl::mode(_prefs), 150, onSoundMode, 0); return; } diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index fbc5da55..ba1135c5 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -13,10 +13,14 @@ // this UI (all of it is this one file and the headers it includes) goes through // these, which swap each pair for the codepoint fonts/ui_emoji.c keeps the // flag's image under. Display only: what's stored and sent stays as it was. +// A label given the text it already shows is left alone: the status bar, the +// clocks and the live values are set every refresh, and each set is a redraw. extern "C" char* ui_emoji_flags(const char* s); static void ui_label_set_text(lv_obj_t* o, const char* t) { char* f = t ? ui_emoji_flags(t) : nullptr; // t == NULL: LVGL's "redraw the current text" - lv_label_set_text(o, f ? f : t); + const char* s = f ? f : t; + const char* cur = lv_label_get_text(o); + if (!s || !cur || strcmp(cur, s) != 0) lv_label_set_text(o, s); lv_free(f); } static void ui_label_set_text_fmt(lv_obj_t* o, const char* fmt, ...) __attribute__((format(printf, 2, 3))); @@ -121,6 +125,59 @@ static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font return l; } +// A list filling the rest of a flex column, scrolling on its own (the +// scrollbar only while it moves). +static lv_obj_t* scrollList(lv_obj_t* parent) { + lv_obj_t* l = lv_obj_create(parent); + styleSurface(l, theme::BG); + lv_obj_set_width(l, LV_PCT(100)); + lv_obj_set_flex_grow(l, 1); + lv_obj_set_flex_flow(l, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(l, theme::GAP, 0); + lv_obj_set_scrollbar_mode(l, LV_SCROLLBAR_MODE_ACTIVE); + return l; +} + +// The dimmed full-screen layer under a popup; it swallows taps on what's below. +static lv_obj_t* dimOverlay(lv_obj_t* parent) { + lv_obj_t* o = lv_obj_create(parent); + lv_obj_remove_style_all(o); + lv_obj_set_size(o, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(o, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(o, LV_OPA_60, 0); + lv_obj_add_flag(o, LV_OBJ_FLAG_CLICKABLE); + lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE); + return o; +} + +// A settings row: the label left (a muted hint under it, cut at `hint_w`), +// room on the right for a switch / dropdown / slider. +static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint, int hint_w = 150) { + lv_obj_t* row = lv_obj_create(parent); + styleSurface(row, theme::SURFACE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); + lv_obj_set_style_radius(row, theme::RADIUS, 0); + lv_obj_align(label(row, text, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13); + if (hint) { + lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); + lv_obj_set_size(h, hint_w, 15); // fixed height: cut, don't wrap onto the label + lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); + } + return row; +} + +// A bare flex row / column sized to its content, taps passing through. +static lv_obj_t* flexBox(lv_obj_t* parent, lv_flex_flow_t flow) { + lv_obj_t* b = lv_obj_create(parent); + lv_obj_remove_style_all(b); + lv_obj_remove_flag(b, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(b, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(b, flow); + return b; +} + // A one-of-N choice: a row (or grid, with "\n" in the map) of checkable // buttons, `sel` checked (-1: none). `item` is the unchecked button colour — // SURFACE on a SURFACE_2 panel, SURFACE_2 on the page. @@ -171,7 +228,6 @@ 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); } -// Unread badge: amber pill with a count, on the right edge of `parent`. // "@[nick]" -> "@nick" in place, for one-line text (previews, quotes) where // the bubble's highlight doesn't reach. static void plainMentions(char* t) { @@ -189,6 +245,7 @@ static void plainMentions(char* t) { *w = '\0'; } +// Unread badge: amber pill with a count, on the right edge of `parent`. static void badge(lv_obj_t* parent, int n, bool overflow) { if (n <= 0) return; lv_obj_t* b = lv_obj_create(parent); @@ -357,7 +414,11 @@ void UITask::loop() { else if (btn_click) { if (!_asleep && !locked()) back(); } // the side button doesn't wake: pockets if (_asleep) { - if (lvport::touched()) { lvport::swallowTouch(); if (_tap_wake) wake(); } // off: the top (WAKE) button only + // Tap to wake (off: the top button only). An I2C read on the touch panel. + if (_tap_wake && (int32_t)(millis() - _next_touch_poll_ms) >= 0) { + _next_touch_poll_ms = millis() + 50; + if (lvport::touched()) { lvport::swallowTouch(); wake(); } + } } else { uint32_t aoff = autoOffMillis(); if (aoff > 0 && lv_display_get_inactive_time(NULL) > aoff && !_core->clock.isRinging() && !otaBusy()) sleep(); @@ -438,6 +499,7 @@ void UITask::loop() { navPollTrackBack(); } + uint32_t lv_next = 0; if (!_asleep) { if ((int32_t)(millis() - _next_status_ms) >= 0) { _next_status_ms = millis() + 1000; @@ -479,8 +541,22 @@ void UITask::loop() { uint32_t sig = threadSignature(); if (_thread_dirty || sig != _thread_sig) refreshThread(); } - lv_timer_handler(); + lv_next = lv_timer_handler(); } + lvport::idle(idleMillis(lv_next)); +} + +// 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). +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 (_asleep) return 20; // the buttons and tap to wake are polled every 50 ms + return lv_next < 10 ? lv_next : 10; // LVGL's next timer: a refresh, an animation, input } void UITask::shutdown(bool restart) { @@ -532,6 +608,7 @@ void UITask::sleep() { if (_asleep) return; _asleep = true; if (_display) _display->turnOff(); + lvport::powerSave(true, _tap_wake); if ((_prefs && _prefs->auto_lock) || _pin[0]) lockScreen(); // Lock screen, or a screen PIN } @@ -539,6 +616,7 @@ void UITask::wake() { lv_display_trigger_activity(NULL); if (!_asleep) return; _asleep = false; + lvport::powerSave(false, _tap_wake); if (_display) _display->turnOn(); refreshStatusBar(); if (_screen == SCR_HOME) refreshHome(); @@ -664,8 +742,9 @@ void UITask::refreshStatusBar() { lv_label_set_text(_status_time, "--:--"); } - // Battery: the icon, then % or volts per Settings > Battery display. - uint16_t mv = getBattMilliVolts(); + // Battery: the icon, then % or volts per Settings > Battery display. The + // smoothed reading (checkLowBattery): an ADC read blocks the loop for 10 ms. + uint16_t mv = _batt_mv ? _batt_mv : getBattMilliVolts(); int pct = battery::percent(mv, _prefs ? _prefs->low_batt_mv : 0); const char* batt = pct > 80 ? LV_SYMBOL_BATTERY_FULL : pct > 55 ? LV_SYMBOL_BATTERY_3 : pct > 30 ? LV_SYMBOL_BATTERY_2 : pct > 10 ? LV_SYMBOL_BATTERY_1 : LV_SYMBOL_BATTERY_EMPTY; @@ -769,6 +848,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { _nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = _nav_rec = _nav_avg_pill = nullptr; _nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr; _wifi_ssid = _wifi_pass = _wifi_kb = _wifi_list = _wifi_status = nullptr; + if (_wifi_scanning && !wifiInUse()) lvport::netEnd(); // left mid-scan: the radio goes off _wifi_scanning = false; _ota_status = _ota_bar = _ota_btn = _ota_btn_lbl = nullptr; for (lv_obj_t*& t : _map_tiles) t = nullptr; @@ -1071,7 +1151,7 @@ void UITask::setHomePage(int page) { else lv_obj_remove_flag(home::s_top, LV_OBJ_FLAG_HIDDEN); lv_obj_set_flex_flow(home::s_row, favs ? LV_FLEX_FLOW_ROW_WRAP : LV_FLEX_FLOW_ROW); if (favs) { - home::s_fav_sig = homeUnreadTotal(); + home::s_fav_sig = unreadTotal(); favGrid(home::s_row); // DeviceScreen.h } int first = (page - home::MAIN) * home::PER_PAGE; @@ -1092,7 +1172,7 @@ void UITask::setHomePage(int page) { refreshHome(); } -int UITask::homeUnreadTotal() { +int UITask::unreadTotal() { return _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread(); } @@ -1109,12 +1189,12 @@ void UITask::refreshHome() { lv_label_set_text(_home_clock, "--:--"); lv_label_set_text(_home_date, "time not synced"); } - if (home::s_page == home::FAVS && home::s_row && homeUnreadTotal() != home::s_fav_sig) { + if (home::s_page == home::FAVS && home::s_row && unreadTotal() != home::s_fav_sig) { setHomePage(home::FAVS); // new messages: the favourites' badges redrawn return; } if (!_home_unread) return; - int unread = homeUnreadTotal(); + int unread = unreadTotal(); if (unread > 0) lv_label_set_text_fmt(_home_unread, "%d", unread); else lv_label_set_text(_home_unread, ""); } @@ -1210,7 +1290,7 @@ static lv_obj_t* headerButton(lv_obj_t* hdr, const char* text, lv_event_cb_t cb, void UITask::buildChats() { lv_obj_t* body = newScreen("Messages", true); - if (_header && _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread() > 0) + if (_header && unreadTotal() > 0) headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 4, NULL); // Channels: favourites first (unless turned off), hold a row for its options @@ -1299,6 +1379,7 @@ void UITask::buildChats() { if (_core->history.dmHistCountForContact(c.id.pub_key) > 0) { const DmHistEntry& e = _core->history.dmAtPos(_core->history.dmHistEntryForContact(c.id.pub_key, 0)); snprintf(sub, sizeof(sub), "%s%s", e.outgoing ? "Me: " : "", e.text); + plainMentions(sub); } else { snprintf(sub, sizeof(sub), "%s", _core->rooms.isLoggedIn(c.id.pub_key) ? "Logged in" : "Tap to log in"); } @@ -1418,13 +1499,7 @@ void UITask::buildNearby() { _nearby_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED); - _nearby_list = lv_obj_create(body); - styleSurface(_nearby_list, theme::BG); - lv_obj_set_width(_nearby_list, LV_PCT(100)); - lv_obj_set_flex_grow(_nearby_list, 1); - lv_obj_set_flex_flow(_nearby_list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(_nearby_list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(_nearby_list, LV_SCROLLBAR_MODE_ACTIVE); + _nearby_list = scrollList(body); _nearby_sig = 0; refreshNearbyList(); @@ -1454,13 +1529,7 @@ void UITask::startNearbyScan() { // Dimmed full-screen overlay (swallows taps) holding a panel with the results. // A child of the current screen, so it goes away with it. void UITask::showScanPopup() { - _scan_overlay = lv_obj_create(screen()); - lv_obj_remove_style_all(_scan_overlay); - lv_obj_set_size(_scan_overlay, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_bg_color(_scan_overlay, lv_color_hex(0x000000), 0); - lv_obj_set_style_bg_opa(_scan_overlay, LV_OPA_60, 0); - lv_obj_add_flag(_scan_overlay, LV_OBJ_FLAG_CLICKABLE); - lv_obj_remove_flag(_scan_overlay, LV_OBJ_FLAG_SCROLLABLE); + _scan_overlay = dimOverlay(screen()); lv_obj_t* panel = lv_obj_create(_scan_overlay); anim::popup(_scan_overlay); @@ -1485,13 +1554,7 @@ void UITask::showScanPopup() { headerButton(hdr, LV_SYMBOL_REFRESH " Again", onNearbyScan, 44, NULL); _scan_status = label(panel, "", THEME_FONT_SMALL, theme::TEXT_MUTED); - _scan_list = lv_obj_create(panel); - styleSurface(_scan_list, theme::BG); - lv_obj_set_width(_scan_list, LV_PCT(100)); - lv_obj_set_flex_grow(_scan_list, 1); - lv_obj_set_flex_flow(_scan_list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(_scan_list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(_scan_list, LV_SCROLLBAR_MODE_ACTIVE); + _scan_list = scrollList(panel); _scan_sig = 0; refreshScanPopup(); @@ -1805,7 +1868,6 @@ void UITask::nodeAction(uint8_t action) { char t[48]; if (_core->waypoints.add(e.lat_e6, e.lon_e6, rtc_clock.getCurrentTime(), e.name[0] ? e.name : "Node")) { snprintf(t, sizeof(t), "Saved %s", _core->waypoints.at(_core->waypoints.count() - 1).label); - the_mesh.savePrefs(); showToast(t); } break; @@ -1931,14 +1993,8 @@ void UITask::buildThread() { headerButton(_header, LV_SYMBOL_SETTINGS, _thread_is_channel ? onChanThreadMenu : onConvThreadMenu, 4, NULL); } - _thread_list = lv_obj_create(body); - styleSurface(_thread_list, theme::BG); - lv_obj_set_width(_thread_list, LV_PCT(100)); - lv_obj_set_flex_grow(_thread_list, 1); - lv_obj_set_flex_flow(_thread_list, LV_FLEX_FLOW_COLUMN); + _thread_list = scrollList(body); lv_obj_set_style_pad_all(_thread_list, theme::PAD, 0); - lv_obj_set_style_pad_row(_thread_list, theme::GAP, 0); - lv_obj_set_scrollbar_mode(_thread_list, LV_SCROLLBAR_MODE_ACTIVE); // only while scrolling _compose_ta = nullptr; _keyboard = nullptr; @@ -1990,7 +2046,7 @@ uint32_t UITask::threadSignature() const { sig = n; for (int j = 0; j < n && j < 8; j++) { const ChHistEntry& e = h.chAtPos(h.histEntryForChannel(_thread_channel, j)); - sig = sig * 31 + e.timestamp + e.relay_status; + sig = sig * 31 + e.timestamp + e.relay_status + e.path_len; // path_len: repeaters heard } } else { int n = h.dmHistCountForContact(_thread_key); @@ -2140,13 +2196,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, // the bubble two lines tall -- matters with the keyboard up. bool meta_in_header = from && from[0] && !status; if (from && from[0]) { - lv_obj_t* hdr = lv_obj_create(b); - styleSurface(hdr, theme::BG); - lv_obj_set_style_bg_opa(hdr, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_remove_flag(hdr, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_size(hdr, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW); + lv_obj_t* hdr = flexBox(b, LV_FLEX_FLOW_ROW); lv_obj_set_style_pad_column(hdr, 8, 0); label(hdr, from, THEME_FONT_SMALL, theme::ACCENT); // names are <= 31 chars: fits the bubble if (meta_in_header) label(hdr, meta, THEME_FONT_SMALL, theme::TEXT_MUTED); @@ -2159,13 +2209,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, } if (loc_idx >= 0) { // position: navigate there / keep it as a waypoint - lv_obj_t* acts = lv_obj_create(b); - styleSurface(acts, theme::BG); - lv_obj_set_style_bg_opa(acts, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_size(acts, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); + lv_obj_t* acts = flexBox(b, LV_FLEX_FLOW_ROW); lv_obj_set_style_pad_column(acts, 6, 0); lv_obj_set_style_pad_top(acts, 2, 0); msgLocButton(acts, UI_SYMBOL_COMPASS " Go", loc_idx, false, true); @@ -2173,11 +2217,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, } if (!meta_in_header) { // the age, then the delivery mark in its colour (as L1) - lv_obj_t* line = lv_obj_create(b); - lv_obj_remove_style_all(line); - lv_obj_remove_flag(line, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_size(line, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(line, LV_FLEX_FLOW_ROW); + lv_obj_t* line = flexBox(b, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(line, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(line, 6, 0); label(line, meta, THEME_FONT_SMALL, theme::TEXT_MUTED); @@ -2399,13 +2439,7 @@ static void onKeyboardAlphabet(lv_event_t* e) { // One settings row: label left, dropdown right. static lv_obj_t* dropdownRow(lv_obj_t* parent, const char* text, const char* options, int sel) { - lv_obj_t* row = lv_obj_create(parent); - styleSurface(row, theme::SURFACE); - lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); - lv_obj_set_style_radius(row, theme::RADIUS, 0); - lv_obj_t* l = label(row, text, THEME_FONT_BODY, theme::TEXT); - lv_obj_align(l, LV_ALIGN_LEFT_MID, theme::PAD, 0); + lv_obj_t* row = settingRow(parent, text, NULL); lv_obj_t* dd = lv_dropdown_create(row); lv_dropdown_set_options_static(dd, options); lv_dropdown_set_selected(dd, sel); @@ -2428,19 +2462,7 @@ static void onGpsSwitch(lv_event_t* e) { // pref == nullptr: the caller wires the switch itself. static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref) { - lv_obj_t* row = lv_obj_create(parent); - styleSurface(row, theme::SURFACE); - lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); - lv_obj_set_style_radius(row, theme::RADIUS, 0); - lv_obj_t* l = label(row, text, THEME_FONT_BODY, theme::TEXT); - lv_obj_align(l, LV_ALIGN_TOP_LEFT, theme::PAD, sub ? 5 : 13); - if (sub) { - lv_obj_t* h = label(row, sub, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); - lv_obj_set_size(h, 236, 15); // clear of the switch - lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); - } + lv_obj_t* row = settingRow(parent, text, sub, 236); // the hint clear of the switch lv_obj_t* sw = lv_switch_create(row); lv_obj_set_size(sw, 46, 24); lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0); @@ -2574,7 +2596,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) { } uint8_t v = settings::get(*_prefs, st); if (st.offset == offsetof(NodePrefs, buzzer_volume)) { // a five-step slider, heard on release - lv_obj_t* row = radioview::settingRow(body, st.label, NULL); + lv_obj_t* row = settingRow(body, st.label, NULL); lv_obj_t* sl = lv_slider_create(row); lv_slider_set_range(sl, 0, 4); lv_slider_set_value(sl, v, LV_ANIM_OFF); @@ -2585,12 +2607,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) { continue; } if (st.offset == offsetof(NodePrefs, display_brightness)) { // a slider here instead of five steps - lv_obj_t* row = lv_obj_create(body); - styleSurface(row, theme::SURFACE); - lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); - lv_obj_set_style_radius(row, theme::RADIUS, 0); - lv_obj_align(label(row, st.label, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_LEFT_MID, theme::PAD, 0); + lv_obj_t* row = settingRow(body, st.label, NULL); lv_obj_t* sl = lv_slider_create(row); lv_slider_set_range(sl, 5, 100); uint8_t pct = _prefs->display_brightness_pct ? _prefs->display_brightness_pct @@ -2617,19 +2634,7 @@ void UITask::schemaRows(lv_obj_t* body, uint8_t page) { o += strlen(opts + o); } opts[o] = '\0'; - lv_obj_t* row = lv_obj_create(body); - styleSurface(row, theme::SURFACE); - lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); - lv_obj_set_style_radius(row, theme::RADIUS, 0); - lv_obj_t* l = label(row, st.label, THEME_FONT_BODY, theme::TEXT); - lv_obj_align(l, LV_ALIGN_TOP_LEFT, theme::PAD, st.hint ? 5 : 13); - if (st.hint) { - lv_obj_t* h = label(row, st.hint, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); - lv_obj_set_size(h, 176, 15); // fixed height: cut, don't wrap onto the label - lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); - } + lv_obj_t* row = settingRow(body, st.label, st.hint, 176); lv_obj_t* dd = lv_dropdown_create(row); lv_dropdown_set_options(dd, opts); lv_dropdown_set_selected(dd, v); diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 3b3b4744..c65eef0d 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -235,7 +235,6 @@ public: void pinRowRefresh(); void accentRow(lv_obj_t* body); // Settings > Display & power > Accent colour void setAccent(int idx); - void showFavourites(); void showDiag(); void diagTab(int tab); void diagResetPopup(); @@ -252,7 +251,7 @@ public: // Kept for main.cpp's sim hooks (ui-new API). void openContactDM(const ContactInfo& ci) { openDM(ci.id.pub_key); } - void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)ci; (void)from_picker; } + void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)from_picker; openAdmin(ci.id.pub_key); } private: enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS }; @@ -278,6 +277,7 @@ private: void otaTick(); void otaLeave(); bool otaBusy() const; + bool wifiInUse() const; // a map download or an update holds the WiFi void buildNearby(); void refreshNearbyList(); uint32_t nearbySignature() const; @@ -344,7 +344,7 @@ private: void lockPoll(); void favGrid(lv_obj_t* grid); void favRefresh(); - int homeUnreadTotal(); + int unreadTotal(); // DMs, channels and rooms void buildDiag(); void refreshDiag(); void buildCompass(); @@ -363,8 +363,10 @@ private: void sleep(); void toggleMute(); uint32_t _next_wake_poll_ms = 0; + uint32_t _next_touch_poll_ms = 0; // tap to wake, while the screen is off bool _wake_down = false; uint32_t autoOffMillis() const; + uint32_t idleMillis(uint32_t lv_next); void checkLowBattery(); #ifdef PIN_BUZZER @@ -402,7 +404,7 @@ private: 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, for the low-battery shutdown + 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; // Screen PIN (DeviceScreen.h); stored in NVS (lvport::loadPin) diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index e38f7a9c..8a27ea36 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -175,7 +175,7 @@ void UITask::pollWifiScan() { } 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."); - if (!mapview::s_dl.active()) lvport::netEnd(); // the scan switched the radio on + if (!wifiInUse()) lvport::netEnd(); // the scan switched the radio on } void UITask::wifiPick(int idx) { diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.cpp b/variants/wio-tracker-l2/WioTrackerL2Board.cpp index 0560fa3f..5dd668f2 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.cpp +++ b/variants/wio-tracker-l2/WioTrackerL2Board.cpp @@ -14,6 +14,11 @@ static const uint8_t ADS_REG_CONFIG = 0x01; static const uint16_t ADS_CFG_BATT = 0x8000 | 0x4000 | 0x0200 | 0x0100 | 0x0080 | 0x0003; // +/-4.096V FSR -> 125uV/LSB; battery is behind a x2 divider -> 0.25 mV/LSB static const float ADS_MV_PER_LSB = 0.125f * 2.0f; +static const uint32_t BATT_SETTLE_MS = 1; // divider on -> input settled (measured: at once) +static const uint32_t BATT_REUSE_MS = 2000; // the UI, diagnostics and the app all ask + +// GT911: its command register; 0x05 = sleep +static const uint8_t GT911_ADDR = 0x5D; bool WioTrackerL2Board::expWriteReg(uint8_t reg, uint8_t val) { Wire.beginTransmission(TCA9535_ADDR); @@ -64,7 +69,7 @@ bool WioTrackerL2Board::initExpander() { delay(10); expSetOutput(EXP_PIN_TF_EN, HIGH); delay(10); - expSetOutput(EXP_PIN_BAT_ADC_EN, HIGH); + expSetOutput(EXP_PIN_BAT_ADC_EN, LOW); // the divider only draws while a reading is taken delay(10); expSetOutput(EXP_PIN_GNSS_EN, HIGH); delay(10); @@ -216,15 +221,40 @@ int16_t WioTrackerL2Board::adsReadRaw() { // A failed or zero ADS1115 read (the I2C bus busy or the ADC not answering) // keeps the last good value instead of reporting 0 mV -- that read as 0 % in // the status bar and could trip the low-battery shutdown. Retried once first. +// The divider is powered only around the reading; one taken in the last 2 s +// is reused. uint16_t WioTrackerL2Board::getBattMilliVolts() { if (!expander_ok) return 0; // BAT_ADC_EN rail never came up + if (batt_mv_last && millis() - batt_read_ms < BATT_REUSE_MS) return batt_mv_last; + expWritePin(EXP_PIN_BAT_ADC_EN, HIGH); + delay(BATT_SETTLE_MS); for (int attempt = 0; attempt < 2; attempt++) { int16_t raw = adsReadRaw(); if (raw > 0) { batt_mv_last = (uint16_t)(raw * ADS_MV_PER_LSB); - return batt_mv_last; + break; } } + expWritePin(EXP_PIN_BAT_ADC_EN, LOW); + batt_read_ms = millis(); return batt_mv_last; } + +void WioTrackerL2Board::touchSleep() { + if (!expander_ok) return; + expWritePin(EXP_PIN_TP_INT, LOW); // held low while it sleeps (it is, from the reset sequence on) + Wire.beginTransmission(GT911_ADDR); + Wire.write((uint8_t)0x80); + Wire.write((uint8_t)0x40); + Wire.write((uint8_t)0x05); + Wire.endTransmission(); +} + +void WioTrackerL2Board::touchWake() { + if (!expander_ok) return; + expWritePin(EXP_PIN_TP_INT, HIGH); // 2-5 ms high wakes it + delay(5); + expWritePin(EXP_PIN_TP_INT, LOW); +} + diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.h b/variants/wio-tracker-l2/WioTrackerL2Board.h index 60ef6e6e..6cbf3485 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.h +++ b/variants/wio-tracker-l2/WioTrackerL2Board.h @@ -20,7 +20,7 @@ #define ADS1115_ADDR 0x48 // TCA9535 pin numbering: 0..7 = port 0 (P00..P07), 8..15 = port 1 (P10..P17) -#define EXP_PIN_WAKE_BTN 0 // input - side WAKE button +#define EXP_PIN_WAKE_BTN 0 // input - WAKE button (top edge) #define EXP_PIN_I2C_IRQ 1 // input - shared I2C IRQ #define EXP_PIN_SD_DETECT 2 // input - microSD card detect #define EXP_PIN_TP_INT 3 // output - touch panel interrupt (driven for reset seq) @@ -57,8 +57,6 @@ public: // antenna away from class-D switching noise and to save battery void setSpeakerAmp(bool on) { expWritePin(EXP_PIN_PA_EN, on); } - // 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 // GNSS rail: off saves the whole receiver when GPS is disabled; on re-runs // the power-up reset so the module comes back cleanly void setGnssPower(bool on) { @@ -73,6 +71,10 @@ public: // 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); @@ -82,6 +84,11 @@ public: // WAKE button on expander P00: pressed = level differs from boot baseline bool readWakeButton(); + // GT911 touch controller: sleep (a few mA -> tens of uA) while the screen is + // off and a touch shouldn't wake it; wake with a pulse on its INT line. + void touchSleep(); + void touchWake(); + // VBUS presence via the AW35615 USB-C controller (I2C 0x22) bool isExternalPowered() override; int awRead(uint8_t reg); // AW35615 register, -1 on error @@ -95,6 +102,7 @@ private: bool aw_ok = false; // AW35615 USB-C controller responded at probe uint8_t wake_btn_baseline = 0; // idle level of P00, captured at init uint16_t batt_mv_last = 0; // last good battery reading (a failed read keeps it) + 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 diff --git a/variants/wio-tracker-l2/WioTrackerL2Display.h b/variants/wio-tracker-l2/WioTrackerL2Display.h index 65fcc2b7..5277245c 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Display.h +++ b/variants/wio-tracker-l2/WioTrackerL2Display.h @@ -33,6 +33,7 @@ class WioTrackerL2Backlight : public lgfx::v1::ILight { static constexpr uint8_t REG_LED0_PWM = 0x18; uint8_t _brightness = 153; // 60% + bool _standby = false; // chip disabled (the screen is off) void writeReg(uint8_t reg, uint8_t value) { Wire.beginTransmission(LP5814_I2C_ADDR); @@ -41,13 +42,7 @@ class WioTrackerL2Backlight : public lgfx::v1::ILight { Wire.endTransmission(); } -public: - bool init(uint8_t brightness) override { - Wire.beginTransmission(LP5814_I2C_ADDR); - if (Wire.endTransmission() != 0) { - return false; // LP5814 not found - } - + void configure() { writeReg(REG_DEVICE_CONFIG0, 0x01); // chip enable writeReg(REG_MAX_CURRENT, 0x01); // 51 mA max current writeReg(REG_ENABLE_CONTROL, 0x00); // outputs off while configuring @@ -59,12 +54,28 @@ public: writeReg(REG_ENABLE_CONTROL, 0x0F); // enable all 4 channels writeReg(REG_UPDATE, 0x55); // latch (LP5814 requires 0x55) delay(5); + } +public: + bool init(uint8_t brightness) override { + Wire.beginTransmission(LP5814_I2C_ADDR); + if (Wire.endTransmission() != 0) { + return false; // LP5814 not found + } + configure(); setBrightness(brightness); return true; } + // 0 (LovyanGFX's sleep()) puts the chip in standby; the next level brings it + // back, set up again. void setBrightness(uint8_t brightness) override { + if (brightness == 0) { + writeReg(REG_DEVICE_CONFIG0, 0x00); // chip disable: standby + _standby = true; + return; + } + if (_standby) { configure(); _standby = false; } for (uint8_t i = 0; i < 4; i++) { writeReg(REG_LED0_PWM + i, brightness); } diff --git a/variants/wio-tracker-l2/target.cpp b/variants/wio-tracker-l2/target.cpp index dd548cb3..e5ee78bb 100644 --- a/variants/wio-tracker-l2/target.cpp +++ b/variants/wio-tracker-l2/target.cpp @@ -10,7 +10,7 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); -MicroNMEALocationProvider gps(Serial1, &rtc_clock); +L2GpsProvider gps(Serial1, &rtc_clock); EnvironmentSensorManager sensors(gps); #ifdef DISPLAY_CLASS @@ -22,6 +22,28 @@ EnvironmentSensorManager sensors(gps); void buzzerAmpPower(bool on) { board.setSpeakerAmp(on); } #endif +void L2GpsProvider::begin() { + board.setGnssPower(true); // with the power-up reset + if (_uart_off) { // back from stop(): the UART again + Serial1.setRxBufferSize(1024); + Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_TX, PIN_GPS_RX); + _uart_off = false; + } + MicroNMEALocationProvider::begin(); +} + +void L2GpsProvider::stop() { + MicroNMEALocationProvider::stop(); + // The UART's TX idling high would feed the unpowered module through its pin. + Serial1.end(); + pinMode(PIN_GPS_RX, INPUT); // PIN_GPS_RX: the module's RX, our TX + _uart_off = true; + board.setGnssPower(false); +} + +void L2GpsProvider::reset() { board.gnssReset(); } +bool L2GpsProvider::isEnabled() { return board.gnssPowered(); } + bool radio_init() { MESH_DEBUG_PRINTLN("radio_init: rtc + sx1262 init"); fallback_clock.begin(); diff --git a/variants/wio-tracker-l2/target.h b/variants/wio-tracker-l2/target.h index 145c464e..f493b866 100644 --- a/variants/wio-tracker-l2/target.h +++ b/variants/wio-tracker-l2/target.h @@ -14,11 +14,24 @@ #include #endif +// The L76K's power and reset lines are on the IO expander, out of the NMEA +// provider's reach: begin() / stop() / reset() drive them here, so GPS off +// (Settings, duty cycling) really powers the receiver down. +class L2GpsProvider : public MicroNMEALocationProvider { + bool _uart_off = false; +public: + using MicroNMEALocationProvider::MicroNMEALocationProvider; + void begin() override; + void stop() override; + void reset() override; + bool isEnabled() override; +}; + extern WioTrackerL2Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; -extern MicroNMEALocationProvider gps; +extern L2GpsProvider gps; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display;