#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "CrossPointSettings.h" #include "CrossPointState.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" #include "OpdsServerStore.h" #include "RecentBooksStore.h" #include "SdCardFontSystem.h" #include "activities/Activity.h" #include "activities/ActivityManager.h" #include "activities/settings/SdFirmwareUpdateActivity.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/LoadingIcon.h" #include "util/ButtonNavigator.h" #include "util/ScreenshotUtil.h" MappedInputManager mappedInputManager(gpio); GfxRenderer renderer(display); ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; SdCardFontSystem sdFontSystem; FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts()); static unsigned long allowSleepAt = 0; // Fonts EpdFont notoserif14RegularFont(¬oserif_14_regular); EpdFont notoserif14BoldFont(¬oserif_14_bold); EpdFont notoserif14ItalicFont(¬oserif_14_italic); EpdFont notoserif14BoldItalicFont(¬oserif_14_bolditalic); EpdFontFamily notoserif14FontFamily(¬oserif14RegularFont, ¬oserif14BoldFont, ¬oserif14ItalicFont, ¬oserif14BoldItalicFont); #ifndef OMIT_FONTS EpdFont notoserif12RegularFont(¬oserif_12_regular); EpdFont notoserif12BoldFont(¬oserif_12_bold); EpdFont notoserif12ItalicFont(¬oserif_12_italic); EpdFont notoserif12BoldItalicFont(¬oserif_12_bolditalic); EpdFontFamily notoserif12FontFamily(¬oserif12RegularFont, ¬oserif12BoldFont, ¬oserif12ItalicFont, ¬oserif12BoldItalicFont); EpdFont notoserif16RegularFont(¬oserif_16_regular); EpdFont notoserif16BoldFont(¬oserif_16_bold); EpdFont notoserif16ItalicFont(¬oserif_16_italic); EpdFont notoserif16BoldItalicFont(¬oserif_16_bolditalic); EpdFontFamily notoserif16FontFamily(¬oserif16RegularFont, ¬oserif16BoldFont, ¬oserif16ItalicFont, ¬oserif16BoldItalicFont); EpdFont notoserif18RegularFont(¬oserif_18_regular); EpdFont notoserif18BoldFont(¬oserif_18_bold); EpdFont notoserif18ItalicFont(¬oserif_18_italic); EpdFont notoserif18BoldItalicFont(¬oserif_18_bolditalic); EpdFontFamily notoserif18FontFamily(¬oserif18RegularFont, ¬oserif18BoldFont, ¬oserif18ItalicFont, ¬oserif18BoldItalicFont); EpdFont notosans12RegularFont(¬osans_12_regular); EpdFont notosans12BoldFont(¬osans_12_bold); EpdFont notosans12ItalicFont(¬osans_12_italic); EpdFont notosans12BoldItalicFont(¬osans_12_bolditalic); EpdFontFamily notosans12FontFamily(¬osans12RegularFont, ¬osans12BoldFont, ¬osans12ItalicFont, ¬osans12BoldItalicFont); EpdFont notosans14RegularFont(¬osans_14_regular); EpdFont notosans14BoldFont(¬osans_14_bold); EpdFont notosans14ItalicFont(¬osans_14_italic); EpdFont notosans14BoldItalicFont(¬osans_14_bolditalic); EpdFontFamily notosans14FontFamily(¬osans14RegularFont, ¬osans14BoldFont, ¬osans14ItalicFont, ¬osans14BoldItalicFont); EpdFont notosans16RegularFont(¬osans_16_regular); EpdFont notosans16BoldFont(¬osans_16_bold); EpdFont notosans16ItalicFont(¬osans_16_italic); EpdFont notosans16BoldItalicFont(¬osans_16_bolditalic); EpdFontFamily notosans16FontFamily(¬osans16RegularFont, ¬osans16BoldFont, ¬osans16ItalicFont, ¬osans16BoldItalicFont); EpdFont notosans18RegularFont(¬osans_18_regular); EpdFont notosans18BoldFont(¬osans_18_bold); EpdFont notosans18ItalicFont(¬osans_18_italic); EpdFont notosans18BoldItalicFont(¬osans_18_bolditalic); EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, ¬osans18ItalicFont, ¬osans18BoldItalicFont); EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular); EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold); EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic); EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic); EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, &opendyslexic8BoldFont, &opendyslexic8ItalicFont, &opendyslexic8BoldItalicFont); EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular); EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold); EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic); EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic); EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, &opendyslexic10BoldFont, &opendyslexic10ItalicFont, &opendyslexic10BoldItalicFont); EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular); EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold); EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic); EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic); EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, &opendyslexic12BoldFont, &opendyslexic12ItalicFont, &opendyslexic12BoldItalicFont); EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular); EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold); EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic); EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic); EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, &opendyslexic14BoldFont, &opendyslexic14ItalicFont, &opendyslexic14BoldItalicFont); #endif // OMIT_FONTS EpdFont smallFont(¬osans_8_regular); EpdFontFamily smallFontFamily(&smallFont); EpdFont ui10RegularFont(&ubuntu_10_regular); EpdFont ui10BoldFont(&ubuntu_10_bold); EpdFontFamily ui10FontFamily(&ui10RegularFont, &ui10BoldFont); EpdFont ui12RegularFont(&ubuntu_12_regular); EpdFont ui12BoldFont(&ubuntu_12_bold); EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont); // measurement of power button press duration calibration value unsigned long t1 = 0; unsigned long t2 = 0; // Definitions for SilentRestart.h. RTC_NOINIT survives ESP.restart() but not power loss. RTC_NOINIT_ATTR uint32_t silentRebootMagic; RTC_NOINIT_ATTR uint32_t silentRebootTarget; 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)"); // 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(); } 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)"); GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); delay(50); ESP.restart(); } // Verify power button press duration on wake-up from deep sleep // Pre-condition: isWakeupByPowerButton() == true void verifyPowerButtonDuration() { if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) { // Fast path for short press // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state return; } // Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration() const auto start = millis(); bool abort = false; // Subtract the current time, because inputManager only starts counting the HeldTime from the first update() // This way, we remove the time we already took to reach here from the duration, // assuming the button was held until now from millis()==0 (i.e. device start time). const uint16_t calibration = start; const uint16_t calibratedPressDuration = (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; gpio.update(); // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) { delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. gpio.update(); } t2 = millis(); if (gpio.isPressed(HalGPIO::BTN_POWER)) { do { delay(10); gpio.update(); } while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() < calibratedPressDuration); abort = gpio.getPowerButtonHeldTime() < calibratedPressDuration; } else { abort = true; } if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again powerManager.startDeepSleep(gpio); } } void waitForPowerRelease() { gpio.update(); while (gpio.isPressed(HalGPIO::BTN_POWER)) { delay(50); gpio.update(); } } constexpr char SLEEP_FRAME_FILE[] = "/.crosspoint/sleep_frame.bin"; static void saveSleepFrameBuffer() { FsFile file; if (!Storage.openFileForWrite("SLP", SLEEP_FRAME_FILE, file)) return; file.write(renderer.getFrameBuffer(), renderer.getBufferSize()); file.close(); } static bool loadSleepFrameBuffer() { FsFile file; if (!Storage.openFileForRead("SLP", SLEEP_FRAME_FILE, file)) return false; const size_t bufferSize = display.getBufferSize(); const size_t bytesRead = file.read(display.getFrameBuffer(), bufferSize); file.close(); if (bytesRead != bufferSize) { Storage.remove(SLEEP_FRAME_FILE); return false; } Storage.remove(SLEEP_FRAME_FILE); return true; } // Enter deep sleep mode void enterDeepSleep(bool fromTimeout = false) { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); const bool isQuickResumeSleep = SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME || (fromTimeout && SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT); APP_STATE.showBootScreen = !isQuickResumeSleep; 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) { saveSleepFrameBuffer(); } // Tear down WiFi so the modem power domain isn't held alive across deep sleep. // Wake from deep sleep is effectively a chip reset, so no state needs to survive. if (WiFi.getMode() != WIFI_MODE_NULL) { WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } halTiltSensor.deepSleep(); display.deepSleep(); LOG_DBG("MAIN", "Entering deep sleep"); powerManager.startDeepSleep(gpio); } void setupDisplayAndFonts(bool seamless = false) { display.begin(seamless); renderer.begin(); activityManager.begin(); LOG_DBG("MAIN", "Display initialized"); // Initialize font decompressor for compressed reader fonts if (!fontDecompressor.init()) { LOG_ERR("MAIN", "Font decompressor init failed"); } fontCacheManager.setFontDecompressor(&fontDecompressor); renderer.setFontCacheManager(&fontCacheManager); renderer.insertFont(NOTOSERIF_14_FONT_ID, notoserif14FontFamily); #ifndef OMIT_FONTS renderer.insertFont(NOTOSERIF_12_FONT_ID, notoserif12FontFamily); renderer.insertFont(NOTOSERIF_16_FONT_ID, notoserif16FontFamily); renderer.insertFont(NOTOSERIF_18_FONT_ID, notoserif18FontFamily); renderer.insertFont(NOTOSANS_12_FONT_ID, notosans12FontFamily); renderer.insertFont(NOTOSANS_14_FONT_ID, notosans14FontFamily); renderer.insertFont(NOTOSANS_16_FONT_ID, notosans16FontFamily); renderer.insertFont(NOTOSANS_18_FONT_ID, notosans18FontFamily); renderer.insertFont(OPENDYSLEXIC_8_FONT_ID, opendyslexic8FontFamily); renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily); renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily); renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily); #endif // OMIT_FONTS renderer.insertFont(UI_10_FONT_ID, ui10FontFamily); renderer.insertFont(UI_12_FONT_ID, ui12FontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily); // Discover and load SD card fonts sdFontSystem.begin(renderer); LOG_DBG("MAIN", "Fonts setup"); } void setup() { t1 = millis(); #ifdef ENABLE_SERIAL_LOG // Earliest possible Serial setup. The 250 ms stall before begin() lets the // USB Serial/JTAG peripheral finish power-on and lets the host complete USB // enumeration before we touch the CDC state — otherwise cold boot races // and the host has to be physically replugged for logs to flow. Warm reboot // worked without the delay because USB was already enumerated. delay(250); Serial.begin(115200); logSerial.setTxTimeoutMs(1); // This is a load-bearing 1. Do not modify. #endif HalSystem::begin(); // Read-and-clear so a panic later in setup() doesn't loop into silent reboot. // Bound the target range too — RTC_NOINIT memory is uninitialized on cold boot. const bool isSilentReboot = (silentRebootMagic == SILENT_REBOOT_MAGIC); const uint32_t snapshotTarget = (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; silentRebootMagic = 0; silentRebootTarget = 0; gpio.begin(); powerManager.begin(); halTiltSensor.begin(); halClock.begin(); LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4"); // SD Card Initialization // We need 6 open files concurrently when parsing a new chapter if (!Storage.begin()) { LOG_ERR("MAIN", "SD card initialization failed"); setupDisplayAndFonts(isSilentReboot); activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); return; } HalSystem::checkPanic(); SETTINGS.loadFromFile(); APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile(); I18N.setLanguage(static_cast(SETTINGS.language)); KOREADER_STORE.loadFromFile(); OPDS_STORE.loadFromFile(); UITheme::getInstance().reload(); ButtonNavigator::setMappedInputManager(mappedInputManager); const auto wakeupReason = gpio.getWakeupReason(); switch (wakeupReason) { case HalGPIO::WakeupReason::PowerButton: LOG_DBG("MAIN", "Verifying power button press duration"); gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(), SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP); break; case HalGPIO::WakeupReason::AfterUSBPower: // If USB power caused a cold boot, go back to sleep LOG_DBG("MAIN", "Wakeup reason: After USB Power"); powerManager.startDeepSleep(gpio); break; case HalGPIO::WakeupReason::AfterFlash: // After flashing, just proceed to boot case HalGPIO::WakeupReason::Other: default: break; } // Recovery firmware mode: hold left side button (BTN_UP) together with the power button at // boot to skip directly to the SD-card firmware update screen. Useful on devices where USB // flashing has been locked down (e.g. recent X3 firmware). bool recoveryFirmwareMode = false; if (wakeupReason == HalGPIO::WakeupReason::PowerButton) { // Refresh the cached button state a few times — isPressed() needs ~half a second to settle // after boot per the HalGPIO contract. Use a millis-based deadline so we always wait the full // settle window even if the loop body takes longer than expected on slow boots. const unsigned long settleStart = millis(); while (millis() - settleStart < 500) { gpio.update(); delay(10); } if (gpio.isPressed(HalGPIO::BTN_UP)) { recoveryFirmwareMode = true; LOG_INF("MAIN", "Recovery firmware mode (UP + POWER held at boot)"); } } // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION); // 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; 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) { // Skip normal home/reader routing: jump straight into the SD firmware picker. activityManager.replaceActivity( std::make_unique(renderer, mappedInputManager, /*recoveryMode=*/true)); } else if (HalSystem::isRebootFromPanic()) { // If we rebooted from a panic, go to crash report screen to show the panic info activityManager.goToCrashReport(); } else if (resume == BootResume::Silent && snapshotTarget == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) { activityManager.goToReader(APP_STATE.openEpubPath); } 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. activityManager.goHome(); } else if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity // crashed (indicated by readerActivityLoadCount > 0) activityManager.goHome(); } else { // Clear app state to avoid getting into a boot loop if the epub doesn't load const auto path = APP_STATE.openEpubPath; APP_STATE.openEpubPath = ""; APP_STATE.readerActivityLoadCount++; APP_STATE.saveToFile(); activityManager.goToReader(path); } 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() // 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; } void loop() { static unsigned long maxLoopDuration = 0; const unsigned long loopStartTime = millis(); static unsigned long lastMemPrint = 0; gpio.update(); halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity()); renderer.setFadingFix(SETTINGS.fadingFix); if (Serial && millis() - lastMemPrint >= 10000) { LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); lastMemPrint = millis(); } // Handle incoming serial commands, // nb: we use logSerial from logging to avoid deprecation warnings if (logSerial.available() > 0) { String line = logSerial.readStringUntil('\n'); if (line.startsWith("CMD:")) { String cmd = line.substring(4); cmd.trim(); if (cmd == "SCREENSHOT") { const uint32_t bufferSize = display.getBufferSize(); logSerial.printf("SCREENSHOT_START:%d\n", bufferSize); uint8_t* buf = display.getFrameBuffer(); logSerial.write(buf, bufferSize); logSerial.printf("SCREENSHOT_END\n"); } } } // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() || activityManager.preventAutoSleep()) { lastActivityTime = millis(); // Reset inactivity timer powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity } static bool screenshotButtonsReleased = true; static bool screenshotComboActive = false; if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { screenshotComboActive = true; if (screenshotButtonsReleased) { screenshotButtonsReleased = false; { RenderLock lock; ScreenshotUtil::takeScreenshot(renderer); } } return; } if (screenshotComboActive) { if (gpio.isPressed(HalGPIO::BTN_POWER)) return; if (gpio.wasReleased(HalGPIO::BTN_POWER)) { screenshotButtonsReleased = true; screenshotComboActive = false; return; } screenshotButtonsReleased = true; screenshotComboActive = false; } const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (millis() - lastActivityTime >= sleepTimeoutMs) { LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs); enterDeepSleep(true); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } if (millis() >= allowSleepAt && gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() > SETTINGS.getPowerButtonDuration()) { // If the screenshot combination is potentially being pressed, don't sleep if (gpio.isPressed(HalGPIO::BTN_DOWN)) { return; } enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } // Refresh screen when power button is short-pressed with FORCE_REFRESH setting. if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH && mappedInputManager.wasReleased(MappedInputManager::Button::Power)) { LOG_DBG("MAIN", "Manual screen refresh triggered"); RenderLock lock; renderer.displayBuffer(HalDisplay::HALF_REFRESH); } // Refresh the battery icon when USB is plugged or unplugged. // Placed after sleep guards so we never queue a render that won't be processed. if (gpio.wasUsbStateChanged()) { activityManager.requestUpdate(); } const unsigned long activityStartTime = millis(); activityManager.loop(); const unsigned long activityDuration = millis() - activityStartTime; const unsigned long loopDuration = millis() - loopStartTime; if (loopDuration > maxLoopDuration) { maxLoopDuration = loopDuration; if (maxLoopDuration > 50) { LOG_DBG("LOOP", "New max loop duration: %lu ms (activity: %lu ms)", maxLoopDuration, activityDuration); } } // Add delay at the end of the loop to prevent tight spinning // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response // Otherwise, use longer delay to save power if (activityManager.skipLoopDelay()) { powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else { if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) { // If we've been inactive for a while, increase the delay to save power powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity delay(50); } else { // Short delay to prevent tight loop while still being responsive delay(10); } } }