#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Battery.h" #include "CrossPointSettings.h" #include "CrossPointState.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 "config.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_CS 12 #define SD_SPI_MISO 7 EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); InputManager inputManager; GfxRenderer renderer(einkDisplay); Activity* currentActivity; // Fonts EpdFont bookerlyFont(&bookerly_2b); EpdFont bookerlyBoldFont(&bookerly_bold_2b); EpdFont bookerlyItalicFont(&bookerly_italic_2b); EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b); EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont); EpdFont smallFont(&pixelarial14); EpdFontFamily smallFontFamily(&smallFont); EpdFont ubuntu10Font(&ubuntu_10); EpdFont ubuntuBold10Font(&ubuntu_bold_10); EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font); // Auto-sleep timeout (10 minutes of inactivity) constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000; 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; 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(); } if (inputManager.isPressed(InputManager::BTN_POWER)) { do { delay(10); inputManager.update(); } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration()); abort = inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration(); } 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, inputManager)); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); delay(1000); // Allow Serial buffer to empty and display to update // Enable Wakeup on LOW (button press) esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); einkDisplay.deepSleep(); // Enter Deep Sleep esp_deep_sleep_start(); } void onGoHome(); void onGoToReader(const std::string& initialEpubPath) { exitActivity(); enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome)); } void onGoToReaderHome() { onGoToReader(std::string()); } void onGoToFileTransfer() { exitActivity(); enterNewActivity(new CrossPointWebServerActivity(renderer, inputManager, onGoHome)); } void onGoToSettings() { exitActivity(); enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome)); } void onGoHome() { exitActivity(); enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer)); } void setup() { Serial.begin(115200); Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); 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 if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ)) { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); exitActivity(); enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "SD card error", BOLD)); return; } SETTINGS.loadFromFile(); // verify power button press duration after we've read settings. verifyWakeupLongPress(); // Initialize display einkDisplay.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis()); renderer.insertFont(READER_FONT_ID, bookerlyFontFamily); renderer.insertFont(UI_FONT_ID, ubuntuFontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily); Serial.printf("[%lu] [ ] Fonts setup\n", millis()); exitActivity(); enterNewActivity(new BootActivity(renderer, inputManager)); APP_STATE.loadFromFile(); if (APP_STATE.openEpubPath.empty()) { onGoHome(); } else { onGoToReader(APP_STATE.openEpubPath); } // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); } void loop() { static unsigned long lastLoopTime = 0; static unsigned long maxLoopDuration = 0; unsigned long loopStartTime = millis(); static unsigned long lastMemPrint = 0; 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(); } inputManager.update(); // 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.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) { enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } unsigned long activityStartTime = millis(); if (currentActivity) { currentActivity->loop(); } unsigned long activityDuration = millis() - activityStartTime; 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); } } lastLoopTime = loopStartTime; // 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 } }