Files
espnav-display/README.md
T
2026-06-18 15:04:18 +02:00

221 lines
8.7 KiB
Markdown

/**
* @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 <Arduino.h>
#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);
}