Boat project

CYD touchscreen display

A live boat data display built on the ESP32-2432S028 "Cheap Yellow Display" — rotating pages showing power, tanks, depth, water temp and tides. Touch to advance pages.

What it does

This firmware runs on a CYD (Cheap Yellow Display) — the ESP32-2432S028, an all-in-one ESP32 development board with a 2.8" 320×240 TFT touchscreen built in. It connects to the boat's WiFi, then opens a WebSocket connection directly to the Signal K server and subscribes to all data updates. It also fetches tide predictions from the UK Admiralty Tidal API over HTTPS. The display rotates through five pages every 15 seconds; a touch anywhere on the screen advances to the next page immediately.

WiFi credentials are stored in LittleFS (the ESP32's flash filesystem) and can be set via a browser-accessible web server on the device — no need to recompile when moving between networks.

Hardware

ComponentNotes
ESP32-2432S028 (CYD)All-in-one: ESP32, 2.8" TFT ILI9341/ST7789, XPT2046 resistive touchscreen. Available for under £10 from AliExpress.
TFT display (320×240)Driven by TFT_eSPI library. Backlight on GPIO 21.
XPT2046 touchscreenSeparate SPI bus. IRQ on GPIO 36, CS on GPIO 33.
Power5V USB or 5V boat supply via USB connector

The CYD has a known quirk: the display backlight must be enabled by setting GPIO 21 HIGH before initialising TFT_eSPI, otherwise the screen stays dark. This is handled in setup().

Code structure

The entire project lives in one file — src/main.cpp — inside the folder 01122024-2211 SK NPT tides fuel graphical - working (the folder name is a timestamp from when that working version was saved). This is a PlatformIO project.

Section in main.cppPurpose
Global variablesAll live data values — voltage, tank percentages, depth, water temp, power in/out. Updated by the WebSocket event handler.
webSocketEvent()Receives JSON updates from Signal K. Parses the updates[].values[] array and stores values in global variables by matching path strings.
fetchTideData()HTTPS GET to the Admiralty API. Returns JSON; parses EventType and DateTime for the next 4 tide events. Uses WiFiClientSecure with certificate validation disabled (setInsecure()).
drawHeader() / updateHeaderTime()Renders the "SEA & SEA" header and a live NTP clock at the top of every page.
drawBody()Generic text-page renderer — centres multiple lines of text in the body area below the header.
drawTankIcon()Animated tank graphic — draws a rounded rectangle outline and fills it pixel-row by pixel-row to animate the level.
displayPage1()displayPage4()Individual page renderers using drawBody(). Page 1: power/voltage. Page 2: all tank levels as text. Page 3: IP address, depth, water temp. Page 4: next 4 tide events.
displayPageTankIcons()Page 5 — graphical animated tank display. Five tank columns (3 water, 2 fuel) drawn side by side.
setup()Backlight, TFT init, touchscreen init, LittleFS mount, WiFi connect, NTP sync, web server start, WebSocket connect, initial tide fetch.
loop()WebSocket events, NTP updates, tide refresh (every 10 minutes), page auto-rotation (every 15 seconds), touch input handling.

Signal K paths consumed

Signal K pathDisplay usage
electrical.batteries.bmv712.voltagePage 1 — main battery voltage
electrical.batteries.bmv712.powerPage 1 — power in/out (W)
tanks.water.starboard.currentLevelPages 2 & 5 — starboard water tank %
tanks.water.centre.currentLevelPages 2 & 5 — centre water tank %
tanks.water.port.currentLevelPages 2 & 5 — port water tank %
tanks.fuel.starboard.currentLevelPages 2 & 5 — starboard fuel tank %
tanks.fuel.port.currentLevelPages 2 & 5 — port fuel tank %
environment.depth.belowKeelPage 3 — depth below keel (m)
environment.water.temperaturePage 3 — water temperature (K → °C)

Key things to customise

What to changeWhereNotes
WiFi credentials Stored in LittleFS at /ssid.txt and /pass.txt Set via the web server at the device's IP address on first boot. Or pre-write the files using PlatformIO's LittleFS upload tool.
Signal K server address webSocket.begin("10.1.1.12", 3000, "/signalk/v1/stream") in setup() Change the IP to your Signal K server. Port 3000 is the default.
Tide station apiEndpoint — change 0122A to your UKHO station ID Station IDs are in the UK Admiralty API. 0122A is North Fambridge (River Crouch).
Admiralty API key const char* apiKey = "..." near the top of main.cpp Register for a free key at admiraltyapi.portal.azure-api.net. The free tier includes 10,000 calls/month.
Page rotation interval if (now - pageTimer >= 15000) in loop() Change 15000 to any millisecond value. 15000 = 15 seconds.
Which pages to show / order The switch (currentPage) block and currentPage % 5 Add, remove or reorder cases. Change the modulus to match the number of pages.
Tank colours TFT_DIESEL_BROWN and TFT_WATER_BLUE definitions Any RGB565 colour value. Use an online RGB565 converter to get the hex constant.

Libraries required

LibraryPurpose
TFT_eSPITFT display driver. Requires User_Setup.h configured for CYD pin mapping.
XPT2046_TouchscreenResistive touchscreen driver
ESPAsyncWebServer / AsyncTCPNon-blocking web server for WiFi credential entry
WebSocketsClient (Links2004)WebSocket client for Signal K stream
ArduinoJsonJSON parsing of Signal K updates and tide API responses
NTPClientNetwork time sync for the clock display
LittleFSFlash filesystem for storing WiFi credentials

The TFT_eSPI library needs a User_Setup.h that defines the CYD's pin assignments. The correct setup for the ESP32-2432S028 is in the library's examples folder, or search for "CYD TFT_eSPI setup".

How the Signal K WebSocket connection works

Signal K exposes a WebSocket stream at ws://[server]:3000/signalk/v1/stream. Once connected, Signal K sends a hello message, then begins streaming JSON update messages whenever any sensor value changes. Each message looks like:

{"updates":[{"source":{...},"values":[{"path":"tanks.water.port.currentLevel","value":0.65}]}]}

The webSocketEvent() function in this project iterates over the updates and values arrays on every received message, matches path strings against a set of known paths, and stores the value in the corresponding global variable. The display then reads those globals on the next page render cycle.

No subscription filtering is applied — the device receives all updates from the Signal K server. For a small boat network this is fine; for a busier network you can send a subscription request to limit the paths received.