Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
17 kB · 428 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429#include <Arduino.h>#include <Epub.h>#include <FontCacheManager.h>#include <FontDecompressor.h>#include <GfxRenderer.h>#include <HalDisplay.h>#include <HalGPIO.h>#include <HalPowerManager.h>#include <HalStorage.h>#include <HalSystem.h>#include <I18n.h>#include <Logging.h>#include <SPI.h>#include <builtinFonts/all.h>
#include <cstring>
#include "CrossPointSettings.h"#include "CrossPointState.h"#include "KOReaderCredentialStore.h"#include "MappedInputManager.h"#include "OpdsServerStore.h"#include "RecentBooksStore.h"#include "activities/Activity.h"#include "activities/ActivityManager.h"#include "components/UITheme.h"#include "fontIds.h"#include "util/ButtonNavigator.h"#include "util/ScreenshotUtil.h"
MappedInputManager mappedInputManager(gpio);GfxRenderer renderer(display);ActivityManager activityManager(renderer, mappedInputManager);FontDecompressor fontDecompressor;FontCacheManager fontCacheManager(renderer.getFontMap());
// FontsEpdFont 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_FONTSEpdFont 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 valueunsigned long t1 = 0;unsigned long t2 = 0;
// Verify power button press duration on wake-up from deep sleep// Pre-condition: isWakeupByPowerButton() == truevoid 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 powerManager.startDeepSleep(gpio); }}void waitForPowerRelease() { gpio.update(); while (gpio.isPressed(HalGPIO::BTN_POWER)) { delay(50); gpio.update(); }}
// Enter deep sleep modevoid enterDeepSleep() { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); APP_STATE.saveToFile();
activityManager.goToSleep();
display.deepSleep(); LOG_DBG("MAIN", "Entering deep sleep");
powerManager.startDeepSleep(gpio);}
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(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); LOG_DBG("MAIN", "Fonts setup");}
void setup() { t1 = millis();
HalSystem::begin(); gpio.begin(); powerManager.begin();
#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");
// 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; }
HalSystem::checkPanic();
SETTINGS.loadFromFile(); I18N.loadSettings(); 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; }
// First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION);
setupDisplayAndFonts();
activityManager.goToBoot();
APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile();
if (HalSystem::isRebootFromPanic()) { // If we rebooted from a panic, go to crash report screen to show the panic info activityManager.goToCrashReport(); } 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); }
// 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();
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() || activityManager.preventAutoSleep()) { lastActivityTime = millis(); // Reset inactivity timer powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity }
static bool screenshotButtonsReleased = true; if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { if (screenshotButtonsReleased) { screenshotButtonsReleased = false; { RenderLock lock; ScreenshotUtil::takeScreenshot(renderer); } } 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; }
if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > 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); } }}