#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ButtonEventManager.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "GlobalBookmarkIndex.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" #include "OpdsServerStore.h" #include "RecentBooksStore.h" #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "WeatherSettingsStore.h" #include "activities/Activity.h" #include "activities/ActivityManager.h" #include "activities/settings/SdFirmwareUpdateActivity.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/ButtonNavigator.h" #include "util/ScreenshotUtil.h" MappedInputManager mappedInputManager(gpio); ButtonEventManager buttonEventManager(mappedInputManager); ButtonEventManager& globalButtonEvents() { return buttonEventManager; } GfxRenderer renderer(display); ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; SdCardFontSystem sdFontSystem; FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts()); // Fonts EpdFont bookerly14RegularFont(&bookerly_14_regular); EpdFont bookerly14BoldFont(&bookerly_14_bold); EpdFont bookerly14ItalicFont(&bookerly_14_italic); EpdFont bookerly14BoldItalicFont(&bookerly_14_bolditalic); EpdFontFamily bookerly14FontFamily(&bookerly14RegularFont, &bookerly14BoldFont, &bookerly14ItalicFont, &bookerly14BoldItalicFont); #ifndef OMIT_FONTS EpdFont bookerly10RegularFont(&bookerly_10_regular); EpdFont bookerly10BoldFont(&bookerly_10_bold); EpdFont bookerly10ItalicFont(&bookerly_10_italic); EpdFont bookerly10BoldItalicFont(&bookerly_10_bolditalic); EpdFontFamily bookerly10FontFamily(&bookerly10RegularFont, &bookerly10BoldFont, &bookerly10ItalicFont, &bookerly10BoldItalicFont); EpdFont bookerly12RegularFont(&bookerly_12_regular); EpdFont bookerly12BoldFont(&bookerly_12_bold); EpdFont bookerly12ItalicFont(&bookerly_12_italic); EpdFont bookerly12BoldItalicFont(&bookerly_12_bolditalic); EpdFontFamily bookerly12FontFamily(&bookerly12RegularFont, &bookerly12BoldFont, &bookerly12ItalicFont, &bookerly12BoldItalicFont); EpdFont bookerly16RegularFont(&bookerly_16_regular); EpdFont bookerly16BoldFont(&bookerly_16_bold); EpdFont bookerly16ItalicFont(&bookerly_16_italic); EpdFont bookerly16BoldItalicFont(&bookerly_16_bolditalic); EpdFontFamily bookerly16FontFamily(&bookerly16RegularFont, &bookerly16BoldFont, &bookerly16ItalicFont, &bookerly16BoldItalicFont); EpdFont bookerly18RegularFont(&bookerly_18_regular); EpdFont bookerly18BoldFont(&bookerly_18_bold); EpdFont bookerly18ItalicFont(&bookerly_18_italic); EpdFont bookerly18BoldItalicFont(&bookerly_18_bolditalic); EpdFontFamily bookerly18FontFamily(&bookerly18RegularFont, &bookerly18BoldFont, &bookerly18ItalicFont, &bookerly18BoldItalicFont); EpdFont notosans10RegularFont(¬osans_10_regular); EpdFont notosans10BoldFont(¬osans_10_bold); EpdFont notosans10ItalicFont(¬osans_10_italic); EpdFont notosans10BoldItalicFont(¬osans_10_bolditalic); EpdFontFamily notosans10FontFamily(¬osans10RegularFont, ¬osans10BoldFont, ¬osans10ItalicFont, ¬osans10BoldItalicFont); 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); #endif // OMIT_FONTS EpdFont smallFont(¬osans_8_regular); EpdFontFamily smallFontFamily(&smallFont); EpdFont ui10RegularFont(&inter_ui_10_regular); EpdFont ui10BoldFont(&inter_ui_10_bold); EpdFontFamily ui10FontFamily(&ui10RegularFont, &ui10BoldFont); EpdFont ui12RegularFont(&inter_ui_12_regular); EpdFont ui12BoldFont(&inter_ui_12_bold); EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont); // SilentRestart.h definitions. 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; void silentRestart() { silentRebootTarget = SILENT_REBOOT_TARGET_HOME; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=home)"); delay(50); ESP.restart(); } void silentRestartToReader() { silentRebootTarget = SILENT_REBOOT_TARGET_READER; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=reader)"); delay(50); ESP.restart(); } // Enter deep sleep mode void enterDeepSleep() { LOG_DBG("MAIN", "enterDeepSleep called at millis=%lu, powerBtn isPressed=%d, rawPin=%d", millis(), gpio.isPressed(HalGPIO::BTN_POWER), digitalRead(InputManager::POWER_BUTTON_PIN) == LOW); HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); // On X3 the DS3231 keeps time independently, so there's no need to keep the MCU // powered during deep sleep for LP timer preservation. const bool keepLpAlive = SETTINGS.useClock && !gpio.deviceIsX3(); HalClock::saveBeforeSleep(keepLpAlive); // If sleeping from a running reader the book loaded successfully, so the boot-loop // guard count is no longer needed. Reset it now because onExit() is never called // on the reader activity during a sleep transition (only queued as a pending action). if (APP_STATE.lastSleepFromReader) { APP_STATE.readerActivityLoadCount = 0; } APP_STATE.saveToFile(); activityManager.goToSleep(); halTiltSensor.deepSleep(); display.deepSleep(); LOG_DBG("MAIN", "Entering deep sleep (powerBtn isPressed=%d, rawPin=%d)", gpio.isPressed(HalGPIO::BTN_POWER), digitalRead(InputManager::POWER_BUTTON_PIN) == LOW); powerManager.startDeepSleep(gpio, keepLpAlive); } void setupDisplayAndFonts() { display.begin(); 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(BOOKERLY_14_FONT_ID, bookerly14FontFamily); #ifndef OMIT_FONTS renderer.insertFont(BOOKERLY_10_FONT_ID, bookerly10FontFamily); renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily); renderer.insertFont(BOOKERLY_16_FONT_ID, bookerly16FontFamily); renderer.insertFont(BOOKERLY_18_FONT_ID, bookerly18FontFamily); renderer.insertFont(NOTOSANS_10_FONT_ID, notosans10FontFamily); 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); #endif // OMIT_FONTS renderer.insertFont(UI_10_FONT_ID, ui10FontFamily); renderer.insertFont(UI_12_FONT_ID, ui12FontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily); // Discover SD card fonts (under /.crosspoint/fonts/) and load the family // currently selected in settings (if any). Safe to call without an SD card. sdFontSystem.begin(renderer); LOG_DBG("MAIN", "Fonts setup"); } // Defined here to satisfy SdCardFontGlobals.h's extern declaration. Keeps // activity-side callers out of SdCardFontSystem internals. void ensureSdFontLoaded() { sdFontSystem.ensureLoaded(renderer); } void ensureSdFontLoadedForPath(const char* path) { if (!path) { ensureSdFontLoaded(); return; } const std::string_view filePath(path); const bool isTxtMd = static_cast(FsHelpers::hasTxtExtension)(filePath) || static_cast(FsHelpers::hasMarkdownExtension)(filePath); if (isTxtMd) { // TXT/MD has no per-book SD font override — use global settings directly. sdFontSystem.ensureLoaded(renderer, SETTINGS.txtSdFontFamilyName, SETTINGS.txtFontSize); return; } // For EPUB: honour per-book SD font and/or size overrides. The book record // is available here — the reader activity hasn't started yet, but // RecentBooksStore already has the persisted overrides for this path. const RecentBook book = RECENT_BOOKS.getBookByPath(path); const uint8_t effectiveSize = (book.fontSizeOverride >= 0) ? static_cast(book.fontSizeOverride) : SETTINGS.fontSize; if (!book.sdFontFamilyOverride.empty()) { // Per-book SD font override: load that family at the effective size. sdFontSystem.ensureLoaded(renderer, book.sdFontFamilyOverride.c_str(), effectiveSize); } else if (book.fontFamilyOverride >= 0) { // Per-book built-in font override: no SD font needed; unload if one was active. sdFontSystem.ensureLoaded(renderer, "", effectiveSize); } else { // No family override: use global SD font (if any) at the effective size. sdFontSystem.ensureLoaded(renderer, SETTINGS.sdFontFamilyName, effectiveSize); } } void setup() { { esp_ota_img_states_t otaState; const esp_partition_t* running = esp_ota_get_running_partition(); if (esp_ota_get_state_partition(running, &otaState) == ESP_OK && otaState == ESP_OTA_IMG_PENDING_VERIFY) { esp_ota_mark_app_valid_cancel_rollback(); } } // 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 silentRebootTargetSnapshot = (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; silentRebootMagic = 0; silentRebootTarget = 0; HalSystem::begin(); gpio.begin(); powerManager.begin(); halTiltSensor.begin(); gpio_deep_sleep_hold_dis(); // Release deep sleep GPIO hold state from previous sleep cycle const auto wakeupReason = gpio.getWakeupReason(); if (wakeupReason == HalGPIO::WakeupReason::AfterUSBPower) { // If USB power caused a cold boot, go back to sleep immediately without initializing subsystems LOG_DBG("MAIN", "Wakeup reason: After USB Power => Deep sleep"); halTiltSensor.deepSleep(); powerManager.startDeepSleep(gpio); return; } #ifdef ENABLE_SERIAL_LOG if (gpio.isUsbConnected()) { Serial.begin(115200); const unsigned long start = millis(); while (!Serial && (millis() - start) < 500) { delay(10); } } #endif LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4"); LOG_DBG("MAIN", "Wakeup reason: %d, millis=%lu, rawPowerPin=%d", static_cast(wakeupReason), millis(), digitalRead(InputManager::POWER_BUTTON_PIN) == LOW); // Load just the settings we need *before* initializing the SD card to speed up and reduce power on unverified wakes SETTINGS.loadStartupFromNvs(); if (wakeupReason == HalGPIO::WakeupReason::PowerButton) { LOG_DBG("MAIN", "Verifying power button press duration (required=%u ms)", CrossPointSettings::getPowerButtonDuration()); // We only want to skip the hold verification (allowing a short press to wake) if the short // press or double press actually have an action assigned, or if the clock screensaver is active. // Otherwise, short presses from sleep should be ignored entirely and return to sleep. // // X3 always cuts all power during sleep (battery-latch MOSFET, keepClockAlive=false), so any // wakeup is a full cold boot. By the time verifyPowerButtonWakeup runs the button may already // be released — hardware design guarantees the press was intentional, so skip hold-verification. bool allowShortPress = gpio.deviceIsX3() || (SETTINGS.useClock != 0) || (SETTINGS.btnShortPower != CrossPointSettings::BTN_DEFAULT) || (SETTINGS.btnDoublePower != CrossPointSettings::BTN_DEFAULT); gpio.verifyPowerButtonWakeup(CrossPointSettings::getPowerButtonDuration(), allowShortPress); LOG_DBG("MAIN", "Power button verification passed, millis=%lu", millis()); } // 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)"); } } // 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(); activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); return; } SETTINGS.loadFromFile(); HalSystem::checkPanic(); HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info HalClock::applyTimezone(SETTINGS.timeZone); I18N.loadSettings(); KOREADER_STORE.loadFromFile(); OPDS_STORE.loadFromFile(); WEATHER_SETTINGS.loadFromFile(); UITheme::getInstance().reload(); ButtonNavigator::setMappedInputManager(mappedInputManager); // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION); setupDisplayAndFonts(); if (!isSilentReboot) { activityManager.goToBoot(); } else { // After a silent reboot the panel still shows the previous session's pixels but // the SDK's RED-RAM diff buffer was cleared by begin(). A FAST refresh would only // flip pixels the SDK *thinks* changed, leaving the old screen visible. Force the // first paint to HALF_REFRESH so the panel cleanly repaints; subsequent paints // resume FAST as normal. renderer.setNextDisplayRefreshMode(HalDisplay::HALF_REFRESH); } APP_STATE.loadFromFile(); HalClock::restore(); RECENT_BOOKS.loadFromFile(); GLOBAL_BOOKMARKS.load(); 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 (isSilentReboot && silentRebootTargetSnapshot == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) { activityManager.goToReader(APP_STATE.openEpubPath); } else if (isSilentReboot) { // 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) { 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); } // Ensure we're not still holding the power button before leaving setup. // waitForStablePowerRelease protects against switch bounce that might register as a false double-press. // Skip on silent reboot: the firmware triggered the restart, so the button isn't held. if (!isSilentReboot) { gpio.waitForStablePowerRelease(); } // Flush any pin state transitions that occurred during boot before entering the main loop mappedInputManager.update(); buttonEventManager.drain(); } void loop() { static unsigned long maxLoopDuration = 0; const unsigned long loopStartTime = millis(); static unsigned long lastMemPrint = 0; gpio.update(); buttonEventManager.update(); HalClock::updatePeriodic(); halTiltSensor.update(static_cast(SETTINGS.tiltPageTurn), static_cast(SETTINGS.orientation), activityManager.isReaderActivity()); renderer.setFadingFix(SETTINGS.fadingFix); renderer.setTextDarkness(SETTINGS.textDarkness); 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 uint16_t width = display.getDisplayWidth(); const uint16_t height = display.getDisplayHeight(); const uint32_t bufferSize = display.getBufferSize(); logSerial.printf("SCREENSHOT_START:%d:%d:%d\n", width, height, 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 } // Power-hold timer for sleep. Hoisted above the screenshot block so the // screenshot path can clear it and avoid a stale POWER press triggering sleep // after the screenshot completes. static unsigned long powerHoldStart = 0; static bool screenshotButtonsReleased = true; if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { if (screenshotButtonsReleased) { screenshotButtonsReleased = false; { RenderLock lock; ScreenshotUtil::takeScreenshot(renderer); } // Discard the POWER+DOWN presses so they don't fire Short/Long events // (e.g. page turn, sleep) once the user releases the combo. buttonEventManager.drain(); powerHoldStart = 0; } return; } else { screenshotButtonsReleased = true; } const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (millis() - lastActivityTime >= sleepTimeoutMs) { LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs); enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } // Track power button hold for sleep. We require a fresh press edge (wasPressed) // before starting to measure hold time, so that a hold carried over from boot // (wake-up press) is never misinterpreted as a "go to sleep" press. // The power button long-press is not user-remappable, so this path always owns it. // Sleep mapped to other buttons is handled by the dispatcher's BTN_SLEEP case below. if (gpio.wasPressed(HalGPIO::BTN_POWER)) { powerHoldStart = millis(); LOG_DBG("MAIN", "loop: power button press detected (fresh edge)"); } if (gpio.isPressed(HalGPIO::BTN_POWER) && powerHoldStart > 0) { const unsigned long heldTime = millis() - powerHoldStart; if (heldTime > SETTINGS.getPowerButtonDuration()) { // If the screenshot combination is potentially being pressed, don't sleep if (gpio.isPressed(HalGPIO::BTN_DOWN)) { return; } LOG_DBG("MAIN", "loop: power button held for %lu ms (> %u ms), entering deep sleep", heldTime, SETTINGS.getPowerButtonDuration()); enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } } if (!gpio.isPressed(HalGPIO::BTN_POWER)) { powerHoldStart = 0; } // 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(); } // Dispatch globally-configured button actions before handing control to the activity. // Only non-Default actions are intercepted here; Default falls through to the activity. { using BA = CrossPointSettings::BUTTON_ACTION; using B = MappedInputManager::Button; auto actionFor = [&](const ButtonEventManager::ButtonEvent& ev) -> uint8_t { switch (ev.button) { case B::Back: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortBack; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoubleBack; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongBack; } break; case B::Confirm: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortConfirm; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoubleConfirm; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongConfirm; } break; case B::Left: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortLeft; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoubleLeft; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongLeft; } break; case B::Right: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortRight; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoubleRight; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongRight; } break; case B::PageBack: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortPageBack; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoublePageBack; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongPageBack; } break; case B::PageForward: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortPageForward; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoublePageForward; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongPageForward; } break; case B::Power: switch (ev.type) { case ButtonEventManager::PressType::Short: return SETTINGS.btnShortPower; case ButtonEventManager::PressType::Double: return SETTINGS.btnDoublePower; case ButtonEventManager::PressType::Long: return SETTINGS.btnLongPower; } break; default: break; // Up/Down have no FSMs — ButtonEventManager never emits these } return BA::BTN_DEFAULT; }; ButtonEventManager::ButtonEvent ev; std::vector defaultEvents; defaultEvents.reserve(8); while (buttonEventManager.consumeEvent(ev)) { const uint8_t action = actionFor(ev); if (action == BA::BTN_DEFAULT) { defaultEvents.push_back(ev); continue; } // When a non-default Long action fires for a page-turn button, mark it so that // detectPageTurn() suppresses the wasReleased-based page turn on button release. if (ev.type == ButtonEventManager::PressType::Long && (ev.button == B::Left || ev.button == B::Right || ev.button == B::PageBack || ev.button == B::PageForward)) { buttonEventManager.markLongPressDispatched(ev.button); } switch (static_cast(action)) { case BA::BTN_PAGE_FORWARD: activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD); break; case BA::BTN_PAGE_BACK: activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK); break; case BA::BTN_PAGE_FORWARD_10: activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD_10); break; case BA::BTN_PAGE_BACK_10: activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK_10); break; case BA::BTN_GO_HOME: activityManager.goHome(); break; case BA::BTN_SLEEP: enterDeepSleep(); return; // enterDeepSleep() never returns, but return here to stop processing case BA::BTN_FORCE_REFRESH: { RenderLock lock; renderer.displayBuffer(HalDisplay::HALF_REFRESH); break; } case BA::BTN_FORCE_FAST_REFRESH: { RenderLock lock; renderer.displayBuffer(HalDisplay::FAST_REFRESH); break; } case BA::BTN_OPEN_TOC: activityManager.dispatchButtonAction(BA::BTN_OPEN_TOC); break; case BA::BTN_OPEN_BOOKMARKS: activityManager.goToGlobalBookmarks(); break; case BA::BTN_STAR_PAGE: activityManager.dispatchButtonAction(BA::BTN_STAR_PAGE); break; case BA::BTN_FOOTNOTES: activityManager.dispatchButtonAction(BA::BTN_FOOTNOTES); break; case BA::BTN_NEXT_SECTION: activityManager.dispatchButtonAction(BA::BTN_NEXT_SECTION); break; case BA::BTN_PREV_SECTION: activityManager.dispatchButtonAction(BA::BTN_PREV_SECTION); break; case BA::BTN_EXIT_READER: activityManager.dispatchButtonAction(BA::BTN_EXIT_READER); break; case BA::BTN_READER_MENU: activityManager.dispatchButtonAction(BA::BTN_READER_MENU); break; case BA::BTN_KOREADER_SYNC: activityManager.dispatchButtonAction(BA::BTN_KOREADER_SYNC); break; case BA::BTN_TOGGLE_BIONIC_READING: activityManager.dispatchButtonAction(BA::BTN_TOGGLE_BIONIC_READING); break; case BA::BTN_CYCLE_FONT_SIZE: activityManager.dispatchButtonAction(BA::BTN_CYCLE_FONT_SIZE); break; case BA::BTN_CYCLE_ORIENTATION: activityManager.dispatchButtonAction(BA::BTN_CYCLE_ORIENTATION); break; default: break; } } for (auto it = defaultEvents.rbegin(); it != defaultEvents.rend(); ++it) { buttonEventManager.pushEventFront(it->button, it->type); } } 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); } } }