mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
Some checks are pending
CI / build (push) Waiting to run
## Summary * Disables going to sleep after uploading new firmware * Makes developer experience easier --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< NO >**_ --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
415 lines
16 KiB
C++
415 lines
16 KiB
C++
#include <Arduino.h>
|
|
#include <EInkDisplay.h>
|
|
#include <Epub.h>
|
|
#include <GfxRenderer.h>
|
|
#include <InputManager.h>
|
|
#include <SDCardManager.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"
|
|
|
|
#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 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 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;
|
|
// 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;
|
|
|
|
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 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() {
|
|
einkDisplay.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());
|
|
}
|
|
|
|
bool isUsbConnected() {
|
|
// U0RXD/GPIO20 reads HIGH when USB is connected
|
|
return digitalRead(UART0_RXD) == HIGH;
|
|
}
|
|
|
|
bool isWakeupAfterFlashing() {
|
|
const auto wakeupCause = esp_sleep_get_wakeup_cause();
|
|
const auto resetReason = esp_reset_reason();
|
|
|
|
return isUsbConnected() && (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_UNKNOWN);
|
|
}
|
|
|
|
void setup() {
|
|
t1 = millis();
|
|
|
|
// Only start serial if USB connected
|
|
pinMode(UART0_RXD, INPUT);
|
|
if (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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
KOREADER_STORE.loadFromFile();
|
|
|
|
if (!isWakeupAfterFlashing()) {
|
|
// For normal wakeups (not immediately after flashing), verify long press
|
|
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();
|
|
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.lastSleepImage = 0;
|
|
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;
|
|
|
|
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) or active background work
|
|
static unsigned long lastActivityTime = millis();
|
|
if (inputManager.wasAnyPressed() || inputManager.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 (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
|
|
}
|
|
}
|