/** * @file main.cpp * @brief Marine Navigation Display — application entry point. * * Boot sequence * ───────────── * 1. Initialise Serial, NeoPixel LED, LittleFS config. * 2. Check for AP-mode button combo held at boot. * If combo detected (or first boot): start WiFi AP + web server. * 3. Otherwise start BLE scanning for the Marine Gateway. * 4. main loop: * a. Poll touch buttons. * b. Handle page navigation events. * c. Check BLE status → update LED. * d. Render display (partial refresh where possible). * e. Run WiFi server tasks. * * WiFi AP ↔ BLE co-existence * ────────────────────────── * The ESP32-S3 radio is shared. We run in one of two mutually exclusive modes: * MODE_BLE — BLE client active, WiFi off. * MODE_WIFI — WiFi AP active, BLE off (configuration / OTA mode). * * The user switches modes via the NEXT+PREV button combo (hold 3 s). */ #include #include "Config.h" #include "ConfigManager.h" #include "BleManager.h" #include "DisplayManager.h" #include "WifiManager.h" #include "TouchManager.h" #include "StatusLed.h" // ============================================================================= // Application state // ============================================================================= enum class AppMode { BLE, WIFI_AP }; static AppMode _mode = AppMode::BLE; static bool _otaInProgress = false; // Cached WiFi status for LED static AppState::WifiStatus _wifiStatus = AppState::WifiStatus::OFF; // ============================================================================= // Forward declarations // ============================================================================= static void enterBleMode(); static void enterApMode(); static void handleTouchEvents(); static void updateDisplay(); // ============================================================================= // setup() // ============================================================================= void setup() { Serial.begin(115200); delay(200); Serial.println("\n╔═══════════════════════════════╗"); Serial.println("║ Marine Navigation Display ║"); Serial.printf( "║ v%-28s║\n", APP_VERSION); Serial.println("╚═══════════════════════════════╝"); // ── Status LED ──────────────────────────────────────────────────────────── statusLed.begin(); statusLed.setColor(0xFFFFFF, 20); // brief white flash on boot // ── Persistent config ───────────────────────────────────────────────────── if (!configManager.begin()) { Serial.println("[MAIN] FATAL: Config init failed"); // Continue with defaults — non-fatal } // ── Display ─────────────────────────────────────────────────────────────── displayManager.begin(); // Shows splash screen // ── Touch ───────────────────────────────────────────────────────────────── touchManager.begin(); // ── Detect boot-time AP combo (hold NEXT+PREV while powering on) ────────── // Sample touch pads for 500 ms at boot uint32_t comboStart = millis(); bool comboDetected = false; while (millis() - comboStart < 500) { if (touchRead(Pins::TOUCH_NEXT) < Touch::THRESHOLD && touchRead(Pins::TOUCH_PREV) < Touch::THRESHOLD) { comboDetected = true; break; } delay(10); } if (comboDetected) { Serial.println("[MAIN] Boot combo detected → entering AP mode"); enterApMode(); } else { enterBleMode(); } } // ============================================================================= // loop() // ============================================================================= void loop() { // ── Touch input ─────────────────────────────────────────────────────────── touchManager.update(); handleTouchEvents(); // ── Status LED ──────────────────────────────────────────────────────────── statusLed.update(bleManager.status(), _wifiStatus, _otaInProgress); // ── BLE mode tasks ──────────────────────────────────────────────────────── if (_mode == AppMode::BLE) { bleManager.update(); // Show BLE status banner when not connected static AppState::BleStatus prevBleStatus = AppState::BleStatus::IDLE; AppState::BleStatus curStatus = bleManager.status(); if (curStatus != prevBleStatus) { prevBleStatus = curStatus; if (curStatus != AppState::BleStatus::CONNECTED) { displayManager.showBleStatus(curStatus); } } } // ── WiFi mode tasks ─────────────────────────────────────────────────────── if (_mode == AppMode::WIFI_AP) { wifiManager.update(); } // ── Display render ──────────────────────────────────────────────────────── if (_mode == AppMode::BLE && !_otaInProgress) { updateDisplay(); } // Small yield to let background tasks run delay(20); } // ============================================================================= // Mode transitions // ============================================================================= static void enterBleMode() { Serial.println("[MAIN] Entering BLE mode"); _mode = AppMode::BLE; _wifiStatus = AppState::WifiStatus::OFF; if (wifiManager.isActive()) wifiManager.stop(); bleManager.begin(); displayManager.forceFullRefresh(); } static void enterApMode() { Serial.println("[MAIN] Entering WiFi AP mode"); _mode = AppMode::WIFI_AP; _wifiStatus = AppState::WifiStatus::AP_ACTIVE; // BLE and WiFi share the radio; stop BLE first (NimBLE deinit is implicit // when WiFi takes over, but we force a clean state) // Note: NimBLEDevice::deinit(true) could be called here if needed. const AppConfig& cfg = configManager.config(); wifiManager.begin(cfg.apSSID, cfg.apPassword); displayManager.showApScreen(cfg.apSSID, WifiManager::AP_IP); } // ============================================================================= // Touch event dispatcher // ============================================================================= static void handleTouchEvents() { // AP combo (NEXT + PREV held 3 s) if (touchManager.apCombo()) { if (_mode == AppMode::BLE) { enterApMode(); } else { enterBleMode(); } return; } if (_mode == AppMode::BLE) { if (touchManager.nextPressed()) { configManager.nextPage(); displayManager.forceFullRefresh(); Serial.printf("[MAIN] Page → %u\n", configManager.activePage()); } if (touchManager.prevPressed()) { configManager.prevPage(); displayManager.forceFullRefresh(); Serial.printf("[MAIN] Page ← %u\n", configManager.activePage()); } if (touchManager.actionPressed()) { // Force a full e-ink refresh (clears ghosting) displayManager.forceFullRefresh(); Serial.println("[MAIN] Manual full refresh"); } } } // ============================================================================= // Display update // ============================================================================= static void updateDisplay() { BoatState state; bleManager.getBoatState(state); const AppConfig& cfg = configManager.config(); const PageConfig& page = cfg.pages[cfg.activePage]; displayManager.render(state, page); }