Marine Navigation Display
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.
Hardware Bill of Materials
| 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) |
Wiring
E-Ink Display (SPI — HSPI bus)
| 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 |
If you rewire these pins, update the
Pins::EPD_*constants ininclude/Config.h.
Touch Buttons
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.
| 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 |
Project Structure
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
Partition Layout
| 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 |
Software Architecture
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
The ESP32-S3 radio is shared between BLE and WiFi. The display operates in one mode at a time:
Boot
├── NEXT+PREV held? → WiFi AP mode
└── Normal boot → BLE mode
BLE mode: BLE scanning → connect → live data display
WiFi AP mode: AP started → web server → config / OTA
Toggle at runtime: hold NEXT+PREV for 3 seconds.
Display Layout
┌────────────────────┬────────────────────┐
│ ████ 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
Refresh strategy
| 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 |
BLE Protocol
The display connects to the Marine Gateway (MarineGateway) as a BLE
central client. Full protocol documentation is in BLE_Client_Documentation.md.
Pairing
- The Marine Gateway displays a 6-digit PIN on its dashboard.
- Submit the PIN via the web UI at
http://192.168.4.1/→ BLE Pairing PIN, or via the Serial console (planned). - 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 ≥ 6.x
- USB-C cable to the ESP32-S3-Zero (native USB CDC)
First flash
# 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)
- Ensure the device is in WiFi AP mode (hold NEXT+PREV 3 s or at boot).
- Connect your computer to the
MarineDisplayWiFi network. - Open
http://192.168.4.1/in a browser. - Scroll to Firmware Update, select the
.binfile, 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:
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
- Add an entry to the
DataFieldenum ininclude/Config.h. - Add the field to
fieldName()andfieldUnit()inline functions. - Add the field extraction in
DisplayManager::_fieldToString(). - 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.