diff --git a/src/main.cpp b/src/main.cpp index 7fc557dd..37af16bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -142,7 +142,25 @@ constexpr uint32_t SILENT_REBOOT_MAGIC = 0xC1EAB007; constexpr uint32_t SILENT_REBOOT_TARGET_HOME = 0; constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1; +// How the device is coming back to life, resolved once at boot. Both resume +// flows suppress the splash and leave the panel holding its pre-boot frame; a +// plain boot shows the splash. See setup() for the resolution. +enum class BootResume : uint8_t { + Splash, // cold boot, flash, panic, or plain reboot + Silent, // heap-defrag ESP.restart() (RTC flag; lost on power loss) + QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss) +}; + +// Latched true once enterDeepSleep() commits to sleeping, before it tears down +// the current activity. WiFi activities call silentRestart() in onExit() to +// clear heap fragmentation on the way out, but deep sleep is a full chip reset +// on wake and already clears the heap, so rebooting here would just power the +// device back up against the user's sleep gesture. Never cleared: +// startDeepSleep() does not return, so a set latch only ends at the wakeup reset. +static bool deepSleepInProgress = false; + void silentRestart() { + if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot silentRebootTarget = SILENT_REBOOT_TARGET_HOME; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=home)"); @@ -156,6 +174,7 @@ void silentRestart() { } void silentRestartToReader() { + if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot silentRebootTarget = SILENT_REBOOT_TARGET_READER; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=reader)"); @@ -251,6 +270,9 @@ void enterDeepSleep(bool fromTimeout = false) { APP_STATE.saveToFile(); + // Commit to sleeping before goToSleep() runs the outgoing activity's onExit(): + // a WiFi activity would otherwise silentRestart() here and reboot instead. + deepSleepInProgress = true; activityManager.goToSleep(fromTimeout); if (isQuickResumeSleep) { @@ -400,27 +422,39 @@ 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(isSilentReboot || /*seamless=*/!APP_STATE.showBootScreen); + // Resolve the single boot-presentation decision. Skipping the splash also + // skips the panel-clearing pass and the X3 initial-full-sync arming (see + // HalDisplay::begin), so the first paint is FAST_REFRESH (~500ms) over the + // retained frame and input dispatches against a visible UI. + const BootResume resume = isSilentReboot ? BootResume::Silent + : !APP_STATE.showBootScreen ? BootResume::QuickResume + : BootResume::Splash; - // 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(); - } else if (loadSleepFrameBuffer()) { - // Seamless wake: buffer restored, replace moon icon with loading icon - const auto pageHeight = renderer.getScreenHeight(); - renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - APP_STATE.showBootScreen = true; - APP_STATE.saveToFile(); - } else { - // Frame buffer file missing — fall back to normal boot screen + setupDisplayAndFonts(resume != BootResume::Splash); + + switch (resume) { + case BootResume::Silent: + // Splash skipped: the routing block below picks the target activity; the + // panel keeps showing the pre-reboot popup until that first paint lands. + break; + case BootResume::QuickResume: + // One-shot flag: re-arm the splash for the next non-quick-resume boot. Save + // before any painting so a hang in the blocking paint path can't strand + // us in a quick-resume-with-no-frame loop on the next boot. APP_STATE.showBootScreen = true; APP_STATE.saveToFile(); + if (loadSleepFrameBuffer()) { + // Frame restored: swap the sleep moon for the loading icon. + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } else { + activityManager.goToBoot(); // frame file missing, fall back to the splash + } + break; + case BootResume::Splash: activityManager.goToBoot(); - } + break; } if (recoveryFirmwareMode) { @@ -430,9 +464,10 @@ void setup() { } else if (HalSystem::isRebootFromPanic()) { // If we rebooted from a panic, go to crash report screen to show the panic info activityManager.goToCrashReport(); - } else if (isSilentReboot && snapshotTarget == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) { + } else if (resume == BootResume::Silent && snapshotTarget == SILENT_REBOOT_TARGET_READER && + !APP_STATE.openEpubPath.empty()) { activityManager.goToReader(APP_STATE.openEpubPath); - } else if (isSilentReboot) { + } else if (resume == BootResume::Silent) { // target == home (or reader with no open book): land on home — don't fall // through to the sleep-wake "resume reader" logic, which fires on stale // openEpubPath + lastSleepFromReader from a prior session. @@ -451,7 +486,7 @@ void setup() { activityManager.goToReader(path); } - if (isSilentReboot) { + if (resume == BootResume::Silent) { // 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()