diff --git a/README.md b/README.md index 1fbeac3..847f9e9 100644 --- a/README.md +++ b/README.md @@ -1,220 +1,301 @@ -/** - * @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). - */ +# Marine Navigation Display -#include -#include "Config.h" -#include "ConfigManager.h" -#include "BleManager.h" -#include "DisplayManager.h" -#include "WifiManager.h" -#include "TouchManager.h" -#include "StatusLed.h" +> Waterproof marine e-ink navigation display based on the **Waveshare ESP32-S3-Zero** +> and a 4.2" monochrome e-paper screen, acting as a BLE client to the **Marine Gateway**. -// ============================================================================= -// Application state -// ============================================================================= +--- -enum class AppMode { BLE, WIFI_AP }; -static AppMode _mode = AppMode::BLE; -static bool _otaInProgress = false; +## Hardware Bill of Materials -// Cached WiFi status for LED -static AppState::WifiStatus _wifiStatus = AppState::WifiStatus::OFF; +| Component | Model | +|---|---| +| Microcontroller | Waveshare ESP32-S3-Zero | +| Display | Waveshare 4.2" E-Ink GDEY042T81 (400×300 px) | +| Touch buttons | 3× capacitive pads on Touch pins 1, 2, 3 | +| Status LED | Built-in WS2812B NeoPixel (GPIO 21) | +| Power | 3.3 V regulated (e.g. waterproof LiPo + TP4056) | -// ============================================================================= -// Forward declarations -// ============================================================================= -static void enterBleMode(); -static void enterApMode(); -static void handleTouchEvents(); -static void updateDisplay(); +--- -// ============================================================================= -// setup() -// ============================================================================= +## Wiring -void setup() { - Serial.begin(115200); - delay(200); - Serial.println("\n╔═══════════════════════════════╗"); - Serial.println("║ Marine Navigation Display ║"); - Serial.printf( "║ v%-28s║\n", APP_VERSION); - Serial.println("╚═══════════════════════════════╝"); +### E-Ink Display (SPI — HSPI bus) - // ── Status LED ──────────────────────────────────────────────────────────── - statusLed.begin(); - statusLed.setColor(0xFFFFFF, 20); // brief white flash on boot +| ESP32-S3 GPIO | E-Ink Pin | Signal | +|---|---|---| +| 10 | CLK | SPI Clock | +| 11 | DIN | SPI MOSI | +| 9 | CS | Chip Select | +| 8 | DC | Data / Command | +| 7 | RST | Reset | +| 6 | BUSY | Busy (active LOW) | +| 3V3 | VCC | Power | +| GND | GND | Ground | - // ── Persistent config ───────────────────────────────────────────────────── - if (!configManager.begin()) { - Serial.println("[MAIN] FATAL: Config init failed"); - // Continue with defaults — non-fatal - } +> If you rewire these pins, update the `Pins::EPD_*` constants in `include/Config.h`. - // ── Display ─────────────────────────────────────────────────────────────── - displayManager.begin(); // Shows splash screen +### Touch Buttons - // ── Touch ───────────────────────────────────────────────────────────────── - touchManager.begin(); +Connect a metallic rod or pad to each touch GPIO. No pull-up or external components +are required — the ESP32-S3 native touch peripheral handles everything. - // ── 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); - } +| Button | GPIO | Action | +|---|---|---| +| NEXT | Touch 1 | Next page (short press) | +| PREV | Touch 2 | Previous page (short press) | +| ACTION | Touch 3 | Force full e-ink refresh | +| NEXT + PREV | — | Hold 3 s → toggle WiFi AP mode | - if (comboDetected) { - Serial.println("[MAIN] Boot combo detected → entering AP mode"); - enterApMode(); - } else { - enterBleMode(); - } -} +--- -// ============================================================================= -// loop() -// ============================================================================= +## Project Structure -void loop() { - // ── Touch input ─────────────────────────────────────────────────────────── - touchManager.update(); - handleTouchEvents(); +``` +marine-display/ +├── platformio.ini # Build system configuration +├── partitions/ +│ └── custom_ota.csv # Custom flash partition table (OTA + LittleFS) +├── scripts/ +│ └── gen_littlefs.py # Pre-build script (PlatformIO extra_script) +├── data/ # LittleFS root — upload with: pio run -t uploadfs +│ └── version.txt # Auto-generated at build time +├── include/ +│ ├── Config.h # All constants, UUIDs, data structures +│ ├── ConfigManager.h # Persistent configuration API +│ ├── BleManager.h # BLE central client API +│ ├── DisplayManager.h # E-Ink display driver API +│ ├── WifiManager.h # WiFi AP + web server API +│ ├── TouchManager.h # Capacitive touch API +│ └── StatusLed.h # NeoPixel status LED API +└── src/ + ├── main.cpp # Application entry point + ├── ConfigManager.cpp + ├── BleManager.cpp + ├── DisplayManager.cpp + ├── WifiManager.cpp + ├── TouchManager.cpp + └── StatusLed.cpp +``` - // ── Status LED ──────────────────────────────────────────────────────────── - statusLed.update(bleManager.status(), _wifiStatus, _otaInProgress); +--- - // ── BLE mode tasks ──────────────────────────────────────────────────────── - if (_mode == AppMode::BLE) { - bleManager.update(); +## Partition Layout - // 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); - } - } - } +| Name | Type | Size | Purpose | +|---|---|---|---| +| nvs | data/nvs | 20 KB | NimBLE bonds, Preferences | +| otadata | data/ota | 8 KB | OTA slot selector | +| app0 | app/ota_0 | 1.5 MB | Running firmware | +| app1 | app/ota_1 | 1.5 MB | OTA target partition | +| littlefs | data/spiffs | 1 MB | Config JSON + web assets | +| coredump | data/coredump | 64 KB | Crash dump | - // ── WiFi mode tasks ─────────────────────────────────────────────────────── - if (_mode == AppMode::WIFI_AP) { - wifiManager.update(); - } +--- - // ── Display render ──────────────────────────────────────────────────────── - if (_mode == AppMode::BLE && !_otaInProgress) { - updateDisplay(); - } +## Software Architecture - // Small yield to let background tasks run - delay(20); -} +``` +main.cpp + │ + ├── ConfigManager LittleFS + ArduinoJson + │ └── /config.json (pages, tiles, AP credentials) + │ + ├── BleManager NimBLE-Arduino (central role) + │ ├── Scan → connect → pair → subscribe (5 characteristics) + │ ├── Notify callbacks → parse JSON → update BoatState (mutex-protected) + │ └── sendAutopilotCmd / sendAdminCmd + │ + ├── DisplayManager GxEPD2_BW + │ ├── Full refresh — page change, boot, timer (5 min) + │ └── Partial refresh — value area only, per-tile change detection + │ + ├── WifiManager ESPAsyncWebServer + │ ├── GET / → embedded HTML config page + │ ├── GET /api/config → config JSON + │ ├── POST /api/config → apply new config + │ ├── GET /api/state → live BoatState JSON + │ ├── POST /api/ble/cmd → forward BLE command + │ ├── POST /api/ble/passkey → provide BLE pairing PIN + │ └── POST /update → OTA firmware upload + │ + ├── TouchManager ESP32-S3 native touchRead() + │ └── Short press / combo detection (NEXT+PREV → AP mode) + │ + └── StatusLed Adafruit NeoPixel + └── Colour + blink pattern encodes BLE/WiFi status +``` -// ============================================================================= -// Mode transitions -// ============================================================================= +### Mode switching -static void enterBleMode() { - Serial.println("[MAIN] Entering BLE mode"); - _mode = AppMode::BLE; - _wifiStatus = AppState::WifiStatus::OFF; +The ESP32-S3 radio is shared between BLE and WiFi. The display operates in one +mode at a time: - if (wifiManager.isActive()) wifiManager.stop(); +``` +Boot + ├── NEXT+PREV held? → WiFi AP mode + └── Normal boot → BLE mode - bleManager.begin(); - displayManager.forceFullRefresh(); -} +BLE mode: BLE scanning → connect → live data display +WiFi AP mode: AP started → web server → config / OTA -static void enterApMode() { - Serial.println("[MAIN] Entering WiFi AP mode"); - _mode = AppMode::WIFI_AP; - _wifiStatus = AppState::WifiStatus::AP_ACTIVE; +Toggle at runtime: hold NEXT+PREV for 3 seconds. +``` - // 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); -} +## Display Layout -// ============================================================================= -// Touch event dispatcher -// ============================================================================= +``` +┌────────────────────┬────────────────────┐ +│ ████ TILE 0 ████ │ ████ TILE 1 ████ │ +│ │ │ +│ 5.2 │ 47.2° │ +│ kn │ │ +├────────────────────┼────────────────────┤ +│ ████ TILE 2 ████ │ ████ TILE 3 ████ │ +│ │ │ +│ 10.1 │ 85 % │ +│ kn │ │ +└────────────────────┴────────────────────┘ + 400 px wide × 300 px tall + Each tile: 200 × 150 px + Label bar: 22 px (inverted — white text on black) + Value area: 128 px tall — large 24pt font, centred +``` -static void handleTouchEvents() { - // AP combo (NEXT + PREV held 3 s) - if (touchManager.apCombo()) { - if (_mode == AppMode::BLE) { - enterApMode(); - } else { - enterBleMode(); - } - return; - } +### Refresh strategy - 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"); - } - } -} +| Event | Refresh type | Duration | +|---|---|---| +| Data value changed | Partial (value area only) | ~200 ms | +| Page change | Full | ~1500 ms | +| Manual (ACTION button) | Full | ~1500 ms | +| Timer (every 5 min) | Full | ~1500 ms | -// ============================================================================= -// Display update -// ============================================================================= +--- -static void updateDisplay() { - BoatState state; - bleManager.getBoatState(state); +## BLE Protocol - const AppConfig& cfg = configManager.config(); - const PageConfig& page = cfg.pages[cfg.activePage]; +The display connects to the **Marine Gateway** (`MarineGateway`) as a BLE +central client. Full protocol documentation is in `BLE_Client_Documentation.md`. - displayManager.render(state, page); -} +### Pairing + +1. The Marine Gateway displays a 6-digit PIN on its dashboard. +2. Submit the PIN via the web UI at `http://192.168.4.1/` → **BLE Pairing PIN**, + or via the Serial console (planned). +3. The bond is saved; subsequent reconnections are automatic. + +### Subscribed characteristics + +| Service | Characteristic | Update rate | +|---|---|---| +| Navigation | NavData | 1 Hz | +| Wind | WindData | 1 Hz | +| Autopilot | AutopilotData | 1 Hz | +| Sail Performance | PerformanceData | 1 Hz | +| Admin | AdminData | 1 Hz | + +--- + +## Building and Flashing + +### Requirements + +- [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/) ≥ 6.x +- USB-C cable to the ESP32-S3-Zero (native USB CDC) + +### First flash + +```bash +# Clone / open the project +cd marine-display + +# Build and flash firmware +pio run -t upload + +# Upload the LittleFS filesystem (web UI assets + default config placeholder) +pio run -t uploadfs +``` + +### OTA update (subsequent flashes) + +1. Ensure the device is in **WiFi AP mode** (hold NEXT+PREV 3 s or at boot). +2. Connect your computer to the `MarineDisplay` WiFi network. +3. Open `http://192.168.4.1/` in a browser. +4. Scroll to **Firmware Update**, select the `.bin` file, click **Flash Firmware**. + +The firmware binary is at `.pio/build/esp32s3_marine_display/firmware.bin` after a build. + +Alternatively, configure OTA upload in `platformio.ini`: + +```ini +upload_protocol = espota +upload_port = 192.168.4.1 +``` + +Then use `pio run -t upload` over WiFi. + +--- + +## Configuration Web UI + +Connect to `MarineDisplay` WiFi (default password: `marine123`) and open +`http://192.168.4.1/` in a browser. + +### Tile configuration + +Each display page has 4 tiles arranged in a 2×2 grid. For each tile, select: +- **Data Field** — which BLE data to display (SOG, TWA, VMG, Depth, etc.) +- **Label override** — custom short label (max 15 chars); leave blank for auto + +Up to **8 pages** can be configured. Navigate with NEXT/PREV buttons. + +### Gateway control + +From the web UI you can: +- Switch the **Marine Gateway** to STA mode (connect it to your boat WiFi) +- Restart the Marine Gateway remotely +- Submit a BLE pairing PIN + +--- + +## Customisation + +### Adding a new data field + +1. Add an entry to the `DataField` enum in `include/Config.h`. +2. Add the field to `fieldName()` and `fieldUnit()` inline functions. +3. Add the field extraction in `DisplayManager::_fieldToString()`. +4. Add parsing in the appropriate `BleManager::_parseXxx()` method and struct. + +### Changing the display model + +Replace `GxEPD2_420_GDEY042T81` in `include/DisplayManager.h` with the correct +GxEPD2 model class for your panel. Update `DISPLAY_WIDTH` / `DISPLAY_HEIGHT` in +`platformio.ini` accordingly. + +### Changing pin assignments + +All pin constants live in `include/Config.h` under the `Pins::` namespace, or +can be overridden via `build_flags` in `platformio.ini`. + +--- + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---|---|---| +| Display shows only splash | BLE not connected | Wait for scan; check gateway is advertising | +| Values show `---` | NMEA data stale (>10 s) | Check gateway serial input | +| Ghosting on display | Partial refresh accumulation | Press ACTION button for full refresh | +| Can't see web UI | Not in AP mode | Hold NEXT+PREV 3 s | +| OTA fails | Wrong .bin file | Use `firmware.bin` from `.pio/build/` | +| Touch not responding | Threshold too high/low | Adjust `Touch::THRESHOLD` in `Config.h` | +| BLE bond stale | Old bond data | Clear NVS: `pio run -t erase` then re-flash | + +--- + +## Licence + +MIT — see `LICENSE` file.