/ src / main.cpp
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  }