Files
crosspoint-reader/src/main.cpp

221 lines
6.0 KiB
C++
Raw Normal View History

2025-12-03 22:00:29 +11:00
#include <Arduino.h>
#include <EInkDisplay.h>
2025-12-03 22:00:29 +11:00
#include <EpdRenderer.h>
#include <Epub.h>
2025-12-06 12:35:41 +11:00
#include <InputManager.h>
2025-12-03 22:00:29 +11:00
#include <SD.h>
#include <SPI.h>
#include "Battery.h"
2025-12-04 00:07:25 +11:00
#include "CrossPointState.h"
2025-12-06 00:35:58 +11:00
#include "screens/BootLogoScreen.h"
2025-12-03 22:00:29 +11:00
#include "screens/EpubReaderScreen.h"
2025-12-04 00:07:25 +11:00
#include "screens/FileSelectionScreen.h"
2025-12-03 22:00:29 +11:00
#include "screens/FullScreenMessageScreen.h"
2025-12-06 04:20:03 +11:00
#include "screens/SleepScreen.h"
2025-12-03 22:00:29 +11:00
#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);
2025-12-06 12:35:41 +11:00
InputManager inputManager;
EpdRenderer renderer(einkDisplay);
2025-12-03 22:00:29 +11:00
Screen* currentScreen;
2025-12-06 12:56:39 +11:00
CrossPointState appState;
2025-12-03 22:00:29 +11:00
// Power button timing
// Time required to confirm boot from sleep
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
2025-12-03 22:00:29 +11:00
// Time required to enter sleep mode
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
Epub* loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) {
Serial.printf("File does not exist: %s\n", path.c_str());
2025-12-03 22:00:29 +11:00
return nullptr;
}
const auto epub = new Epub(path, "/.crosspoint");
if (epub->load()) {
return epub;
}
Serial.println("Failed to load epub");
delete epub;
2025-12-03 22:00:29 +11:00
return nullptr;
}
2025-12-06 01:37:20 +11:00
void exitScreen() {
2025-12-03 22:00:29 +11:00
if (currentScreen) {
currentScreen->onExit();
delete currentScreen;
}
2025-12-06 01:37:20 +11:00
}
void enterNewScreen(Screen* screen) {
2025-12-03 22:00:29 +11:00
currentScreen = screen;
currentScreen->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 POWER_BUTTON_WAKEUP_MS
const auto start = millis();
2025-12-06 12:35:41 +11:00
bool abort = false;
Serial.println("Verifying power button press");
inputManager.update();
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
delay(50);
2025-12-06 12:35:41 +11:00
inputManager.update();
Serial.println("Waiting...");
}
2025-12-03 22:00:29 +11:00
2025-12-06 12:35:41 +11:00
Serial.printf("Made it? %s\n", inputManager.isPressed(InputManager::BTN_POWER) ? "yes" : "no");
if (inputManager.isPressed(InputManager::BTN_POWER)) {
do {
delay(50);
inputManager.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS);
abort = inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS;
} else {
abort = true;
}
Serial.printf("held for %lu\n", inputManager.getHeldTime());
if (abort) {
2025-12-03 22:00:29 +11:00
// Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
2025-12-06 12:35:41 +11:00
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
2025-12-03 22:00:29 +11:00
esp_deep_sleep_start();
}
}
2025-12-06 12:35:41 +11:00
void waitForPowerRelease() {
inputManager.update();
while (inputManager.isPressed(InputManager::BTN_POWER)) {
2025-12-05 17:47:23 +11:00
delay(50);
2025-12-06 12:35:41 +11:00
inputManager.update();
2025-12-05 17:47:23 +11:00
}
}
2025-12-03 22:00:29 +11:00
// Enter deep sleep mode
void enterDeepSleep() {
2025-12-06 01:37:20 +11:00
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new SleepScreen(renderer, inputManager));
2025-12-03 22:00:29 +11:00
Serial.println("Power button released after a long press. Entering deep sleep.");
delay(1000); // Allow Serial buffer to empty and display to update
2025-12-03 22:00:29 +11:00
// Enable Wakeup on LOW (button press)
2025-12-06 12:35:41 +11:00
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
2025-12-03 22:00:29 +11:00
einkDisplay.deepSleep();
2025-12-03 22:00:29 +11:00
// Enter Deep Sleep
esp_deep_sleep_start();
}
2025-12-04 00:07:25 +11:00
void onGoHome();
void onSelectEpubFile(const std::string& path) {
2025-12-06 01:37:20 +11:00
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
2025-12-04 00:07:25 +11:00
Epub* epub = loadEpub(path);
if (epub) {
2025-12-06 12:56:39 +11:00
appState.openEpubPath = path;
appState.saveToFile();
2025-12-06 01:37:20 +11:00
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
2025-12-04 00:07:25 +11:00
} else {
2025-12-06 01:37:20 +11:00
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
delay(2000);
onGoHome();
2025-12-04 00:07:25 +11:00
}
}
2025-12-06 01:37:20 +11:00
void onGoHome() {
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
2025-12-06 01:37:20 +11:00
}
2025-12-04 00:07:25 +11:00
2025-12-03 22:00:29 +11:00
void setup() {
2025-12-06 12:35:41 +11:00
inputManager.begin();
verifyWakeupLongPress();
2025-12-06 04:20:03 +11:00
// Begin serial only if USB connected
pinMode(UART0_RXD, INPUT);
if (digitalRead(UART0_RXD) == HIGH) {
Serial.begin(115200);
}
2025-12-03 22:00:29 +11:00
// Initialize pins
pinMode(BAT_GPIO0, INPUT);
// Initialize SPI with custom pins
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
// Initialize display
einkDisplay.begin();
2025-12-03 22:00:29 +11:00
Serial.println("Display initialized");
2025-12-06 01:37:20 +11:00
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new BootLogoScreen(renderer, inputManager));
2025-12-03 22:00:29 +11:00
// SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
2025-12-06 12:56:39 +11:00
appState.loadFromFile();
if (!appState.openEpubPath.empty()) {
Epub* epub = loadEpub(appState.openEpubPath);
2025-12-04 00:07:25 +11:00
if (epub) {
2025-12-06 01:37:20 +11:00
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
2025-12-05 17:47:23 +11:00
// Ensure we're not still holding the power button before leaving setup
2025-12-06 12:35:41 +11:00
waitForPowerRelease();
2025-12-04 00:07:25 +11:00
return;
2025-12-03 22:00:29 +11:00
}
}
2025-12-06 01:37:20 +11:00
exitScreen();
2025-12-06 12:35:41 +11:00
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
2025-12-05 17:47:23 +11:00
// Ensure we're not still holding the power button before leaving setup
2025-12-06 12:35:41 +11:00
waitForPowerRelease();
2025-12-03 22:00:29 +11:00
}
void loop() {
2025-12-06 12:35:41 +11:00
delay(10);
2025-12-03 22:00:29 +11:00
2025-12-06 12:56:39 +11:00
static unsigned long lastMemPrint = 0;
2025-12-06 20:24:24 +11:00
if (Serial && millis() - lastMemPrint >= 5000) {
2025-12-06 12:56:39 +11:00
Serial.printf("[%lu] Memory - Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
ESP.getHeapSize(), ESP.getMinFreeHeap());
lastMemPrint = millis();
}
2025-12-06 12:35:41 +11:00
inputManager.update();
if (inputManager.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > POWER_BUTTON_WAKEUP_MS) {
2025-12-03 22:00:29 +11:00
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return;
}
if (currentScreen) {
2025-12-06 12:35:41 +11:00
currentScreen->handleInput();
2025-12-03 22:00:29 +11:00
}
}