Skip to content

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 modularityModule (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.

  • main entry point on PC; CMake and PlatformIO builds for PC and esp32dev.
  • 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.

  • Module base class with setup() / loop() / teardown().
  • Scheduler registers modules, drives the hot path, reports per-module ms and FPS.
  • HelloModule increments a counter in loop() 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 RGB struct; dynamically-sized channels array allocated in setup().
  • 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:

  1. ModuleManagermain.cpp hardcodes the module list; introduce a ModuleManager that owns persistent JSON config, instantiates modules, and calls scheduler.add(). Scheduler stays a minimal execution engine. Architecture
  2. 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
  3. Layer typesRGB and Channel are domain-specific and don't belong in src/core/; introduce ProducerLayer and ConsumerLayer module types in src/modules/layer/. Architecture
  4. Blend stepPreviewModule should not hold a SineEffectModule*; ConsumerLayer blends all registered ProducerLayers and PreviewModule reads only from that. Architecture
  5. 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.h and Channel.h moved to src/modules/layer/.
  • ProducerLayer and ConsumerLayer introduced; SineEffectModule writes into a ProducerLayer, PreviewModule reads from a ConsumerLayer.
  • ConsumerLayer::loop() blends all registered ProducerLayers.
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.

  • ModuleManager reads state/modulemanager.json, instantiates modules by type name, wires layer references, calls scheduler.add().
  • REGISTER_MODULE() macro populates a TypeRegistry at link time — no RTTI, no manual factory.
  • main.cpp reduced 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
  • addControl pattern — direct-pointer binding: addControl(variable, name, type, min, max) stores the variable address; setControl writes through without JSON per tick; virtual onUpdate(key) for reactions. Design
  • DebouncesetControl on StatefulModule; filesystem flush (500 ms idle) in ModuleManager.
  • 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 + getSchema on StatefulModule; schema versioned ("schema": "0.1").
  • KvStore — fixed-capacity typed-slot table (float, uint8_t, bool); hashed keys, lock-free reads.
  • BrightnessModifierModule reads a brightness scalar from KV each loop(); SineEffectModule publishes it; both blend into ConsumerLayer.
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 / serves frontend_bundle.h.
  • GET /api/modules returns the schema array; POST /api/modules/{id}/controls/{key} calls setControl, 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: ConsumerLayer snapshot → 5-byte header + raw RGB → gl.texImage2D WebGL 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.