221 lines
8.7 KiB
Markdown
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);
|
|
}
|