mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
397 lines
16 KiB
C++
397 lines
16 KiB
C++
#include <Arduino.h>
|
|
#include <Epub.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalDisplay.h>
|
|
#include <HalGPIO.h>
|
|
#include <HalStorage.h>
|
|
#include <SPI.h>
|
|
#include <builtinFonts/all.h>
|
|
|
|
#include <cstring>
|
|
|
|
#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 (!Storage.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
|
|
}
|
|
}
|