Goal: stop entering book data on a 32-key touchscreen keyboard. Also make the firmware work for every hobby simultaneously. Also put it in a box.
Spoiler: all three happened. One required power tools.
The Web UI
2.8 inches of touchscreen is fine for a WiFi password you type once. It is not fine for entering titles, dates, and star ratings across 50 books a year.
The ESP32 runs a web server. Connect to WiFi → settings screen shows a QR code → scan it → you’re in a glassmorphism web UI served directly from PROGMEM.
The background actually comes from the device. /api/bg streams the current RGB565 frame raw.
The browser decodes it via the Canvas API and sets it as the page background.
It matches the display exactly — because it is the display.
Features:
- Year tabs
- Add / delete entries
- Live card titles
- Star ratings
- Started / finished dates
API surface (minimal and boring on purpose):
GET /api/data — item list for year
GET /api/years — available years
POST /api/item — add or update
POST /api/deleteitem — remove
Data lives in /books_2026.json on LittleFS.
Previous years remain untouched.
NTP sync sets the year automatically on WiFi connect. If offline, there’s a manual year screen with big ± buttons.
22 Variants, One File
At some point I wrote down every metric I’ve ever wanted to track. It was 22.
Options:
- 22 copies of
main.cpp(no) - One codebase + per-variant config (yes)
Each variant defines six macros:
#define VARIANT_ID "gym"
#define COUNTER_LABEL "GYM VISITS"
#define WEB_TITLE "Gym"
#define WEB_HEADING "★ Gym Visits"
#define WEB_ADD_BTN "+ Log Visit"
#define WEB_CFG_JS "var CFG={pfx:'Session',ph:'Notes (optional)',d1:'Date',d2:'',showD2:false,showRating:false};"
VARIANT_ID sets the LittleFS filename prefix, so each device’s data is isolated.
WEB_CFG_JS is injected into the PROGMEM HTML at build time.
The web UI reads it and decides:
- Show second date field?
- Show star rating?
A gym session doesn’t need a “Finished” date. A book does.
There are 22 [env:cyd-*] entries in platformio.ini, each pointing to its own config via:
-I firmware/cyd/variants/<name>
Flash any variant:
pio run -e cyd-yoga --target upload
One codebase.
One platformio.ini.
22 different devices.

Port Detection Detour
upload_port = COM15 was fragile because Windows reassigns COM numbers on reconnect.
Tried the “correct” fix:
HWGREP://1A86:7523
(PySerial pattern matching CH340 VID:PID.)
Result:
A fatal error occurred: Could not open HWGREP://1A86:7523
(OSError(22, 'The filename, directory name, or volume label syntax is incorrect.'))
When upload_port is set explicitly, PlatformIO skips its resolution phase
and hands the string directly to esptool.
Esptool sees HWGREP://... and (correctly) concludes that is not a serial port.
Fix: delete upload_port entirely.
Auto-detect works perfectly when only one ESP32 is connected. Time spent: longer than I’d like to admit.
The Case
Found a 3D model for the CYD. Printed it. It looked great.
The board has two connectors:
- USB-C (power)
- micro-USB (flashing)
The model had one cutout.
Got out the Dremel.
Opened the second port manually. It is not a perfect rectangle. Nobody will ever see it.
The front looks like a finished product. The back looks like a finished product that had a brief disagreement with a rotary tool and moved on.
Both ports are accessible. Board doesn’t rattle. Case clicks shut. Calling it done.
End of Day 2 State
- Web UI in PROGMEM, backgrounds fetched live from the device
- Multi-year storage, NTP sync, manual year fallback when offline
- 22 build variants: books, movies, TV series, anime, video games, gym, yoga, hiking, fishing, board games, cities, countries, restaurants, craft, resin, art, miniatures, videos, designs, orders, clients, products
- Port detection: auto-detect, no hardcoded COM
- Physical hardware in a case, both ports usable, Dremel retired
Next
- Per-variant background image sets (currently all share
data/) - Laser Cut Acrylic Case
Julia M
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.