From 3800179595ce673e8c86da5e3dd0b1bfaa2d2813 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 20 May 2026 05:58:43 -0700 Subject: [PATCH] fix: wire through silent restart clear resume state with sdk (#2033) ## Summary After a silent reboot, there was a small window where the esp32 would listen for button presses but the full refresh would hold the event loop. This gave a UX experience where the silent reboot had completed to the home screen, a user taps select (at any time during the process), and they find themselves unexpectedly in a book. ## Additional Context This must land after https://github.com/crosspoint-reader/community-sdk/pull/11 and will need the submodule SHA changes included in. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? partially --- lib/hal/HalDisplay.cpp | 18 +++++++++++------- lib/hal/HalDisplay.h | 7 ++++++- open-x4-sdk | 2 +- src/main.cpp | 32 ++++++++++++++++++++++++++++---- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index e531f1da..a528db9f 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -18,14 +18,18 @@ void HalDisplay::begin(bool seamless) { einkDisplay.begin(); + if (seamless) { + // Defuse the SDK's X3 _x3InitialFullSyncsRemaining counter (no-op on X4) + // so the first paint isn't promoted to FULL (~770ms). Skips the wakeup- + // gated requestResync() below for the same reason. + einkDisplay.skipInitialResync(); + return; + } // Request resync after specific wakeup events to ensure clean display state. - // Skip when seamless=true so the current screen content is preserved. - if (!seamless) { - const auto wakeupReason = gpio.getWakeupReason(); - if (wakeupReason == HalGPIO::WakeupReason::PowerButton || wakeupReason == HalGPIO::WakeupReason::AfterFlash || - wakeupReason == HalGPIO::WakeupReason::Other) { - einkDisplay.requestResync(); - } + const auto wakeupReason = gpio.getWakeupReason(); + if (wakeupReason == HalGPIO::WakeupReason::PowerButton || wakeupReason == HalGPIO::WakeupReason::AfterFlash || + wakeupReason == HalGPIO::WakeupReason::Other) { + einkDisplay.requestResync(); } } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 4d51bdda..3d43126a 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -17,7 +17,12 @@ class HalDisplay { FAST_REFRESH // Fast refresh using custom LUT }; - // Initialize the display hardware and driver + // Pass seamless=true on any path where the panel already shows the + // content it should after begin() returns (silent reboot's popup, + // sleep-wake with a restored buffer). Skips the wakeup-gated + // requestResync() and defuses the SDK's X3 _x3InitialFullSyncsRemaining + // counter; otherwise the first two paints get promoted to FULL + // (~770ms each on X3). void begin(bool seamless = false); // Display dimensions diff --git a/open-x4-sdk b/open-x4-sdk index 4d93e09f..ff444b0f 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 4d93e09f73a8cccfd63898c11086841eba5df362 +Subproject commit ff444b0f2c9211341e8a4a6c9d8d9c6bac03f3e8 diff --git a/src/main.cpp b/src/main.cpp index 6ee382a4..2a93a1cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -146,6 +146,11 @@ void silentRestart() { silentRebootTarget = SILENT_REBOOT_TARGET_HOME; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=home)"); + // E-ink retains the previous frame until Home's first paint lands (~2-3s). + // Without an overlay, users don't see the reboot and fire input through to + // Home. Select on the default selectorIndex=0 then opens the most-recent + // book, looking like a trampoline back to the reader they just exited. + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); delay(50); ESP.restart(); } @@ -154,6 +159,7 @@ void silentRestartToReader() { silentRebootTarget = SILENT_REBOOT_TARGET_READER; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=reader)"); + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); delay(50); ESP.restart(); } @@ -336,7 +342,7 @@ void setup() { // We need 6 open files concurrently when parsing a new chapter if (!Storage.begin()) { LOG_ERR("MAIN", "SD card initialization failed"); - setupDisplayAndFonts(); + setupDisplayAndFonts(isSilentReboot); activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); return; } @@ -393,10 +399,11 @@ void setup() { // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION); - setupDisplayAndFonts(/*seamless=*/!APP_STATE.showBootScreen); + setupDisplayAndFonts(isSilentReboot || /*seamless=*/!APP_STATE.showBootScreen); - // First paint after silent reboot is HALF_REFRESH (SDK forces it after begin()'s - // panel reset); subsequent paints FAST. + // Silent reboot suppresses the boot splash and the X3 initial-full-sync + // arming (see HalDisplay::begin), so the first Home paint is FAST_REFRESH + // (~500ms) and input dispatches against the visible menu. if (!isSilentReboot) { if (APP_STATE.showBootScreen) { activityManager.goToBoot(); @@ -443,6 +450,23 @@ void setup() { activityManager.goToReader(path); } + if (isSilentReboot) { + // Block until the first paint physically completes. refreshDisplay() + // waits on the panel BUSY pin so when this returns the user can see the + // new activity. Without the wait, an edge captured by gpio.update() + // during boot dispatches against an invisible Home and the default + // selectorIndex=0 opens the most-recent book. + activityManager.requestUpdateAndWait(); + // Absorb any button held at this point into currentState as a non-edge: + // two gpio.update() calls separated by > InputManager's 5ms debounce + // transition the held bit through lastDebounceTime into currentState + // without setting pressedEvents, so the first loop()'s own gpio.update() + // sees state == currentState and emits nothing. + gpio.update(); + delay(10); + gpio.update(); + } + // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); allowSleepAt = millis() + 2000;