From 638eea7bb0d0050a7379978a45f54e532992f4b7 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:40:14 +0200 Subject: [PATCH] feat(ui): full e-ink refresh on screen change + prune gone favourites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two UI quick wins from the 2026-07-05 review: - E-ink: force a full (non-partial) refresh on the first frame of a new screen. Inter-screen ghosting was the most visible cheap win — the N-partials interval alone doesn't catch navigation. New DisplayDriver::forceFullRefresh() (no-op on OLED), set in setCurrScreen() and consumed by GxEPDDisplay::endFrame(). - Favourites: clear a stale "(gone)" tile at render time so it reverts to an empty "+" slot. Happens when prefs outlive the contact list (e.g. a wiped /contacts3); onContactRemoved only catches a live delete. Pruned slots are persisted once per pass (self-healing — an emptied slot can't re-fire). Builds green: WioTrackerL1_companion_solo_dual (OLED), WioTrackerL1Eink_companion_solo_dual (e-ink). Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 25 ++++++++++++++++++++-- src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/GxEPDDisplay.cpp | 6 +++++- src/helpers/ui/GxEPDDisplay.h | 2 ++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 19bca57b..2ab94163 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1192,6 +1192,7 @@ public: if (_fav_sel >= NodePrefs::FAVOURITES_COUNT) _fav_sel = 0; + bool fav_changed = false; // a stale (gone) slot was pruned this pass → persist once after the loop for (uint8_t i = 0; i < NodePrefs::FAVOURITES_COUNT; i++) { int row = i / cols; int col = i % cols; @@ -1218,9 +1219,21 @@ public: ci = c; found = true; break; } } + if (!found) { + // Pinned contact is gone — prefs outlived the contact list (e.g. a + // wiped /contacts3; onContactRemoved only catches a live delete). + // Clear the stale slot at render time so it reverts to an empty "+" + // tile instead of showing "(gone)". Persisted once after the loop. + if (_node_prefs) + memset(_node_prefs->favourite_contacts[i], 0, NodePrefs::FAVOURITE_PREFIX_LEN); + fav_changed = true; + int plus_y = cy + (cell_h - line_h) / 2; + display.drawTextCentered(cx + cell_w / 2, plus_y, "+"); + if (sel) display.setColor(DisplayDriver::LIGHT); + continue; + } char name[24]; - if (found) display.translateUTF8ToBlocks(name, ci.name, sizeof(name)); - else strncpy(name, "(gone)", sizeof(name) - 1), name[sizeof(name) - 1] = '\0'; + display.translateUTF8ToBlocks(name, ci.name, sizeof(name)); // Reserve space for the unread badge so the name's ellipsis lands // before it instead of underneath. Badge and name share one baseline. @@ -1247,6 +1260,10 @@ public: } if (sel) display.setColor(DisplayDriver::LIGHT); } + // Persist any pruned slots once, outside the loop — a render pass can clear + // several gone tiles but only one flash write is needed. Self-healing: once + // cleared, the slot is empty next frame so this can't re-fire per frame. + if (fav_changed) the_mesh.savePrefs(); if (_pin_menu.active) _pin_menu.render(display); } else if (_page == HomePage::SHUTDOWN) { display.setColor(DisplayDriver::LIGHT); @@ -1910,6 +1927,10 @@ void UITask::setCurrScreen(UIScreen* c) { // that mistake is an inert no-op instead of a null deref in render()/poll(). if (!c) return; curr = c; + // E-ink: force a full refresh on the first frame of the new screen so leftover + // ghosting from the previous screen is cleared (the N-partials interval alone + // doesn't catch navigation). No-op on OLED. + if (_display) _display->forceFullRefresh(); c->onShow(); // central per-visit reset hook (see UIScreen::onShow) _next_refresh = 100; } diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 47220db1..b57f818d 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -272,6 +272,7 @@ public: virtual void setBrightness(uint8_t level) { } // level 0-4 (min to max), no-op default virtual void setDisplayRotation(uint8_t rot) { } // 0-3, no-op for fixed-orientation displays virtual void setFullRefreshInterval(uint8_t n) { } // e-ink: do full refresh every n partial refreshes (0=never) + virtual void forceFullRefresh() { } // e-ink: make the next frame a full (non-partial) refresh — clears ghosting on screen change; no-op on OLED virtual void endFrame() = 0; #ifdef ENABLE_SCREENSHOT diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 5e45ac4a..fa53bf01 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -275,10 +275,14 @@ void GxEPDDisplay::endFrame() { uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { bool partial = true; - if (_full_refresh_interval > 0 && ++_partial_count >= _full_refresh_interval) { + // A forced full refresh (e.g. screen change) clears inter-screen ghosting. + // Short-circuit so _partial_count isn't bumped on the forced pass; the block + // resets it either way, matching the interval-driven full refresh. + if (_force_full || (_full_refresh_interval > 0 && ++_partial_count >= _full_refresh_interval)) { partial = false; _partial_count = 0; } + _force_full = false; display.display(partial); last_display_crc_value = crc; } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c8837f48..1372f540 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -64,6 +64,7 @@ class GxEPDDisplay : public DisplayDriver { int _text_sz = 1; uint8_t _full_refresh_interval = 0; uint8_t _partial_count = 0; + bool _force_full = false; // set by forceFullRefresh(); consumed by the next endFrame() int16_t drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc); uint8_t lemonXAdvance(uint32_t cp, int sc); @@ -144,5 +145,6 @@ public: } void setDisplayRotation(uint8_t rot) override; void setFullRefreshInterval(uint8_t n) override { _full_refresh_interval = n; _partial_count = 0; } + void forceFullRefresh() override { _force_full = true; } void endFrame() override; };