#include #include #include #include #include #include #include #include #include "Battery.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" #include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/SleepActivity.h" #include "activities/home/HomeActivity.h" #include "activities/network/CrossPointWebServerActivity.h" #include "activities/reader/ReaderActivity.h" #include "activities/settings/SettingsActivity.h" #include "activities/util/FullScreenMessageActivity.h" #include "fontIds.h" #define SPI_FQ 40000000 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) #define EPD_SCLK 8 // SPI Clock #define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) #define EPD_CS 21 // Chip Select #define EPD_DC 4 // Data/Command #define EPD_RST 5 // Reset #define EPD_BUSY 6 // Busy #define UART0_RXD 20 // Used for USB connection detection #define SD_SPI_MISO 7 EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); InputManager inputManager; MappedInputManager mappedInputManager(inputManager); GfxRenderer renderer(einkDisplay); Activity* currentActivity; // 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 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); 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); 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); // Auto-sleep timeout (10 minutes of inactivity) constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000; // 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 long press on wake-up from deep sleep void verifyWakeupLongPress() { // 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; // It takes us some time to wake up from deep sleep, so we need to subtract that from the duration constexpr uint16_t calibration = 29; const uint16_t calibratedPressDuration = (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; inputManager.update(); // Verify the user has actually pressed while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. inputManager.update(); } t2 = millis(); if (inputManager.isPressed(InputManager::BTN_POWER)) { do { delay(10); inputManager.update(); } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration); abort = inputManager.getHeldTime() < calibratedPressDuration; } else { abort = true; } if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); esp_deep_sleep_start(); } } void waitForPowerRelease() { inputManager.update(); while (inputManager.isPressed(InputManager::BTN_POWER)) { delay(50); inputManager.update(); } } // Enter deep sleep mode void enterDeepSleep() { exitActivity(); enterNewActivity(new SleepActivity(renderer, mappedInputManager)); einkDisplay.deepSleep(); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Ensure that the power button has been released to avoid immediately turning back on if you're holding it waitForPowerRelease(); // Enter Deep Sleep esp_deep_sleep_start(); } void onGoHome(); void onGoToReader(const std::string& initialEpubPath) { exitActivity(); enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome)); } void onGoToReaderHome() { onGoToReader(std::string()); } void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onGoToFileTransfer() { exitActivity(); enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); } void onGoToSettings() { exitActivity(); enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } void onGoHome() { exitActivity(); enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, onGoToFileTransfer)); } void setupDisplayAndFonts() { einkDisplay.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis()); renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); 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); 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(); // Only start serial if USB connected pinMode(UART0_RXD, INPUT); if (digitalRead(UART0_RXD) == HIGH) { Serial.begin(115200); } inputManager.begin(); // Initialize pins pinMode(BAT_GPIO0, INPUT); // Initialize SPI with custom pins SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS); // 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(); // verify power button press duration after we've read settings. verifyWakeupLongPress(); // 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(); 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); } // 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; inputManager.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) static unsigned long lastActivityTime = millis(); if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) { lastActivityTime = millis(); // Reset inactivity timer } if (millis() - lastActivityTime >= AUTO_SLEEP_TIMEOUT_MS) { Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), AUTO_SLEEP_TIMEOUT_MS); enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } if (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.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 } }