fix readme
This commit is contained in:
@@ -1,220 +1,301 @@
|
|||||||
/**
|
# Marine Navigation Display
|
||||||
* @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>
|
> Waterproof marine e-ink navigation display based on the **Waveshare ESP32-S3-Zero**
|
||||||
#include "Config.h"
|
> and a 4.2" monochrome e-paper screen, acting as a BLE client to the **Marine Gateway**.
|
||||||
#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 };
|
## Hardware Bill of Materials
|
||||||
static AppMode _mode = AppMode::BLE;
|
|
||||||
static bool _otaInProgress = false;
|
|
||||||
|
|
||||||
// Cached WiFi status for LED
|
| Component | Model |
|
||||||
static AppState::WifiStatus _wifiStatus = AppState::WifiStatus::OFF;
|
|---|---|
|
||||||
|
| 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();
|
|
||||||
|
|
||||||
// =============================================================================
|
## Wiring
|
||||||
// setup()
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
void setup() {
|
### E-Ink Display (SPI — HSPI bus)
|
||||||
Serial.begin(115200);
|
|
||||||
delay(200);
|
|
||||||
Serial.println("\n╔═══════════════════════════════╗");
|
|
||||||
Serial.println("║ Marine Navigation Display ║");
|
|
||||||
Serial.printf( "║ v%-28s║\n", APP_VERSION);
|
|
||||||
Serial.println("╚═══════════════════════════════╝");
|
|
||||||
|
|
||||||
// ── Status LED ────────────────────────────────────────────────────────────
|
| ESP32-S3 GPIO | E-Ink Pin | Signal |
|
||||||
statusLed.begin();
|
|---|---|---|
|
||||||
statusLed.setColor(0xFFFFFF, 20); // brief white flash on boot
|
| 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 you rewire these pins, update the `Pins::EPD_*` constants in `include/Config.h`.
|
||||||
if (!configManager.begin()) {
|
|
||||||
Serial.println("[MAIN] FATAL: Config init failed");
|
|
||||||
// Continue with defaults — non-fatal
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Display ───────────────────────────────────────────────────────────────
|
### Touch Buttons
|
||||||
displayManager.begin(); // Shows splash screen
|
|
||||||
|
|
||||||
// ── Touch ─────────────────────────────────────────────────────────────────
|
Connect a metallic rod or pad to each touch GPIO. No pull-up or external components
|
||||||
touchManager.begin();
|
are required — the ESP32-S3 native touch peripheral handles everything.
|
||||||
|
|
||||||
// ── Detect boot-time AP combo (hold NEXT+PREV while powering on) ──────────
|
| Button | GPIO | Action |
|
||||||
// Sample touch pads for 500 ms at boot
|
|---|---|---|
|
||||||
uint32_t comboStart = millis();
|
| NEXT | Touch 1 | Next page (short press) |
|
||||||
bool comboDetected = false;
|
| PREV | Touch 2 | Previous page (short press) |
|
||||||
while (millis() - comboStart < 500) {
|
| ACTION | Touch 3 | Force full e-ink refresh |
|
||||||
if (touchRead(Pins::TOUCH_NEXT) < Touch::THRESHOLD &&
|
| NEXT + PREV | — | Hold 3 s → toggle WiFi AP mode |
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
## Project Structure
|
||||||
// loop()
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
void loop() {
|
```
|
||||||
// ── Touch input ───────────────────────────────────────────────────────────
|
marine-display/
|
||||||
touchManager.update();
|
├── platformio.ini # Build system configuration
|
||||||
handleTouchEvents();
|
├── 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 ────────────────────────────────────────────────────────
|
## Partition Layout
|
||||||
if (_mode == AppMode::BLE) {
|
|
||||||
bleManager.update();
|
|
||||||
|
|
||||||
// Show BLE status banner when not connected
|
| Name | Type | Size | Purpose |
|
||||||
static AppState::BleStatus prevBleStatus = AppState::BleStatus::IDLE;
|
|---|---|---|---|
|
||||||
AppState::BleStatus curStatus = bleManager.status();
|
| nvs | data/nvs | 20 KB | NimBLE bonds, Preferences |
|
||||||
if (curStatus != prevBleStatus) {
|
| otadata | data/ota | 8 KB | OTA slot selector |
|
||||||
prevBleStatus = curStatus;
|
| app0 | app/ota_0 | 1.5 MB | Running firmware |
|
||||||
if (curStatus != AppState::BleStatus::CONNECTED) {
|
| app1 | app/ota_1 | 1.5 MB | OTA target partition |
|
||||||
displayManager.showBleStatus(curStatus);
|
| 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 ────────────────────────────────────────────────────────
|
## Software Architecture
|
||||||
if (_mode == AppMode::BLE && !_otaInProgress) {
|
|
||||||
updateDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 switching
|
||||||
// Mode transitions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
static void enterBleMode() {
|
The ESP32-S3 radio is shared between BLE and WiFi. The display operates in one
|
||||||
Serial.println("[MAIN] Entering BLE mode");
|
mode at a time:
|
||||||
_mode = AppMode::BLE;
|
|
||||||
_wifiStatus = AppState::WifiStatus::OFF;
|
|
||||||
|
|
||||||
if (wifiManager.isActive()) wifiManager.stop();
|
```
|
||||||
|
Boot
|
||||||
|
├── NEXT+PREV held? → WiFi AP mode
|
||||||
|
└── Normal boot → BLE mode
|
||||||
|
|
||||||
bleManager.begin();
|
BLE mode: BLE scanning → connect → live data display
|
||||||
displayManager.forceFullRefresh();
|
WiFi AP mode: AP started → web server → config / OTA
|
||||||
}
|
|
||||||
|
|
||||||
static void enterApMode() {
|
Toggle at runtime: hold NEXT+PREV for 3 seconds.
|
||||||
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();
|
## Display Layout
|
||||||
wifiManager.begin(cfg.apSSID, cfg.apPassword);
|
|
||||||
displayManager.showApScreen(cfg.apSSID, WifiManager::AP_IP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
```
|
||||||
// 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() {
|
### Refresh strategy
|
||||||
// AP combo (NEXT + PREV held 3 s)
|
|
||||||
if (touchManager.apCombo()) {
|
|
||||||
if (_mode == AppMode::BLE) {
|
|
||||||
enterApMode();
|
|
||||||
} else {
|
|
||||||
enterBleMode();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_mode == AppMode::BLE) {
|
| Event | Refresh type | Duration |
|
||||||
if (touchManager.nextPressed()) {
|
|---|---|---|
|
||||||
configManager.nextPage();
|
| Data value changed | Partial (value area only) | ~200 ms |
|
||||||
displayManager.forceFullRefresh();
|
| Page change | Full | ~1500 ms |
|
||||||
Serial.printf("[MAIN] Page → %u\n", configManager.activePage());
|
| Manual (ACTION button) | Full | ~1500 ms |
|
||||||
}
|
| Timer (every 5 min) | Full | ~1500 ms |
|
||||||
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() {
|
## BLE Protocol
|
||||||
BoatState state;
|
|
||||||
bleManager.getBoatState(state);
|
|
||||||
|
|
||||||
const AppConfig& cfg = configManager.config();
|
The display connects to the **Marine Gateway** (`MarineGateway`) as a BLE
|
||||||
const PageConfig& page = cfg.pages[cfg.activePage];
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user