{"id":50901353,"url":"https://github.com/navado/esp32-boat-mfd","last_synced_at":"2026-06-16T03:03:59.781Z","repository":{"id":360029422,"uuid":"1248350630","full_name":"navado/esp32-boat-mfd","owner":"navado","description":"Flexible marine instruments system for ESP32 based screens","archived":false,"fork":false,"pushed_at":"2026-06-12T20:30:48.000Z","size":9062,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T21:15:33.286Z","etag":null,"topics":["boat-instruments","boat-simulation","esp32","marine-display","modular-screen","signalk-plugin","signalk-webapp"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/navado.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-24T14:28:15.000Z","updated_at":"2026-06-12T20:30:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/navado/esp32-boat-mfd","commit_stats":null,"previous_names":["navado/esp32-boat-mfd"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/navado/esp32-boat-mfd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navado%2Fesp32-boat-mfd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navado%2Fesp32-boat-mfd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navado%2Fesp32-boat-mfd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navado%2Fesp32-boat-mfd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/navado","download_url":"https://codeload.github.com/navado/esp32-boat-mfd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navado%2Fesp32-boat-mfd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34388670,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["boat-instruments","boat-simulation","esp32","marine-display","modular-screen","signalk-plugin","signalk-webapp"],"created_at":"2026-06-16T03:03:55.384Z","updated_at":"2026-06-16T03:03:59.761Z","avatar_url":"https://github.com/navado.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# esp32-boat-mfd\n\n[![CI](https://github.com/navado/esp32-boat-mfd/actions/workflows/ci.yml/badge.svg)](https://github.com/navado/esp32-boat-mfd/actions/workflows/ci.yml)\n[![Release](https://github.com/navado/esp32-boat-mfd/actions/workflows/release.yml/badge.svg)](https://github.com/navado/esp32-boat-mfd/actions/workflows/release.yml)\n[![License: PolyForm-NC-1.0.0](https://img.shields.io/badge/license-PolyForm--NC--1.0.0-yellow.svg)](LICENSE)\n[![PlatformIO](https://img.shields.io/badge/PlatformIO-6.x-orange.svg)](https://platformio.org)\n[![Board: ESP32-4848S040](https://img.shields.io/badge/board-ESP32--4848S040-blue.svg)](#hardware)\n[![SignalK](https://img.shields.io/badge/SignalK-client-1eb2a8.svg)](https://signalk.org)\n\nA source-available marine multi-function display (MFD) firmware for the\nESP32-S3 family of touch panels. Acts as a [SignalK](https://signalk.org)\nWebSocket client and renders live navigation data on an LVGL dashboard.\n\nThe current development line also includes a repo-owned SignalK lab stack and\nan experimental SignalK plugin for centralized ESP display registration,\nprofiles, widget layout, commands, and firmware update orchestration. The\nplugin side is test-covered, and the firmware now has an MVP manager client\nfor registration, heartbeats, config pull, command polling, and pull OTA.\nReal-boat validation and security hardening are still in progress.\n\nLicensed under [PolyForm Noncommercial 1.0.0](LICENSE) — free for\npersonal, research, educational, and other noncommercial use.\n**Commercial use requires a separate license** (see [Commercial use](#commercial-use)).\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"docs/demo.mp4\"\u003e\n    \u003cimg src=\"docs/demo.gif\" alt=\"Live dashboard demo\" width=\"320\"\u003e\n  \u003c/a\u003e\n  \u003cbr\u003e\n  \u003cem\u003eLive SignalK data — wind, navigation, depth, position, battery. Click for full-quality MP4.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/screen-dashboard.png\" alt=\"Dashboard test screenshot\" width=\"150\"\u003e\n  \u003cimg src=\"docs/images/screen-wind.png\" alt=\"Wind screen test screenshot\" width=\"150\"\u003e\n  \u003cimg src=\"docs/images/screen-nav.png\" alt=\"Navigation screen test screenshot\" width=\"150\"\u003e\n  \u003cimg src=\"docs/images/screen-route.png\" alt=\"Route screen test screenshot\" width=\"150\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/images/screen-depth.png\" alt=\"Depth screen test screenshot\" width=\"150\"\u003e\n  \u003cimg src=\"docs/images/screen-settings.png\" alt=\"Settings screen test screenshot\" width=\"150\"\u003e\n  \u003cimg src=\"docs/images/screen-wifi.png\" alt=\"WiFi setup screen test screenshot\" width=\"150\"\u003e\n  \u003cimg src=\"docs/images/screen-touch-grid.png\" alt=\"Touch grid test screenshot\" width=\"150\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eLive screenshots captured from the device (glass-cockpit theme).\u003c/em\u003e\n\u003c/p\u003e\n\n\u003e **Latest UI (2026-06):** a \"glass-cockpit\" redesign — bordered gradient\n\u003e cells, a cool high-contrast palette, large hero numerals (custom 64 px\n\u003e font), semantic color, and a single consolidated style source. The\n\u003e **autopilot** and **wind** screens were reworked into a reference HUD style:\n\u003e a semicircular heading compass (white band, green rail, red cardinals, amber\n\u003e target bug), a centered HDG readout with COG/SOG, a cross-track-error strip,\n\u003e and clean numeric tiles. The wind rose keeps the full 360° (wind can blow\n\u003e from any bearing) plus a live tidal **current vector**, with all numbers moved\n\u003e out to tiles. Tapping a dashboard tile opens a full-screen **zoom** view. See\n\u003e [docs/user-guide-signalk.md](docs/user-guide-signalk.md) for managing\n\u003e displays, dashboards, and OTA firmware from SignalK.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/ap-480x480.png\" alt=\"Autopilot HUD 480x480\" width=\"200\"\u003e\n  \u003cimg src=\"docs/sim-shots/ap-800x480.png\" alt=\"Autopilot HUD 800x480\" width=\"300\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/sim-shots/ap-1024x600.png\" alt=\"Autopilot HUD 1024x600\" width=\"340\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eAutopilot HUD — semicircular compass, target bug, XTE strip, and numeric\n  tiles — rendered at every supported display class (480×480 square, 800×480 and\n  1024×600 wide). Engage/standby and mode are touch (long-press = mode picker)\n  or driven by the external network knob.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/wind-480x480.png\" alt=\"Wind dial 480x480\" width=\"190\"\u003e\n  \u003cimg src=\"docs/sim-shots/wind-800x480.png\" alt=\"Wind dial 800x480\" width=\"300\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/sim-shots/wind-1024x600.png\" alt=\"Wind dial 1024x600\" width=\"340\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eWind dial — full 360° rose with apparent/true wind indices, close-hauled\n  sectors, and the tidal current vector; AWS/AWA/TWS/TWA in tiles.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/dash-480x480.png\" alt=\"Dashboard 480x480\" width=\"150\"\u003e\n  \u003cimg src=\"docs/sim-shots/dash-800x480.png\" alt=\"Dashboard 800x480\" width=\"250\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/sim-shots/dash-1024x600.png\" alt=\"Dashboard 1024x600\" width=\"320\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/sim-shots/knob-ap_hud.png\" alt=\"Round autopilot control (Waveshare knob)\" width=\"150\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eDashboard at every supported display class (480×480, 800×480, 1024×600),\n  and the round autopilot control on the Waveshare knob (360×360). All produced\n  by the headless LVGL host harness — \u003ccode\u003emake sim\u003c/code\u003e — which runs the real\n  screen code and asserts no-overlap/in-bounds per resolution.\u003c/em\u003e\n\u003c/p\u003e\n\nThe navigation, depth, route, trip and full-screen tap-to-zoom views render\nthrough the same host harness and are validated at every resolution:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/nav-480x480.png\" alt=\"Nav 480x480\" width=\"150\"\u003e\n  \u003cimg src=\"docs/sim-shots/route-480x480.png\" alt=\"Route 480x480\" width=\"150\"\u003e\n  \u003cimg src=\"docs/sim-shots/depth-480x480.png\" alt=\"Depth 480x480\" width=\"150\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/sim-shots/trip-480x480.png\" alt=\"Trip 480x480\" width=\"150\"\u003e\n  \u003cimg src=\"docs/sim-shots/wind_steer-480x480.png\" alt=\"Wind steering 480x480\" width=\"150\"\u003e\n  \u003cimg src=\"docs/sim-shots/zoom-pos-480x480.png\" alt=\"Zoom: position 480x480\" width=\"150\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eNav (compass + SOG + COG + position), Route (DTW/BTW/XTE/VMG), Depth, Trip,\n  the Wind-steering aid (TWA + tack/gybe angles + new heading + TWD), and the\n  full-screen tap-to-zoom view — which fits a two-line lat/lon as well as a single\n  big number. Wide-panel (800×480, 1024×600) renders are in\n  \u003ca href=\"docs/sim-shots/\"\u003edocs/sim-shots/\u003c/a\u003e.\u003c/em\u003e\n\u003c/p\u003e\n\n## Features\n\n- **SignalK over WebSocket** — subscribes to navigation, wind, depth, water temp, battery, tanks, route, and autopilot state\n- **9 fullscreen screens** — Dashboard, Wind (compass rose + AWA/TWA arrows + tac sectors), Nav (huge SOG + DDM position), Depth (chart history + alarm bands), Steering (heading bug + CTS + XTE bar), Route (DTW/BTW/CTS/XTE/VMG/TTG/ETA), Autopilot (PUT to SignalK target/state), Trip (NVS-persisted distance/time/avg/max), System health\n- **Swipe navigation** — horizontal swipes cycle screens; bottom swipe jumps to Dashboard\n- **Day / night theme** — `theme day|night` console command, persisted in NVS\n- **On-screen WiFi setup** — touch-keyboard SSID scan + password entry, no cable needed\n- **MOB + alarms** — global overlays available from every screen\n- **Touch diagnostics** — touch calibration/grid screens and GT911 interrupt/config-dump validation specs\n- **Over-the-air updates** — ArduinoOTA on port 3232 (no USB cable for iteration)\n- **BLE diagnostics + config** — Nordic UART service for logs + a structured Connection/Configuration GATT for companion apps\n- **SignalK lab stack** — Dockerized SignalK server with official NMEA 0183 TCP and autopilot emulator plugins for repeatable testing\n- **Control protocol (P2P)** — a versioned, schema-generated control protocol that lets a controller (knob, plugin, harness) discover and drive any display directly over IP (mDNS + `/api/p2p/*`, primary) and BLE (Control GATT, fallback), with lightweight many-to-many sessions and a per-controller colored frame on controlled displays — see [Control Protocol (P2P)](#control-protocol-p2p)\n- **Experimental ESP display manager** — local SignalK plugin for device registry, provisioning, profiles, widget configs, command queues, firmware catalog/jobs, and a dashboard UI\n- **Multi-target logging** — Serial / UDP broadcast / BLE notify, the same `logf()` writes to all three\n- **Host-portable parser** — SignalK delta logic builds and tests on macOS / Linux as well as the device\n- **CI + release automation** — GitHub Actions builds firmware and the SignalK plugin package on every push; tagged releases attach firmware binaries plus the matching plugin tarball\n\n## Project status\n\nThis repository is suitable for lab testing and firmware/plugin development.\nIt is not yet a production navigation instrument.\n\n| Area | Status |\n|------|--------|\n| ESP32 display firmware | Active; core screens, touch UI, WiFi, BLE, OTA, SignalK ingest, and diagnostics exist |\n| SignalK local test stack | Active; `make demo-up` starts the configured SignalK container |\n| NMEA 0183 over WiFi | Configured through the official `@signalk/signalk-to-nmea0183` plugin on TCP `10110` |\n| Autopilot simulator | Configured through the official `@signalk/signalk-autopilot` emulator backend |\n| ESP display manager plugin | Experimental but implemented and covered by local plugin tests |\n| Firmware manager client | MVP implemented; opt-in contract tests exist; real-network validation and hardening remain |\n| OTA fleet management | MVP implemented; plugin-side artifact/job model and firmware pull/apply path exist; hardware failure-path validation remains |\n\n## Architecture Overview\n\nThe project has two cooperating halves:\n\n- **ESP display firmware** runs on the touch panel. It renders the local UI,\n  ingests SignalK/NMEA data, exposes diagnostics over serial/BLE/UDP, supports\n  OTA, and includes a manager client that can register with SignalK, fetch a\n  generated config, poll commands, report status, and run pull-based firmware\n  updates.\n- **SignalK lab and manager stack** runs on the development machine or boat\n  server. It provides synthetic data, NMEA 0183 TCP output, an autopilot\n  emulator, and the repo-owned `espdisp-manager` plugin for fleet-style display\n  management.\n\nThe manager plugin is the control plane for configuring ESP display dashboards\nfrom SignalK:\n\n```text\nSignalK server\n  espdisp-manager plugin\n    registry          known devices, status, display geometry, capabilities\n    dashboard presets reusable screen/theme/widget configs for similar devices\n    generated config  per-device dashboard merge of preset + overrides\n    commands          reload dashboard config, screen actions, firmware actions\n    firmware catalog  vendor/product/version metadata and OTA jobs\n\nESP display firmware\n  register -\u003e fetch config -\u003e render -\u003e heartbeat/status -\u003e poll commands\n```\n\nDevices are not expected to receive arbitrary JSON from the operator UI.\nOperators use structured pages:\n\n- `Devices` lists registered panels, health, selected dashboard preset,\n  display size, config drift, and pending commands.\n- `Device config` edits a single dashboard from SignalK: preset assignment,\n  day/night theme, brightness, NMEA WiFi source, autopilot widgets, widget font\n  sizes, touch/debug mode, and per-device overrides.\n- `Presets` manages reusable dashboard configurations so several panels of the\n  same size or role can share a common setup. Presets can be imported/exported\n  as JSON or YAML for review and version control.\n- `Preset detail` applies one dashboard preset to selected devices and can\n  queue `config.reload` so the devices pull the new generated dashboard config.\n- The device web UI exposes matching dashboard config import/export endpoints:\n  `/api/dashboard/config.json` and `/api/dashboard/config.yaml`.\n- `Firmware` tracks plugin-side firmware artifacts and OTA jobs; the firmware\n  can pull, verify, install, reboot, and confirm jobs, with hardware\n  failure-path validation still outstanding.\n\nCurrent SignalK dashboard-configuration screenshots:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/signalk-manager-overview.png\" alt=\"ESP Display Manager overview\" width=\"420\"\u003e\n  \u003cimg src=\"docs/images/signalk-manager-device-config.png\" alt=\"ESP Display Manager device configuration\" width=\"420\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/images/signalk-manager-device-config-day.png\" alt=\"ESP Display Manager day dashboard configuration\" width=\"420\"\u003e\n  \u003cimg src=\"docs/images/signalk-manager-preset-apply-day.png\" alt=\"ESP Display Manager day preset apply page\" width=\"420\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"docs/images/signalk-manager-presets.png\" alt=\"ESP Display Manager presets\" width=\"420\"\u003e\n  \u003cimg src=\"docs/images/signalk-manager-preset-apply.png\" alt=\"ESP Display Manager preset apply page\" width=\"420\"\u003e\n\u003c/p\u003e\n\n## Hardware\n\nPrimary target:\n\n| Component | Detail |\n|-----------|--------|\n| Board | Sunton / Guition **ESP32-4848S040** (also labelled `ESP32-4840S040`) |\n| MCU | ESP32-S3-WROOM-1 **N16R8** — 16 MB flash + 8 MB octal PSRAM |\n| Display | 4.0″ IPS 480×480, ST7701 RGB parallel |\n| Touch | GT911 capacitive, I²C `SDA=19 SCL=45` |\n| Storage | microSD slot |\n| USB | USB-C with CH340 USB-UART |\n\nAdditional ESP32-S3 RGB touch profiles now compile through the same board\nabstraction and report their geometry to the device identity/status APIs:\n\n| PlatformIO env | Board profile | Display | Layout class |\n|----------------|---------------|---------|--------------|\n| `waveshare-touch-lcd-4` | Waveshare Touch LCD 4 | 480x480 square | `square-480` |\n| `waveshare-touch-lcd-4_3` | Waveshare Touch LCD 4.3 | 800x480 landscape | `landscape-800x480` |\n| `waveshare-touch-lcd-4_3b` | Waveshare Touch LCD 4.3B | 800x480 landscape | `landscape-800x480` |\n| `waveshare-touch-lcd-5_800x480` | Waveshare Touch LCD 5 | 800x480 landscape | `landscape-800x480` |\n| `waveshare-touch-lcd-5_1024x600` | Waveshare Touch LCD 5 | 1024x600 landscape | `landscape-1024x600` |\n| `waveshare-touch-lcd-7_800x480` | Waveshare Touch LCD 7 | 800x480 landscape | `landscape-800x480` |\n| `waveshare-touch-lcd-7b_1024x600` | Waveshare Touch LCD 7B | 1024x600 landscape | `landscape-1024x600` |\n| `waveshare-knob-1_8` | Waveshare ESP32-S3-Knob 1.8 (rotary remote) | 360x360 **round** (ST77916 QSPI) | `square-compact` |\n\nThe `waveshare-knob-1_8` profile is a **rotary-encoder remote controller** with\na small set of dedicated round views rather than a full dashboard panel — see\n[Remote Knob](#remote-knob) below and the\n[deploy \u0026 use guide](docs/remote-knob.md).\n\nThese profiles share the current RGB/LVGL initialization path. They are build\nprofiles and geometry/layout contracts until each physical board passes the\nhardware checklist for panel timing, backlight, touch coordinates, rotation,\nCAN/RS485 exposure, SignalK connectivity, and dashboard rendering.\n\n```sh\npio run -e waveshare-touch-lcd-4\npio run -e waveshare-touch-lcd-7b_1024x600\n```\n\nThe firmware reports board metadata including resolution, shape, density,\nlayout class, usable area, display bus, touch controller, touch interrupt, and\nNMEA 2000 CAN capability so the SignalK manager can select presets by geometry\ninstead of hardcoded board names.\n\n## Remote Knob\n\nThe **Waveshare ESP32-S3-Knob-Touch-LCD-1.8** (`waveshare-knob-1_8`) runs the\nsame firmware as a dedicated **rotary remote controller** instead of a full\ndashboard. It is a 360×360 **round** ST77916 QSPI panel driven by a rotary\nencoder with a push button: scroll the knob to adjust, click to select,\nlong-press / double-click for menus. It carries a small set of round views and\ncan drive **other** displays on the network through the `espdisp-manager`\nplugin (switching their active view, instant-apply via `configPush`).\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/knob-gallery.png\" alt=\"Knob round views: Autopilot HUD, Compass, Wind, Big number\" width=\"480\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eThe four dedicated round views — Autopilot HUD, Compass, Wind angle, Big\n  number — rendered at 360×360 by the \u003ccode\u003emake sim\u003c/code\u003e harness.\u003c/em\u003e\n\u003c/p\u003e\n\n### Gesture cheat-sheet\n\nThe **Autopilot HUD** is home. Gestures there control the autopilot directly:\n\n| Gesture | Action (Autopilot HUD / home) |\n|---------|-------------------------------|\n| Scroll | Adjust target heading ±1° (apparent wind angle in Wind mode) |\n| Hold + scroll | Adjust ±5° |\n| Click | Engage / disengage (toggle Standby ⇄ last active mode) |\n| Long-press | Open the **mode picker** (Standby / Compass / Wind / Route) |\n| Double-click | Open the **menu** (Select Display → Select View) |\n\nScrolling while in Standby pre-sets the target so engaging Compass holds it.\n\nInside menus the vocabulary is uniform: **scroll** moves the highlight,\n**click** selects/enters, **double-click** goes back one level.\n\n### Menu map\n\n```text\nAutopilot HUD  (home)\n  long-press  -\u003e Mode Picker  (Standby / Compass / Wind / Route)\n  double-click-\u003e Select Display\n                   click on a display -\u003e Select View\n                                           click on a view -\u003e switch that\n                                                              display to it\n                   double-click -\u003e back to home\n```\n\nThe display list = the knob itself (its four round views) plus the remote MFDs\ndiscovered through the manager. \"Select View\" switches the chosen display's\nactive view.\n\nThe four dedicated round views:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/knob-ap_hud.png\" alt=\"Knob Autopilot HUD\" width=\"120\"\u003e\n  \u003cimg src=\"docs/sim-shots/knob-compass.png\" alt=\"Knob Compass\" width=\"120\"\u003e\n  \u003cimg src=\"docs/sim-shots/knob-wind.png\" alt=\"Knob Wind angle\" width=\"120\"\u003e\n  \u003cimg src=\"docs/sim-shots/knob-big.png\" alt=\"Knob Big number\" width=\"120\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eAutopilot HUD · Compass · Wind angle · Big number (depth/SOG).\u003c/em\u003e\n\u003c/p\u003e\n\nFor flashing, provisioning, and using the knob to drive other displays, see\n[Deploy \u0026 use the remote knob](docs/remote-knob.md). For how the knob is verified\nin software and the hardware bring-up checklist, see\n[Testing \u0026 simulation](docs/knob-testing.md).\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/sim-shots/knob-menu-gallery.png\" alt=\"Knob menu overlays: mode picker, Select Display, Select View\" width=\"480\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eMenu overlays — mode picker · Select Display · Select View — rendered at\n  360×360 by the \u003ccode\u003emake sim\u003c/code\u003e harness.\u003c/em\u003e\n\u003c/p\u003e\n\n## Control Protocol (P2P)\n\nA **versioned, transport-agnostic control protocol** lets any controller (the\nknob, the SignalK plugin, a future phone app) discover and drive any target\ndisplay **directly** — without routing through the SignalK manager. It is defined\nonce as a JSON Schema and code-generated into a C++ library (firmware) and a JS\nlibrary (`@espdisp/proto`, plugin), kept in lockstep by shared fixtures.\n\n- **Discovery + control over IP (primary):** controllers browse `_espdisp._tcp`\n  via mDNS and control targets over the versioned `GET/POST /api/p2p/*` HTTP/JSON\n  surface (`device`, `attach`, `switch`, `heartbeat`, `detach`, `state`).\n- **BLE (fallback):** targets expose an espdisp Control GATT service; controllers\n  use an on-demand BLE central (scan → connect-one → control → disconnect) only\n  when there is no reachable IP.\n- **Sessions + colored frame:** lightweight many-to-many sessions (last-writer-wins,\n  heartbeat/TTL reap); a controlled display renders a frame in each controlling\n  controller's color with its name.\n- **Verified on hardware** by the headless **ESP32-S3-DevKitC-1 harness**\n  (`harness-s3-devkitc`), which loops discover → attach → switch → detach against\n  a real display in place of the knob.\n\nSee the [espdisp Control Protocol guide](docs/control-protocol.md) for the message\ntypes, endpoints, BLE UUIDs, the `ctl` config commands, and the harness procedure.\n\n## Onboard setup\n\nNormal boat installs should use firmware and plugin artifacts built by this\nrepository's GitHub Actions workflows, not ad-hoc local builds.\n\nStable release path:\n\n1. Open the\n   [latest GitHub release](https://github.com/navado/esp32-boat-mfd/releases).\n2. Download the merged firmware image for the physical board, for example\n   `esp32-4848s040-merged_firmware.bin` or\n   `waveshare-touch-lcd-7b_1024x600-merged_firmware.bin`.\n3. Download `signalk-espdisp-manager-\u003cversion\u003e.tgz` for the SignalK plugin.\n4. Verify artifacts with the release `SHA256SUMS` file.\n5. Flash the display over USB:\n\n```sh\nesptool.py --chip esp32s3 --port /dev/cu.usbserial-* write_flash 0x0 \u003ctarget\u003e-merged_firmware.bin\n```\n\n6. Install the plugin on the boat SignalK server:\n\n```sh\ncd ~/.signalk\nnpm install /path/to/signalk-espdisp-manager-\u003cversion\u003e.tgz\n```\n\n7. Restart SignalK, then enable **ESP Display Manager** in the SignalK admin\n   plugin UI if you want centralized display management.\n8. Provision the display onto the boat WiFi and SignalK server from serial or\n   BLE:\n\n```text\nwifi \u003cboat-ssid\u003e \u003cboat-wifi-password\u003e\nsk \u003csignalk-host-or-ip\u003e 3000\n```\n\nLatest CI artifact path:\n\n- Firmware artifacts are attached to successful CI runs as\n  `firmware-\u003cplatformio-env\u003e-latest`, for example\n  `firmware-esp32-4848s040-latest` or\n  `firmware-waveshare-touch-lcd-7b_1024x600-latest`.\n  Each artifact includes `merged_firmware.bin` for first USB flashing plus\n  `firmware.bin`, `firmware.elf`, `bootloader.bin`, and `partitions.bin`.\n- The plugin package artifact is named `signalk-espdisp-manager-\u003cgit-sha\u003e`.\n- CI artifacts are useful for testing current `main` or newer board profiles;\n  releases remain the preferred stable boat install source.\n\nFor the full onboard checklist and network model, see\n[Boat setup](docs/boat-setup.md). For development-only lab workflows, see\n[Running with synthetic data](#running-with-synthetic-data) and\n[Lab topology](docs/lab-topology.md).\nFor managed-display registry/config/command concepts, see\n[SignalK ESP Display Manager](docs/signalk-espdisp-manager.md).\nFor the full status and upcoming work, see the [project roadmap](docs/roadmap.md).\n\n## Development make targets\n\nThese targets are for development, testing, and local flashing.  Normal onboard\ninstalls should start from the CI/release artifacts described above.\n\n```\nmake help          List all targets\nmake setup         First-time setup (PlatformIO check + secrets.h)\nmake build         Build firmware\nmake test          Run host-side unit tests\nmake flash         Flash over USB (auto-detects /dev/cu.usbserial-*)\nmake ota              Flash over WiFi  (DEVICE_IP defaults to espdisp.local)\nmake monitor          Open serial monitor\nmake ble              Open BLE console (logs + commands without WiFi)\nmake logs             Listen for UDP log broadcasts on :9999\nmake demo-up          Start SignalK + synthetic data in local Docker\nmake demo-down        Stop the local demo stack\nmake demo-up-remote   Start SignalK on nav-server over SSH+Docker\nmake demo-down-remote Stop the remote stack\nmake sys-test-remote  Source .env.test and run system tests against the lab rig\nmake lint          Check formatting + Python syntax\nmake format        Auto-format C++ sources\nmake backup        Dump device flash to backup/full_flash_16MB.bin\nmake release-tag   Tag a release locally (VERSION=v0.1.0)\nmake clean         Remove build artifacts\n```\n\n## Versioning\n\n`VERSION` is the project version source. `tools/check_version.py` verifies\nthat it matches the SignalK plugin package metadata. PlatformIO runs\n`tools/version.py` before each build and injects:\n\n- `FW_VERSION` from `VERSION`, or `ESPDISP_VERSION` when set by release CI.\n- `FW_GIT_COMMIT` from `GITHUB_SHA` or local Git.\n- `PIO_ENV` from the active PlatformIO environment.\n\nThe firmware exposes those fields through device identity, mDNS discovery,\nmanager registration, `/api/state`, and OTA confirmation payloads. The\nSignalK plugin reports its version from its `package.json`.\n\nLocal version commands:\n\n```sh\nmake version-check\nmake version-set VERSION=0.1.1\nmake build\nmake build PROJECT_VERSION=0.1.1-dev\n```\n\nRelease tags must match the `VERSION` file, for example `v0.1.0`. Tagged\nGitHub releases build every supported firmware target, package the matching\n`signalk-espdisp-manager-\u003cversion\u003e.tgz` plugin, and publish checksums for all\nrelease artifacts. Use those release assets for normal SignalK installs.\n\nTo install the SignalK plugin from the release asset or build a local package,\nsee\n[SignalK plugin install](signalk/README.md#install-esp-display-manager-from-this-repo).\n\n## Console commands\n\nSend these over the serial monitor (`make monitor`) or BLE (`make ble`):\n\n| Command | Effect |\n|---------|--------|\n| `wifi \u003cssid\u003e \u003cpass\u003e` | Save WiFi credentials and reboot |\n| `wifi-forget` | Clear credentials, fall back to AP `espdisp-setup` |\n| `ip` | Print current IP / mode / RSSI |\n| `id` / `id \u003cname\u003e` / `id auto` | Show, set, or restore the hardware-derived device id |\n| `scan` | List visible 2.4 GHz networks |\n| `sk \u003chost\u003e [port]` | Save SignalK server target and reboot |\n| `sk-status` | Print SignalK connection state + age of last delta |\n| `sk-dump` | Print currently-parsed values of every tracked field |\n| `screen \u003cid\\|next\\|prev\\|N\u003e` | Switch screens (ids: dashboard wind nav depth steering route autopilot trip status wifi) |\n| `theme \u003cday\\|night\u003e` | Switch palette (saved to NVS) |\n| `pos-format \u003cddm\\|dd\\|dms\u003e` | Lat/lon formatting; ddm is marine default |\n| `trip-reset` | Zero trip distance / time / max-speed |\n| `mob` / `mob-clear` | Trigger / clear Man Overboard |\n| `demo [N]` / `demo-off` | Auto-cycle screens every N seconds |\n| `fps` / `bench` | Toggle FPS overlay / dump rendering stats |\n| `reboot` | Soft restart |\n\n## BLE access\n\nThe device advertises as `espdisp` with **two** GATT services:\n\n### 1. Nordic UART (text console)\n\nUUID `6E400001-B5A3-F393-E0A3-9F4DD9E3A05A` — line-oriented, same commands\nas the serial console. Subscribe to TX `6E400003-…` for streamed logs;\nwrite UTF-8 lines to RX `6E400002-…`.\n\n```sh\nmake ble                       # sends `ip` + `sk-status`, then streams logs\nmake ble-cmd CMD=\"sk-status\"   # one-shot command\n```\n\n### 2. boat-mfd config service (structured)\n\nService UUID `a3f7e000-7a6b-4f47-b3a5-c4d2e5f6a000` — intended for a\ncompanion mobile app (task #26).\n\n| Characteristic | UUID suffix | Props | Payload |\n|---|---|---|---|\n| **CONNECTION** | `…e001…` | Read · Write · Notify | JSON: `{ \"wifi\": {ssid, ip, rssi, mode}, \"sk\": {host, port, state}, \"device\": {uptime_ms, heap_free, psram_free} }` |\n| **CONFIGURATION** | `…e003…` | Read · Write · Notify | Layout JSON (same schema as the SignalK resource at `configuration.boat-mfd.layouts`), up to 512 B |\n\nWrite to CONNECTION with a partial JSON to update WiFi or SignalK target:\n\n```jsonc\n{ \"wifi\": { \"ssid\": \"MyHomeNet\", \"password\": \"secret\" } }   // saves + reboots\n{ \"wifi\": { \"forget\": true } }                              // clears creds + reboots\n{ \"sk\": { \"host\": \"192.168.1.100\", \"port\": 3000 } }          // saves + reboots\n```\n\nWrite to CONFIGURATION with a complete layout JSON to replace the live config.\nReads return the last successfully applied document **only if it fits in 512\nbytes** (the BLE attribute-value cap per the BT spec). Larger layouts return\na JSON summary stub:\n\n```json\n{ \"truncated\": true, \"size\": 917, \"screen_count\": 1, \"alarm_count\": 2,\n  \"default_screen\": \"dashboard\" }\n```\n\nFor full-layout transfer above 512 B, smartphone apps should use SignalK's\nREST endpoint (`PUT /signalk/v1/api/vessels/self/configuration/boat-mfd/layouts/value`)\nand trigger a re-load via the device's `layout-fetch` command. Native BLE\nchunked transfer is on the roadmap (see task #20).\n\n## Running with synthetic data\n\nThe local and remote demo stacks are for development and repeatable testing.\nFor a real boat network, use [Boat setup](docs/boat-setup.md) instead.\n\nTo exercise the firmware without a boat:\n\n```sh\nmake demo-up\n#   - starts signalk/signalk-server in Docker on :3000\n#   - launches tools/fake_boat.py that pushes sinusoidal nav data\nmake demo-down\n```\n\n`fake_boat.py` connects to SignalK as an authenticated provider and emits\ndeltas for navigation, wind, depth, water temperature, battery, and tanks\nonce per second.\n\n### Development lab rig (remote SignalK + dedicated AP)\n\nThe remote lab workflow is intentionally separate from the onboard setup. It\nuses a Docker-capable Linux mini-PC (`nav-server`) to run SignalK and broadcast\na dedicated `esp-lab` AP for repeatable development tests. My lab host is a\n[Compulab IOT-GATE-IMX8PLUS industrial ARM IoT gateway](https://www.compulab.com/products/iot-gateways/iot-gate-imx8plus-industrial-arm-iot-gateway/).\n\nSee [Lab topology](docs/lab-topology.md) for the full development diagram,\nremote demo commands, `nav-server` setup, and Router/Starlink/etc. WAN-router\nrouting notes. Normal boat installs should use [Boat setup](docs/boat-setup.md).\n\n### NMEA 0183 over WiFi\n\nThe demo SignalK server can also expose NMEA 0183 over WiFi using the official\nSignalK plugin `@signalk/signalk-to-nmea0183`.\n\nConfigured service ports:\n\n```text\nSignalK HTTP/WebSocket: 3000/tcp\nNMEA 0183 TCP:         10110/tcp\n```\n\nThe plugin converts SignalK deltas to NMEA 0183 and publishes them through\nSignalK's built-in `nmea-tcp` interface. In the local test server, the plugin\nis configured with every supported conversion enabled at a 1000 ms minimum\ninterval; sentences only emit when their required SignalK paths have data.\n\nUseful test:\n\n```sh\nnc localhost 10110\n```\n\nExpected demo output includes `GGA`, `RMC`, `HDT`, `MWV`, `VWR`, `VWT`, `DBT`,\nand `MTW` when `tools/fake_boat.py` is pushing data.\n\nIf rebuilding the local SignalK container from scratch, install and enable:\n\n```sh\n./signalk/scripts/run.sh\n```\n\nThe repo-owned config in `signalk/config` installs and enables:\n\n```text\n@signalk/signalk-to-nmea0183\nServer setting: interfaces.nmea-tcp = true\n```\n\n### Autopilot command simulator\n\nSignalK can run an autopilot simulator using the official plugin\n`@signalk/signalk-autopilot` with its `emulator` backend. This gives the\nfirmware a queryable endpoint for testing autopilot commands without a real\npilot on the network.\n\nInstall and enable it with the repo-owned SignalK test config:\n\n```sh\n./signalk/scripts/run.sh\n```\n\nThe config in `signalk/config/plugin-config-data/autopilot.json` enables:\n\n```text\n@signalk/signalk-autopilot\ntype: emulator\n```\n\nUseful query paths:\n\n```text\n/signalk/v1/api/vessels/self/steering/autopilot/state/value\n/signalk/v1/api/vessels/self/steering/autopilot/target/headingMagnetic/value\n```\n\nAuthenticated PUTs to `steering.autopilot.state` can be verified by reading\nthe state path back. The emulator also supports `actions.adjustHeading`; the\ncurrent firmware target-heading command uses `target.headingTrue`, which is\nnot the emulator's writable heading target.\n\n## Layout configuration (work in progress)\n\nMulti-screen layouts are described by a JSON document on the SignalK server\n(`configuration.boat-mfd.layouts`). The device fetches the config at boot,\nfalls back to a baked-in default if unreachable, and re-fetches on reconnect.\n\n### Schema\n\n```jsonc\n{\n  \"version\": 1,\n  \"settings\": {\n    \"default_screen\": \"dashboard\",\n    \"demo_period_ms\": 3000\n  },\n  \"screens\": [\n    {\n      \"id\": \"dashboard\",\n      \"title\": \"Dashboard\",\n      \"type\": \"quadrants\",\n      \"tiles\": [\n        {\n          \"id\": \"wind\",\n          \"title\": \"WIND\",\n          \"type\": \"wind\",\n          \"paths\": {\n            \"awa\": \"environment.wind.angleApparent\",\n            \"aws\": \"environment.wind.speedApparent\"\n          }\n        }\n      ]\n    },\n    {\n      \"id\": \"steering\",\n      \"title\": \"Steering\",\n      \"type\": \"steering\",\n      \"paths\": {\n        \"hdg\": \"navigation.headingTrue\",\n        \"cts\": \"navigation.courseRhumbline.courseToSteer\",\n        \"xte\": \"navigation.courseRhumbline.crossTrackError\"\n      }\n    }\n  ],\n  \"alarms\": [\n    {\n      \"id\": \"shallow\",\n      \"path\": \"environment.depth.belowTransducer\",\n      \"level\": \"alarm\",\n      \"lt\": 3.0,\n      \"message\": \"SHALLOW WATER\"\n    }\n  ]\n}\n```\n\n### Field reference\n\n| Field | Allowed values |\n|-------|----------------|\n| `screens[].type` | `quadrants` \u0026middot; `steering` \u0026middot; `autopilot` \u0026middot; `route` \u0026middot; `trip` \u0026middot; `chart` |\n| `screens[].tiles[].type` | `wind` \u0026middot; `nav` \u0026middot; `depth_temp` \u0026middot; `device_status` \u0026middot; `big_number` \u0026middot; `compass` |\n| `alarms[].level` | `info` \u0026middot; `warn` \u0026middot; `alarm` \u0026middot; `emergency` |\n| `alarms[].lt` / `.gt` | Number — trigger when the path's value crosses below `lt` or above `gt` |\n\nBounds (compile-time, see `include/layout.h`): max 8 screens, 4 tiles per\nscreen, 6 path bindings per object, 8 alarms. Strings truncate to 32\nchars for ids/titles, 96 for SignalK paths.\n\n### Status\n\n- Schema defined + host-portable parser with 9 unit tests passing — `include/layout.h`, `src/layout.cpp`\n- Fetcher (SignalK REST) and LVGL renderer not yet wired (tracked in task #7)\n\n## Architecture\n\n```\n                 +-------------------+\n                 |  ESP32-4848S040   |\n                 |  ESP32-S3-N16R8   |\n                 +---------+---------+\n                           |\n        +---- WiFi --------+--------- BLE --------+\n        |                                         |\n        v                                         v\n SignalK WebSocket                          Nordic UART\n ws://host:3000/                            (logs + commands)\n signalk/v1/stream\n        |\n        v\n +------+---------+\n | signalk_parser | -- applyDelta(json, Data) ----\u003e sk::data\n +----------------+\n        |\n        v\n +----------------+\n |   LVGL UI      | -- 5 Hz refresh from sk::data\n |  4 quadrants   |\n +----------------+\n```\n\n| File | Purpose |\n|------|---------|\n| `src/main.cpp` | Display + touch init, LVGL UI, main loop |\n| `src/net.cpp` | WiFi STA/AP, ArduinoOTA, mDNS, BLE GATT, multi-target logging |\n| `src/signalk.cpp` | WebSocket client, subscription, NVS-persisted target |\n| `src/signalk_parser.cpp` | Pure delta parser (host-portable, unit tested) |\n| `include/board_pins.h` | GPIO map for the supported board |\n| `include/lv_conf.h` | LVGL build configuration |\n| `include/secrets.h.example` | Template for WiFi/OTA credentials |\n| `tools/ble_console.py` | BLE debug / config tool |\n| `tools/fake_boat.py` | Synthetic SignalK data pusher |\n| `tools/dump_chunked.sh` | Chunked, resumable full flash backup |\n\n## Testing\n\n```sh\nmake test\n```\n\nUnit tests live under `test/test_parser/` and run under PlatformIO's `native`\nenvironment (Unity + ArduinoJson). The parser deliberately has no Arduino\ndependencies, so the same code path that runs on the device is exercised on\nthe CI host. Tests cover every supported SignalK path, partial / malformed\npayloads, and keep-alive frames.\n\n## Releasing\n\nMaintainers cut releases by tagging:\n\n```sh\nmake release-tag VERSION=v0.1.0\ngit push origin v0.1.0\n```\n\nFor tagged releases, GitHub builds with `ESPDISP_VERSION=${TAG_NAME#v}` so\nfirmware version `0.1.0` corresponds to Git tag `v0.1.0`.\n\nThe `release.yml` workflow builds all supported firmware targets from\n`release-*` PlatformIO environments on push of a `v*` tag. These profiles keep\nthe same board IDs as development builds, but compile with\n`ESPDISP_RELEASE_BUILD=1`, `CORE_DEBUG_LEVEL=0`, and debug/test controls\ndisabled. The release publishes target-prefixed `firmware.bin`,\n`merged_firmware.bin`, ELF, bootloader, partition table, plugin package, and\nSHA-256 sums to the GitHub release, and generates release notes from commits\nsince the previous tag.\n\nThe merged image names are part of the SignalK firmware-catalog contract:\n`esp32-4848s040-merged_firmware.bin`,\n`waveshare-touch-lcd-4-merged_firmware.bin`, and the other supported target\nnames must be present alongside `SHA256SUMS` for the plugin to import\nupgradable versions from GitHub.\n\nPre-releases are detected automatically: tags matching `*-rc*`, `*-alpha*`,\nor `*-beta*` are marked as pre-release.\n\n## Roadmap\n\n- [ ] Move position (lat/lon) into the Nav quadrant; promote Status to a device-health panel\n- [ ] Multi-screen layouts with server-managed configuration (JSON document on SignalK)\n- [ ] Triple-tap to expand a tile to fullscreen; triple-tap again to restore\n- [ ] Swipe gestures to scroll between screens\n- [ ] Advanced screens: compass rose, AIS targets, engine, anchor watch, tank levels, history graphs\n- [ ] Raster chart display fed by a SignalK charts plugin\n- [ ] NMEA 0183 input via RS-422 transceiver on a free UART\n- [ ] Optional NMEA 2000 (CAN) support\n- [ ] NVS caching of last-known config so the device boots into the right layout without network\n\n## Related projects\n\n| Project | Scope | License |\n|---------|-------|---------|\n| [`pypilot/pypilot_mfd`](https://github.com/pypilot/pypilot_mfd) | ESP32-S3 MFD: NMEA 0183 + SignalK + pypilot integration | GPLv3 |\n| [`mxtommy/Kip`](https://github.com/mxtommy/Kip) | Web-based SignalK instrument package | — |\n| [`mrstas/SC01_PLUS_MARINE_INSTRUMENTS`](https://github.com/mrstas/SC01_PLUS_MARINE_INSTRUMENTS) | SignalK instruments on Panlee SC01 Plus | GPLv3 |\n| [`SignalK/SensESP`](https://github.com/SignalK/SensESP) | Sensor-side ESP32 framework (good companion) | Apache 2.0 |\n| [`open-boat-projects-org/esp32-nmea2000-obp60`](https://github.com/open-boat-projects-org/esp32-nmea2000-obp60) | N2K gateway with OBP60 e-ink display | — |\n\n## Contributing\n\nBug reports, board ports, and PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)\nfor development workflow and conventions. By contributing, you agree that\nyour contributions are licensed under PolyForm Noncommercial 1.0.0 (the\nproject license).\n\n## Commercial use\n\nThis firmware is **not** licensed for commercial use under the default terms.\n\"Commercial use\" includes selling the firmware, bundling it with hardware sold\nfor profit, integrating it into a paid service, or using it as part of a\ncommercial operation (e.g. charter fleets, paid installations).\n\nFor commercial licensing, open a\n[GitHub Discussion](https://github.com/navado/esp32-boat-mfd/discussions) or\nfile an issue marked `licensing` to start the conversation.\n\nNoncommercial uses — personal boats, research, education, charitable and\ngovernmental organizations — are explicitly permitted under the project\nlicense.\n\n## License\n\n[PolyForm Noncommercial 1.0.0](LICENSE) © 2026 navado and contributors.\n\nThis project bundles and links against the following libraries, each under\nits own license:\n\n| Library | License |\n|---------|---------|\n| LVGL | MIT |\n| Arduino_GFX | MIT |\n| NimBLE-Arduino | Apache 2.0 |\n| WebSockets | LGPL-2.1 |\n| ArduinoJson | MIT |\n| Arduino-ESP32 | LGPL-2.1 |\n\nThese are unmodified upstream dependencies and remain governed by their\nrespective licenses.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnavado%2Fesp32-boat-mfd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnavado%2Fesp32-boat-mfd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnavado%2Fesp32-boat-mfd/lists"}