main.cpp
1 #include <Arduino.h> 2 #include <EInkDisplay.h> 3 #include <Epub.h> 4 #include <GfxRenderer.h> 5 #include <InputManager.h> 6 #include <LittleFS.h> // Must be before SdFat includes to avoid FILE_READ/FILE_WRITE redefinition 7 #include <SDCardManager.h> 8 #include <SPI.h> 9 #include <builtinFonts/reader_2b.h> 10 #include <builtinFonts/reader_bold_2b.h> 11 #include <builtinFonts/reader_italic_2b.h> 12 #include <driver/gpio.h> 13 #include <esp_sleep.h> 14 #include <esp_system.h> 15 // Medium font (16pt) 16 #include <builtinFonts/reader_medium_2b.h> 17 #include <builtinFonts/reader_medium_bold_2b.h> 18 #include <builtinFonts/reader_medium_italic_2b.h> 19 // Large font (18pt) 20 #include <builtinFonts/reader_large_2b.h> 21 #include <builtinFonts/reader_large_bold_2b.h> 22 #include <builtinFonts/reader_large_italic_2b.h> 23 #include <builtinFonts/small14.h> 24 #include <builtinFonts/ui_12.h> 25 #include <builtinFonts/ui_bold_12.h> 26 27 #include "Battery.h" 28 #include "FontManager.h" 29 #include "MappedInputManager.h" 30 #include "ThemeManager.h" 31 #include "config.h" 32 #include "content/ContentTypes.h" 33 34 // New refactored core system 35 #include "core/BootMode.h" 36 #include "core/Core.h" 37 #include "core/StateMachine.h" 38 #include "images/SignalOSLogo.h" 39 #include "states/CalibreSyncState.h" 40 #include "states/ErrorState.h" 41 #include "states/FileListState.h" 42 #include "states/HomeState.h" 43 #include "states/NetworkState.h" 44 #include "states/ReaderState.h" 45 #include "states/SettingsState.h" 46 #include "states/SleepState.h" 47 #include "states/StartupState.h" 48 #include "states/SyncState.h" 49 #include "ui/views/BootSleepViews.h" 50 51 #define SPI_FQ 40000000 52 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) 53 #define EPD_SCLK 8 // SPI Clock 54 #define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) 55 #define EPD_CS 21 // Chip Select 56 #define EPD_DC 4 // Data/Command 57 #define EPD_RST 5 // Reset 58 #define EPD_BUSY 6 // Busy 59 60 #define UART0_RXD 20 // Used for USB connection detection 61 62 #define SD_SPI_MISO 7 63 64 #define SERIAL_INIT_DELAY_MS 10 65 #define SERIAL_READY_TIMEOUT_MS 3000 66 67 EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); 68 InputManager inputManager; 69 MappedInputManager mappedInputManager(inputManager); 70 GfxRenderer renderer(einkDisplay); 71 72 // Extern references for driver wrappers 73 EInkDisplay& display = einkDisplay; 74 MappedInputManager& mappedInput = mappedInputManager; 75 76 // Core system 77 namespace signalos { 78 Core core; 79 } 80 81 // State instances (pre-allocated, no heap per transition) 82 static signalos::StartupState startupState; 83 static signalos::HomeState homeState(renderer); 84 static signalos::FileListState fileListState(renderer); 85 static signalos::ReaderState readerState(renderer); 86 static signalos::SettingsState settingsState(renderer); 87 static signalos::SyncState syncState(renderer); 88 static signalos::NetworkState networkState(renderer); 89 static signalos::CalibreSyncState calibreSyncState(renderer); 90 static signalos::SleepState sleepState(renderer); 91 static signalos::ErrorState errorState(renderer); 92 static signalos::StateMachine stateMachine; 93 94 RTC_DATA_ATTR uint16_t rtcPowerButtonDurationMs = 400; 95 96 // Fonts - Small (14pt, default) 97 EpdFont readerFont(&reader_2b); 98 EpdFont readerBoldFont(&reader_bold_2b); 99 EpdFont readerItalicFont(&reader_italic_2b); 100 EpdFontFamily readerFontFamily(&readerFont, &readerBoldFont, &readerItalicFont, &readerBoldFont); 101 102 // Fonts - Medium (16pt) 103 EpdFont readerMediumFont(&reader_medium_2b); 104 EpdFont readerMediumBoldFont(&reader_medium_bold_2b); 105 EpdFont readerMediumItalicFont(&reader_medium_italic_2b); 106 EpdFontFamily readerMediumFontFamily(&readerMediumFont, &readerMediumBoldFont, &readerMediumItalicFont, 107 &readerMediumBoldFont); 108 109 // Fonts - Large (18pt) 110 EpdFont readerLargeFont(&reader_large_2b); 111 EpdFont readerLargeBoldFont(&reader_large_bold_2b); 112 EpdFont readerLargeItalicFont(&reader_large_italic_2b); 113 EpdFontFamily readerLargeFontFamily(&readerLargeFont, &readerLargeBoldFont, &readerLargeItalicFont, 114 &readerLargeBoldFont); 115 116 EpdFont smallFont(&small14); 117 EpdFontFamily smallFontFamily(&smallFont); 118 119 EpdFont ui12Font(&ui_12); 120 EpdFont uiBold12Font(&ui_bold_12); 121 EpdFontFamily uiFontFamily(&ui12Font, &uiBold12Font); 122 123 bool isUsbConnected() { return digitalRead(UART0_RXD) == HIGH; } 124 125 bool isWakeupAfterFlashing() { 126 const auto wakeupCause = esp_sleep_get_wakeup_cause(); 127 const auto resetReason = esp_reset_reason(); 128 return isUsbConnected() && (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_UNKNOWN); 129 } 130 131 // Verify long press on wake-up from deep sleep 132 void verifyWakeupLongPress() { 133 // Skip verification on software restart (e.g., after WiFi memory cleanup) 134 const auto resetReason = esp_reset_reason(); 135 if (resetReason == ESP_RST_SW) { 136 Serial.printf("[%lu] [ ] Skipping wakeup verification (software restart)\n", millis()); 137 return; 138 } 139 140 // Fast path for short press mode - skip verification entirely. 141 // When "Short Power Button" is set to "Sleep", rtcPowerButtonDurationMs is 10ms. 142 // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state after wake-up. 143 if (rtcPowerButtonDurationMs <= 10) { 144 Serial.printf("[%lu] [ ] Skipping wakeup verification (short press mode)\n", millis()); 145 return; 146 } 147 148 // Give the user up to 1000ms to start holding the power button, and must hold for the configured duration 149 const auto start = millis(); 150 bool abort = false; 151 const uint16_t requiredPressDuration = rtcPowerButtonDurationMs; 152 153 inputManager.update(); 154 // Verify the user has actually pressed 155 while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { 156 delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. 157 inputManager.update(); 158 } 159 160 if (inputManager.isPressed(InputManager::BTN_POWER)) { 161 do { 162 delay(10); 163 inputManager.update(); 164 } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < requiredPressDuration); 165 abort = inputManager.getHeldTime() < requiredPressDuration; 166 } else { 167 abort = true; 168 } 169 170 if (abort) { 171 // Button released too early. Returning to sleep. 172 // IMPORTANT: Re-arm the wakeup trigger before sleeping again 173 esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); 174 // Hold all GPIO pins at their current state during deep sleep to keep the X4's LDO enabled. 175 // Without this, floating pins can cause increased power draw during sleep. 176 gpio_deep_sleep_hold_en(); 177 esp_deep_sleep_start(); 178 } 179 } 180 181 void waitForPowerRelease() { 182 inputManager.update(); 183 while (inputManager.isPressed(InputManager::BTN_POWER)) { 184 delay(50); 185 inputManager.update(); 186 } 187 } 188 189 void setupDisplayAndFonts() { 190 einkDisplay.begin(); 191 Serial.printf("[%lu] [ ] Display initialized\n", millis()); 192 renderer.insertFont(READER_FONT_ID, readerFontFamily); 193 renderer.insertFont(READER_FONT_ID_MEDIUM, readerMediumFontFamily); 194 renderer.insertFont(READER_FONT_ID_LARGE, readerLargeFontFamily); 195 renderer.insertFont(UI_FONT_ID, uiFontFamily); 196 renderer.insertFont(SMALL_FONT_ID, smallFontFamily); 197 Serial.printf("[%lu] [ ] Fonts setup\n", millis()); 198 } 199 200 void applyThemeFonts() { 201 Theme& theme = THEME_MANAGER.mutableCurrent(); 202 203 // Reset UI font to builtin first in case custom font loading fails 204 theme.uiFontId = UI_FONT_ID; 205 206 // Apply custom UI font if specified (small, always safe to load) 207 if (theme.uiFontFamily[0] != '\0') { 208 int customUiFontId = FONT_MANAGER.getFontId(theme.uiFontFamily, UI_FONT_ID); 209 if (customUiFontId != UI_FONT_ID) { 210 theme.uiFontId = customUiFontId; 211 Serial.printf("[%lu] [FONT] UI font: %s (ID: %d)\n", millis(), theme.uiFontFamily, customUiFontId); 212 } 213 } 214 215 // Only load the reader font that matches current font size setting 216 // This saves ~500KB+ of RAM by not loading all three sizes 217 const char* fontFamilyName = nullptr; 218 int* targetFontId = nullptr; 219 int builtinFontId = 0; 220 221 switch (signalos::core.settings.fontSize) { 222 case signalos::Settings::FontMedium: 223 fontFamilyName = theme.readerFontFamilyMedium; 224 targetFontId = &theme.readerFontIdMedium; 225 builtinFontId = READER_FONT_ID_MEDIUM; 226 break; 227 case signalos::Settings::FontLarge: 228 fontFamilyName = theme.readerFontFamilyLarge; 229 targetFontId = &theme.readerFontIdLarge; 230 builtinFontId = READER_FONT_ID_LARGE; 231 break; 232 default: // FontSmall 233 fontFamilyName = theme.readerFontFamilySmall; 234 targetFontId = &theme.readerFontId; 235 builtinFontId = READER_FONT_ID; 236 break; 237 } 238 239 // Reset to builtin first in case custom font loading fails 240 *targetFontId = builtinFontId; 241 242 if (fontFamilyName && fontFamilyName[0] != '\0') { 243 int customFontId = FONT_MANAGER.getFontId(fontFamilyName, builtinFontId); 244 if (customFontId != builtinFontId) { 245 *targetFontId = customFontId; 246 Serial.printf("[%lu] [FONT] Reader font: %s (ID: %d)\n", millis(), fontFamilyName, customFontId); 247 } 248 } 249 } 250 251 void showErrorScreen(const char* message) { 252 renderer.clearScreen(false); 253 renderer.drawCenteredText(UI_FONT_ID, 100, message, true, BOLD); 254 renderer.displayBuffer(); 255 } 256 257 // Track current boot mode for loop behavior 258 static signalos::BootMode currentBootMode = signalos::BootMode::UI; 259 260 // Early initialization - common to both boot modes 261 // Returns false if critical initialization failed 262 bool earlyInit() { 263 // Only start serial if USB connected 264 pinMode(UART0_RXD, INPUT); 265 gpio_deep_sleep_hold_dis(); // Release GPIO hold from deep sleep to allow fresh readings 266 if (isUsbConnected()) { 267 Serial.begin(115200); 268 delay(SERIAL_INIT_DELAY_MS); // Allow USB CDC to initialize 269 unsigned long start = millis(); 270 while (!Serial && (millis() - start) < SERIAL_READY_TIMEOUT_MS) { 271 delay(SERIAL_INIT_DELAY_MS); 272 } 273 } 274 275 inputManager.begin(); 276 if (!isWakeupAfterFlashing()) { 277 verifyWakeupLongPress(); 278 } 279 280 Serial.printf("[%lu] [ ] Starting DWS SignalOS version " SIGNALOS_VERSION "\n", millis()); 281 282 // Initialize battery ADC pin with proper attenuation for 0-3.3V range 283 analogSetPinAttenuation(BAT_GPIO0, ADC_11db); 284 285 // Initialize SPI with custom pins 286 SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS); 287 288 // SD Card Initialization - critical 289 if (!SdMan.begin()) { 290 Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); 291 setupDisplayAndFonts(); 292 showErrorScreen("SD card error"); 293 return false; 294 } 295 296 // Load settings early (needed for theme/font decisions) 297 signalos::core.settings.loadFromFile(); 298 rtcPowerButtonDurationMs = signalos::core.settings.getPowerButtonDuration(); 299 300 // Initialize internal flash filesystem for font storage 301 if (!LittleFS.begin(false)) { 302 Serial.printf("[%lu] [FS] LittleFS mount failed, attempting format\n", millis()); 303 if (!LittleFS.format() || !LittleFS.begin(false)) { 304 Serial.printf("[%lu] [FS] LittleFS recovery failed\n", millis()); 305 showErrorScreen("Internal storage error"); 306 return false; 307 } 308 Serial.printf("[%lu] [FS] LittleFS formatted and mounted\n", millis()); 309 } else { 310 Serial.printf("[%lu] [FS] LittleFS mounted\n", millis()); 311 } 312 313 return true; 314 } 315 316 // Initialize UI mode - full state registration, all resources 317 void initUIMode() { 318 Serial.printf("[%lu] [BOOT] Initializing UI mode\n", millis()); 319 Serial.printf("[%lu] [BOOT] [UI mode] Free heap: %lu, Max block: %lu\n", millis(), ESP.getFreeHeap(), 320 ESP.getMaxAllocHeap()); 321 322 // Initialize theme and font managers (full) 323 FONT_MANAGER.init(renderer); 324 THEME_MANAGER.loadTheme(signalos::core.settings.themeName); 325 THEME_MANAGER.createDefaultThemeFiles(); 326 Serial.printf("[%lu] [ ] Theme loaded: %s\n", millis(), THEME_MANAGER.currentThemeName()); 327 328 setupDisplayAndFonts(); 329 applyThemeFonts(); 330 331 // Show boot splash only on cold boot (not mode transition) 332 const auto& preInitTransition = signalos::getTransition(); 333 if (!preInitTransition.isValid()) { 334 ui::BootView bootView; 335 bootView.setLogo(SignalOSLogo, 128, 128); 336 bootView.setVersion(SIGNALOS_VERSION); 337 bootView.setStatus("BOOTING"); 338 ui::render(renderer, THEME, bootView); 339 } 340 341 // Register ALL states for UI mode 342 stateMachine.registerState(&startupState); 343 stateMachine.registerState(&homeState); 344 stateMachine.registerState(&fileListState); 345 stateMachine.registerState(&readerState); 346 stateMachine.registerState(&settingsState); 347 stateMachine.registerState(&syncState); 348 stateMachine.registerState(&networkState); 349 stateMachine.registerState(&calibreSyncState); 350 stateMachine.registerState(&sleepState); 351 stateMachine.registerState(&errorState); 352 353 // Initialize core 354 auto result = signalos::core.init(); 355 if (!result.ok()) { 356 Serial.printf("[%lu] [CORE] Init failed: %s\n", millis(), signalos::errorToString(result.err)); 357 showErrorScreen("Core init failed"); 358 return; 359 } 360 361 Serial.printf("[%lu] [CORE] State machine starting (UI mode)\n", millis()); 362 mappedInputManager.setSettings(&signalos::core.settings); 363 364 // Determine initial state - check for return from reader mode 365 signalos::StateId initialState = signalos::StateId::Home; 366 const auto& transition = signalos::getTransition(); 367 368 if (transition.returnTo == signalos::ReturnTo::FILE_MANAGER) { 369 initialState = signalos::StateId::FileList; 370 Serial.printf("[%lu] [BOOT] Returning to FileList from Reader\n", millis()); 371 } else { 372 Serial.printf("[%lu] [BOOT] Starting at Home\n", millis()); 373 } 374 375 stateMachine.init(signalos::core, initialState); 376 377 // Force initial render 378 Serial.printf("[%lu] [CORE] Forcing initial render\n", millis()); 379 stateMachine.update(signalos::core); 380 381 Serial.printf("[%lu] [BOOT] [UI mode] After init - Free heap: %lu, Max block: %lu\n", millis(), ESP.getFreeHeap(), 382 ESP.getMaxAllocHeap()); 383 } 384 385 // Initialize Reader mode - minimal state registration, single font size 386 void initReaderMode() { 387 Serial.printf("[%lu] [BOOT] Initializing READER mode\n", millis()); 388 Serial.printf("[%lu] [BOOT] [READER mode] Free heap: %lu, Max block: %lu\n", millis(), ESP.getFreeHeap(), 389 ESP.getMaxAllocHeap()); 390 391 // Detect content type early to decide if we need custom fonts 392 // XTC/XTCH files contain pre-rendered bitmaps and don't need fonts for page rendering 393 const auto& transition = signalos::getTransition(); 394 signalos::ContentType contentType = signalos::detectContentType(transition.bookPath); 395 bool needsCustomFonts = (contentType != signalos::ContentType::Xtc); 396 397 // Initialize theme and font managers (minimal - no cache) 398 FONT_MANAGER.init(renderer); 399 THEME_MANAGER.loadTheme(signalos::core.settings.themeName); 400 // Skip createDefaultThemeFiles() - not needed in reader mode 401 Serial.printf("[%lu] [ ] Theme loaded: %s (reader mode)\n", millis(), THEME_MANAGER.currentThemeName()); 402 403 setupDisplayAndFonts(); // Builtin fonts - always needed for UI 404 405 if (needsCustomFonts) { 406 applyThemeFonts(); // Custom fonts - skip for XTC/XTCH to save ~500KB+ RAM 407 } else { 408 Serial.printf("[%lu] [BOOT] Skipping custom fonts for XTC content\n", millis()); 409 } 410 411 // Register ONLY states needed for Reader mode 412 stateMachine.registerState(&readerState); 413 stateMachine.registerState(&sleepState); 414 stateMachine.registerState(&errorState); 415 416 // Initialize core 417 auto result = signalos::core.init(); 418 if (!result.ok()) { 419 Serial.printf("[%lu] [CORE] Init failed: %s\n", millis(), signalos::errorToString(result.err)); 420 showErrorScreen("Core init failed"); 421 return; 422 } 423 424 Serial.printf("[%lu] [CORE] State machine starting (READER mode)\n", millis()); 425 mappedInputManager.setSettings(&signalos::core.settings); 426 427 if (transition.bookPath[0] != '\0') { 428 // Copy path to shared buffer for ReaderState to consume 429 strncpy(signalos::core.buf.path, transition.bookPath, sizeof(signalos::core.buf.path) - 1); 430 signalos::core.buf.path[sizeof(signalos::core.buf.path) - 1] = '\0'; 431 Serial.printf("[%lu] [BOOT] Opening book: %s\n", millis(), signalos::core.buf.path); 432 } else { 433 // No book path - fall back to UI mode to avoid boot loop 434 Serial.printf("[%lu] [BOOT] ERROR: No book path in transition, falling back to UI\n", millis()); 435 initUIMode(); 436 return; 437 } 438 439 stateMachine.init(signalos::core, signalos::StateId::Reader); 440 441 // Force initial render 442 Serial.printf("[%lu] [CORE] Forcing initial render\n", millis()); 443 stateMachine.update(signalos::core); 444 445 Serial.printf("[%lu] [BOOT] [READER mode] After init - Free heap: %lu, Max block: %lu\n", millis(), ESP.getFreeHeap(), 446 ESP.getMaxAllocHeap()); 447 } 448 449 void setup() { 450 // Early initialization (common to both modes) 451 if (!earlyInit()) { 452 return; // Critical failure 453 } 454 455 // Detect boot mode from RTC memory or settings 456 currentBootMode = signalos::detectBootMode(); 457 458 if (currentBootMode == signalos::BootMode::READER) { 459 initReaderMode(); 460 } else { 461 initUIMode(); 462 } 463 464 // Ensure we're not still holding the power button before leaving setup 465 waitForPowerRelease(); 466 } 467 468 void loop() { 469 static unsigned long maxLoopDuration = 0; 470 const unsigned long loopStartTime = millis(); 471 static unsigned long lastMemPrint = 0; 472 473 inputManager.update(); 474 475 if (Serial && millis() - lastMemPrint >= 10000) { 476 Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), 477 ESP.getHeapSize(), ESP.getMinFreeHeap()); 478 lastMemPrint = millis(); 479 } 480 481 // Poll input and push events to queue 482 signalos::core.input.poll(); 483 484 // Auto-sleep after inactivity 485 const auto autoSleepTimeout = signalos::core.settings.getAutoSleepTimeoutMs(); 486 if (autoSleepTimeout > 0 && signalos::core.input.idleTimeMs() >= autoSleepTimeout) { 487 Serial.printf("[%lu] [SLP] Auto-sleep after %lu ms idle\n", millis(), autoSleepTimeout); 488 stateMachine.init(signalos::core, signalos::StateId::Sleep); 489 return; 490 } 491 492 // Check for power button long press -> sleep 493 if (inputManager.isPressed(InputManager::BTN_POWER) && 494 inputManager.getHeldTime() > signalos::core.settings.getPowerButtonDuration()) { 495 stateMachine.init(signalos::core, signalos::StateId::Sleep); 496 return; 497 } 498 499 // Update state machine (handles transitions and rendering) 500 const unsigned long activityStartTime = millis(); 501 stateMachine.update(signalos::core); 502 const unsigned long activityDuration = millis() - activityStartTime; 503 504 const unsigned long loopDuration = millis() - loopStartTime; 505 if (loopDuration > maxLoopDuration) { 506 maxLoopDuration = loopDuration; 507 if (maxLoopDuration > 50) { 508 Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration, 509 activityDuration); 510 } 511 } 512 513 // Add delay at the end of the loop to prevent tight spinning 514 delay(10); 515 }