#include #include #include #include #include #include #include #include #include #include "Battery.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" #include "RecentBooksStore.h" #include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/SleepActivity.h" #include "activities/browser/OpdsBookBrowserActivity.h" #include "activities/home/HomeActivity.h" #include "activities/home/MyLibraryActivity.h" #include "activities/network/CrossPointWebServerActivity.h" #include "activities/reader/ReaderActivity.h" #include "activities/settings/SettingsActivity.h" #include "activities/util/FullScreenMessageActivity.h" #include "fontIds.h" HalDisplay display; HalGPIO gpio; MappedInputManager mappedInputManager(gpio); GfxRenderer renderer(display); Activity* currentActivity; // 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 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 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; void exitActivity() { if (currentActivity) { currentActivity->onExit(); delete currentActivity; currentActivity = nullptr; } } void enterNewActivity(Activity* activity) { currentActivity = activity; currentActivity->onEnter(); } // 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.getHeldTime() < calibratedPressDuration); abort = gpio.getHeldTime() < calibratedPressDuration; } else { abort = true; } if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again gpio.startDeepSleep(); } } void waitForPowerRelease() { gpio.update(); while (gpio.isPressed(HalGPIO::BTN_POWER)) { delay(50); gpio.update(); } } // Enter deep sleep mode void enterDeepSleep() { exitActivity(); enterNewActivity(new SleepActivity(renderer, mappedInputManager)); display.deepSleep(); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); gpio.startDeepSleep(); } void onGoHome(); void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab); void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) { exitActivity(); enterNewActivity( new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab)); } void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); } void onGoToFileTransfer() { exitActivity(); enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); } void onGoToSettings() { exitActivity(); enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } void onGoToMyLibrary() { exitActivity(); enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); } void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) { exitActivity(); enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, tab, path)); } void onGoToBrowser() { exitActivity(); enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); } void onGoHome() { exitActivity(); enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser)); } void setupDisplayAndFonts() { display.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis()); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); #ifndef OMIT_FONTS renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily); renderer.insertFont(BOOKERLY_16_FONT_ID, bookerly16FontFamily); renderer.insertFont(BOOKERLY_18_FONT_ID, bookerly18FontFamily); 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); Serial.printf("[%lu] [ ] Fonts setup\n", millis()); } void setup() { t1 = millis(); gpio.begin(); // Only start serial if USB connected if (gpio.isUsbConnected()) { Serial.begin(115200); // Wait up to 3 seconds for Serial to be ready to catch early logs unsigned long start = millis(); while (!Serial && (millis() - start) < 3000) { delay(10); } } // SD Card Initialization // We need 6 open files concurrently when parsing a new chapter if (!SdMan.begin()) { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); setupDisplayAndFonts(); exitActivity(); enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); return; } SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); switch (gpio.getWakeupReason()) { case HalGPIO::WakeupReason::PowerButton: // For normal wakeups, verify power button press duration Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); verifyPowerButtonDuration(); break; case HalGPIO::WakeupReason::AfterUSBPower: // If USB power caused a cold boot, go back to sleep Serial.printf("[%lu] [ ] Wakeup reason: After USB Power\n", millis()); gpio.startDeepSleep(); break; case HalGPIO::WakeupReason::AfterFlash: // After flashing, just proceed to boot case HalGPIO::WakeupReason::Other: default: break; } // First serial output only here to avoid timing inconsistencies for power button press duration verification Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); setupDisplayAndFonts(); exitActivity(); enterNewActivity(new BootActivity(renderer, mappedInputManager)); APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile(); if (APP_STATE.openEpubPath.empty()) { onGoHome(); } 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.saveToFile(); onGoToReader(path, MyLibraryActivity::Tab::Recent); } // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); } void loop() { static unsigned long maxLoopDuration = 0; const unsigned long loopStartTime = millis(); static unsigned long lastMemPrint = 0; gpio.update(); if (Serial && millis() - lastMemPrint >= 10000) { Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap()); lastMemPrint = millis(); } // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { lastActivityTime = millis(); // Reset inactivity timer } const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (millis() - lastActivityTime >= sleepTimeoutMs) { Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), sleepTimeoutMs); enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) { enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } const unsigned long activityStartTime = millis(); if (currentActivity) { currentActivity->loop(); } const unsigned long activityDuration = millis() - activityStartTime; const unsigned long loopDuration = millis() - loopStartTime; if (loopDuration > maxLoopDuration) { maxLoopDuration = loopDuration; if (maxLoopDuration > 50) { Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), 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 (currentActivity && currentActivity->skipLoopDelay()) { yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else { delay(10); // Normal delay when no activity requires fast response } }