Close

Day 2: Web UI, 22 lives, and a Dremel

A project log for ESP32 Personal Library Console

ESP32-based personal library console for tracking reads, organizing TBR, and book discovery.

julia-mJulia M 03/04/2026 at 05:250 Comments

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:

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:

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:

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:

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


Next

Discussions