Release 1 — Proof of Concept¶
Status: Complete | Sprints: 10 | GitHub Release tag: (pending)
Release 1 proves the core idea end to end: a cross-platform module runtime with a scheduler hot path, producer/consumer pixel pipeline, JSON-driven UI served from the device, and automated CI on PC and ESP32. No real LED output yet — that is Release 2 territory.
Sprint 1 — Project Vision and Architecture¶
Design-only sprint. Four Q&S (Questions and Suggestions) cycles; no code produced.
Key decisions made:
- Unit of modularity —
Module(not "nodes and noodles");setup()/loop()/teardown()lifecycle. Architecture - Platform priority — ESP32, PC, rPi as peer targets; PC first for fast iteration.
- Frontend — no framework; HTML/CSS/JS driven by JSON metadata served from the device at runtime.
- Hot path — all active module loops driven as fast as possible; soft 20 ms / 50 Hz target, log-only enforcement.
- Concurrency — producer/consumer model with double buffering; channels array with blending for bulk pixel data.
- Module configuration — controls (persisted, UI-mutable) separated from data-flow inputs (injected via Layers); modules are default-constructible.
- State persistence — LittleFS on ESP32; JSON state blob per module; load before
setup(), save on teardown. - CI matrix — PC macOS + one ESP32 build; doctest as the test framework.
- PAL design — two decoupled axes: ESP-IDF/Arduino and ESP32/rPi/PC.
- Living documents written: architecture.md, standards.md, deploy.md.
Sprint 2 — Hello, Scheduler¶
Goal: smallest green CI run — toolchain proven end to end, no Modules yet.
mainentry point on PC; CMake and PlatformIO builds for PC andesp32dev.- doctest wired in; one trivial test passes in CI.
- GitHub Actions workflow: PC build + test suite; ESP32 compile.
| Metric | Value |
|---|---|
| Tests | 1 total, 1 assertion |
| ESP32 RAM | 4.2% (13 724 B / 327 680 B) |
| ESP32 Flash | 10.9% (142 569 B / 1 310 720 B) |
Sprint 3 — Module Lifecycle¶
Goal: one Module the Scheduler can call.
Modulebase class withsetup()/loop()/teardown().- Scheduler registers modules, drives the hot path, reports per-module ms and FPS.
HelloModuleincrements a counter inloop()and logs every N ticks.
| Metric | Value |
|---|---|
| Tests | 4 total, 14 assertions |
| PC fps | ~196 K |
| ESP32 fps | ~314 |
| ESP32 RAM | 4.2% (13 828 B / 327 680 B) |
| ESP32 Flash | 11.4% (149 025 B / 1 310 720 B) |
Sprint 4 — Pixel Pipeline¶
Goal: producer/consumer hand-off with real pixel data.
- Plain
RGBstruct; dynamically-sized channels array allocated insetup(). SineEffectModule(producer) writes a 2D sine pattern;PreviewModule(consumer) logs a frame checksum.
| Metric | Value |
|---|---|
| Tests | 7 total, 23 assertions |
| PC fps | ~196 K |
| ESP32 fps | ~257 (SineEffect 3.9 ms / 16×16) |
| ESP32 RAM | 4.2% (13 828 B / 327 680 B) |
| ESP32 Flash | 11.4% (149 025 B / 1 310 720 B) |
Retrospective — five architectural decisions for upcoming sprints:
- ModuleManager —
main.cpphardcodes the module list; introduce aModuleManagerthat owns persistent JSON config, instantiates modules, and callsscheduler.add(). Scheduler stays a minimal execution engine. Architecture - Controls vs data-flow inputs — modules are default-constructible; parameters are either persisted controls (UI-mutable) or data-flow inputs (injected by other modules via Layers). Design
- Layer types —
RGBandChannelare domain-specific and don't belong insrc/core/; introduceProducerLayerandConsumerLayermodule types insrc/modules/layer/. Architecture - Blend step —
PreviewModuleshould not hold aSineEffectModule*;ConsumerLayerblends all registeredProducerLayers andPreviewModulereads only from that. Architecture - State persistence — modules expose a JSON state blob; runtime loads it before
setup()and saves it on teardown. Architecture
Sprint 5 — Layer Refactor¶
Goal: remove domain-specific types from src/core/; decouple effect modules from each other via layers.
RGB.handChannel.hmoved tosrc/modules/layer/.ProducerLayerandConsumerLayerintroduced;SineEffectModulewrites into aProducerLayer,PreviewModulereads from aConsumerLayer.ConsumerLayer::loop()blends all registeredProducerLayers.
| Metric | Value |
|---|---|
| Tests | 13 total, 86 assertions |
| PC fps | ~434 K (5 modules) |
| ESP32 fps | ~245 |
| ESP32 RAM | 4.2% (+64 B) |
| ESP32 Flash | 11.4% (+1 016 B) |
Sprint 6 — Runtime Introspection¶
Goal: memory logging and automated health checks for observability without a GUI.
- Scheduler reports RAM / PSRAM / FS stats each tick (platform-abstracted).
- Each module implements
healthReport()— a lightweight state snapshot logged periodically, used by automated tests to verify data flows end to end.
| Metric | Value |
|---|---|
| Tests | 17 total, 54 assertions |
| PC fps | ~360 K |
| ESP32 fps | ~244 |
| ESP32 RAM | 4.3% (+264 B) |
| ESP32 Flash | 12.3% (+10 716 B) |
Per-module char[64] health buffer is a known cost (~64 B/module); tracked in the Backlog.
Sprint 7 — ModuleManager and Persistence¶
Goal: replace hardcoded module list in main.cpp with a JSON-driven ModuleManager.
ModuleManagerreadsstate/modulemanager.json, instantiates modules by type name, wires layer references, callsscheduler.add().REGISTER_MODULE()macro populates aTypeRegistryat link time — no RTTI, no manual factory.main.cppreduced to: create Scheduler, create ModuleManager, add it, run.- Per-module state persistence: load before
setup(), flush on teardown.
| Metric | Value |
|---|---|
| Tests | 24 total, 72 assertions |
| PC fps | ~175 K |
| ESP32 fps | ~248 |
| ESP32 RAM | 4.3% (−132 B, linker reorder) |
| ESP32 Flash | 15.3% (+39 312 B — ArduinoJson + LittleFS + ModuleManager) |
Key design decisions for upcoming sprints:
- HTTP server — ESPAsyncWebServer (ESP32), cpp-httplib (PC/rPi). Architecture
addControlpattern — direct-pointer binding:addControl(variable, name, type, min, max)stores the variable address;setControlwrites through without JSON per tick;virtual onUpdate(key)for reactions. Design- Debounce —
setControlonStatefulModule; filesystem flush (500 ms idle) inModuleManager. - Frontend serving — HTML/CSS/JS compiled into
frontend_bundle.h; served from flash. - Pixel data transport — binary WebSocket frames: 5-byte header (type, width LE16, height LE16) + raw RGB bytes.
Sprint 8 — Control Schema and Key/Value Store¶
Goal: addControl / setControl / getSchema on StatefulModule, a typed KV store for inter-module data, and a second effect proving the pipeline composes.
addControl+setControl+getSchemaonStatefulModule; schema versioned ("schema": "0.1").KvStore— fixed-capacity typed-slot table (float,uint8_t,bool); hashed keys, lock-free reads.BrightnessModifierModulereads a brightness scalar from KV eachloop();SineEffectModulepublishes it; both blend intoConsumerLayer.
| Metric | Value |
|---|---|
| Tests | 59 total, 199 assertions |
| PC fps | ~282 K |
| ESP32 fps | ~146 (two sine effects) |
| ESP32 RAM | 4.4% (+256 B) |
| ESP32 Flash | 15.6% (+3 904 B) |
Per-module sizeof grew from ~84 B to ~312 B (embedded controls_[8] array). Two concurrent sine effects halve the ESP32 tick rate.
Sprint 9 — HTTP Server and Frontend¶
Goal: first human-visible interaction — open a URL, see module controls, change a slider, have it persist.
- HTTP server started after the Scheduler;
GET /servesfrontend_bundle.h. GET /api/modulesreturns the schema array;POST /api/modules/{id}/controls/{key}callssetControl, triggers debounce.- Frontend: flat card list per module, controls populated from last-saved state.
| Metric | Value |
|---|---|
| Tests | 70 total, 241 assertions |
| PC fps | ~282 K |
| ESP32 fps | ~134 |
| ESP32 RAM | 11.2% (+22 692 B) |
| ESP32 Flash | 49.7% (+447 340 B) |
Flash jumped +447 KB: WiFi stack + LwIP (~250 KB), ESPAsyncWebServer (~120 KB), AsyncTCP (~60 KB), embedded bundle (~17 KB). One-time cost — future feature additions will be much smaller deltas.
Sprint 10 — WebSocket, Pixel Preview, and Footprint CI¶
Goal: live frontend (no polling), WebGL pixel preview, automated footprint reporting in CI.
- WebSocket
/ws: on connect sends full module state; each tick (debounced ~20 ms) pushes state + timing JSON. Modules are unaware of WebSocket. - Binary pixel frame:
ConsumerLayersnapshot → 5-byte header + raw RGB →gl.texImage2DWebGL canvas at ~50 Hz. - Footprint CI: GitHub Actions parses ESP32 ELF/map, computes delta vs
main, annotates PR (green / yellow / red vs flash and RAM budgets).
| Metric | Value |
|---|---|
| Tests | 85 total, 284 assertions |
| PC fps | ~304 K |
| ESP32 fps | ~126–133 (under WiFi + WebSocket load) |
| ESP32 RAM | 11.3% (+268 B) |
| ESP32 Flash | 51.7% (+26 012 B) |
Retrospective — Release 1 complete¶
What was proven:
- Cross-platform module runtime (PC + ESP32) with 85 automated tests
- Producer/consumer pixel pipeline with double-buffered blending
- JSON-driven control schema with direct-pointer binding (
addControl) - Inter-module communication via typed KV store
- HTTP server with embedded frontend, live WebSocket push, WebGL preview at ~50 Hz
- Automated footprint CI with PR annotation
What worked well:
- Front-loading design decisions in Sprint 1 Q&S cycles avoided costly rework during implementation.
- The hot path stayed clean throughout — HTTP/WebSocket never touched the scheduler thread.
- doctest + CI caught regressions early; no sprint broke a previous sprint's tests.
Watch points going into Release 2:
- Flash at 51.7% after just the proof of concept — real LED drivers and more effects will push this further.
- ESP32 tick rate (~130 fps under load) is adequate for 50 Hz but leaves limited headroom for complex effects.
controls_[8]per module adds ~224 B statically — fine at 6 modules, worth revisiting at scale.
Ready for Release 2: FastLED integration, real LED output, rPi build, and features tracked in the Backlog.