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.
Project links
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
| Component | Notes |
|---|---|
| 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 touchscreen | Separate SPI bus. IRQ on GPIO 36, CS on GPIO 33. |
| Power | 5V 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.cpp | Purpose |
|---|---|
| Global variables | All 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 path | Display usage |
|---|---|
electrical.batteries.bmv712.voltage | Page 1 — main battery voltage |
electrical.batteries.bmv712.power | Page 1 — power in/out (W) |
tanks.water.starboard.currentLevel | Pages 2 & 5 — starboard water tank % |
tanks.water.centre.currentLevel | Pages 2 & 5 — centre water tank % |
tanks.water.port.currentLevel | Pages 2 & 5 — port water tank % |
tanks.fuel.starboard.currentLevel | Pages 2 & 5 — starboard fuel tank % |
tanks.fuel.port.currentLevel | Pages 2 & 5 — port fuel tank % |
environment.depth.belowKeel | Page 3 — depth below keel (m) |
environment.water.temperature | Page 3 — water temperature (K → °C) |
Key things to customise
| What to change | Where | Notes |
|---|---|---|
| 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
| Library | Purpose |
|---|---|
| TFT_eSPI | TFT display driver. Requires User_Setup.h configured for CYD pin mapping. |
| XPT2046_Touchscreen | Resistive touchscreen driver |
| ESPAsyncWebServer / AsyncTCP | Non-blocking web server for WiFi credential entry |
| WebSocketsClient (Links2004) | WebSocket client for Signal K stream |
| ArduinoJson | JSON parsing of Signal K updates and tide API responses |
| NTPClient | Network time sync for the clock display |
| LittleFS | Flash 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.