esptube-clock

Web Control Panel — spec

The clock serves its own control panel. Open http://esptube.local (or the IP, e.g. http://<clock-ip>) from any browser on the same network — phone or laptop. No app, no hosting; the page is embedded in the firmware and talks to the on-board REST API (same-origin).

Source of truth: firmware/custom-fw/data/index.html (self-contained HTML/CSS/JS). A PlatformIO pre-build step (scripts/gen_web_index.py) embeds it into the app as PROGMEM (src/web_index.h) on every build, so editing that one file + reflashing updates the UI — no separate filesystem upload.

Layout & controls

Header — title + a status pill that polls GET /status every 5 s (green dot + IP + free heap when online; “offline” otherwise).

Top bar | Control | Action | Endpoint | |—|—|—| | Clear all | blank every tube | POST /tubes/clear | | Refresh | re-read status now | GET /status | | MODE ◀ ▶ ⏻ | virtual hardware buttons (same actions as the physical ones) | POST /button/{mode\|left\|right\|power} |

The four buttons mirror — and now match — the clock’s physical UP/MODE/DOWN/POWER (GPIO 35/34/39/36, active-low, external pull-ups). Actions: MODE cycles Clock↔Off, POWER toggles Off↔last mode, UP/DOWN (◀/▶) step brightness ±32. Physical and web buttons run the identical handler. (Add more canned modes and MODE-cycle gains more stops.)

Display, modes & clock

| Control | Action | Endpoint | |—|—|—| | Mode: Clock / Off | switch display mode (CLOCK = NTP time; OFF = blank; MANUAL auto-set when you push content) | POST /mode/{clock\|off\|manual} | | Brightness slider | scale the back LEDs (0–255, persisted) | POST /config/brightness {"value":N} | | Timezone field | POSIX TZ for the clock (persisted) | POST /config/tz {"tz":"…"} |

CLOCK renders NTP time across the populated tubes (6→HHMMSS, 5→HH MM + seconds, 4→HHMM), right-aligned, redrawing only changed digits. GET /status reports mode, time_valid, tz, brightness. Default TZ PST8PDT,M3.2.0,M11.1.0 (America/Los_Angeles).

Per-tube cards (one per slot, shown in physical order left→right = index 5…0; dead/unpopulated slots are dimmed) | Control | Action | Endpoint | |—|—|—| | text field + Draw | draw centered text (≤8 chars) | POST /tube/{i}/text (raw body) | | color + Glow | set that tube’s back WS2812 LED | POST /tube/{i}/rgb ({r,g,b}) | | Clear | blank that tube (text + LED off) | text "" + rgb 0,0,0 | | Image (file) | pick any image → resized to 135×240 and BMP-encoded in the browser → sent | POST /tube/{i}/image (BMP bytes) |

Back LEDs — all tubes — one color picker → Set all / All off; loops the populated tubes via POST /tube/{i}/rgb. (Per-tube “Glow” sets one; this sets them all.)

Which tubes are populated — a checkbox per slot + Save layout. Since these panels can’t be auto-detected, you declare which slots have a working display; saved on the device. POST /config/populated {"mask":N} (bit i = slot i). Reflected in GET /config and /status (populated_mask, per-tube populated, dead_tubes). Checkboxes load from the device once, then don’t get clobbered by the background poll while you edit.

Behind the scenes

Ideas / not-yet