fix readme
This commit is contained in:
@@ -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 <Arduino.h>
|
||||
#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.
|
||||
|
||||
Reference in New Issue
Block a user