--- phase: 01-esp32-firmware plan: 03 subsystem: firmware tags: [esp32, animation-engine, freertos, neopixelbus, rgbw, sk6812, ws2801, animations] requires: - phase: 01-01 provides: config.h constants (ANIM_FPS, BRIGHTNESS_CAP, BRIGHTNESS_MAX, LED counts, FreeRTOS priorities/stacks), led_driver.h strip API (stripWand, stripSchrank, applyBrightnessW/R, showBothStrips) - phase: 01-02 provides: protocol.h (AnimParams, LedCommand, CmdType, Zone), wifi_server.h (commandQueue extern) provides: - AnimTickFn typedef and AnimEntry struct (animation_base.h) - ANIM_REGISTRY with all 8 animation names (exact strings for Pi JSON commands) - ZoneState struct and animation engine state machines (animation_engine.h) - 50fps FreeRTOS task via vTaskDelayUntil (animation_engine.cpp) - W-channel auto-derive formula: min(r, g, b) / 2 (animation_engine.cpp) - All 8 animation implementations (animations/*.cpp) - Startup breathing animation for D-11 affects: [01-04, phase-02-pi-app] tech-stack: added: [esp_random.h for hardware RNG in sparkle, math.h for sinf()] patterns: [AnimTickFn-function-pointer-registry, zone-state-machine-50fps, autoWhite-W-derive] key-files: created: - firmware/src/animations/animation_base.h - firmware/src/animation_engine.h - firmware/src/animation_engine.cpp - firmware/src/animations/chase.cpp - firmware/src/animations/pulse.cpp - firmware/src/animations/rainbow.cpp - firmware/src/animations/strobe.cpp - firmware/src/animations/color_wash.cpp - firmware/src/animations/breathe.cpp - firmware/src/animations/sparkle.cpp - firmware/src/animations/gradient_sweep.cpp modified: [] key-decisions: - "autoWhite formula: min(r,g,b)/2 — warm-white approximation, only triggers when W=0 on RGBW strip" - "vTaskDelayUntil (not vTaskDelay) for drift-free 50fps — accumulates timing debt correction across frames" - "startup breathing at g_brightness=0.15f before WiFi connects — resets to BRIGHTNESS_CAP after WiFi up" - "commandQueue drained non-blocking at start of each tick (not blocking) — animation frame never stalls" - "sparkle uses esp_random() hardware RNG — no pseudo-random seed needed, avoids seeding issues" - "breathe enforces 5% minimum brightness (never fully off) per D-11 alive indicator design" patterns-established: - "AnimTickFn pattern: function pointer signature (uint32_t tick, const AnimParams& params, uint16_t ledCount, bool isRGBW) used by all 8 animations" - "isRGBW branch pattern: every animation handles RGBW (stripWand) and RGB (stripSchrank) via isRGBW param" - "autoWhite call pattern: check w==0 before calling autoWhite() — explicit W values always honored" - "brightness pattern: bright = params.intensity * g_brightness — never access BRIGHTNESS_MAX directly in animations" requirements-completed: [FW-03, FW-04] duration: 14min completed: "2026-04-03" --- # Phase 01 Plan 03: Animation Engine + 8 Animations Summary **50fps FreeRTOS animation engine with ANIM_REGISTRY, zone state machines, W-channel auto-derive, and all 8 named animations (Chase/Pulse/Rainbow/Strobe/ColorWash/Breathe/Sparkle/GradientSweep) with palette-based RGBW support** ## Performance - **Duration:** ~14 min - **Started:** 2026-04-03T11:23:05Z - **Completed:** 2026-04-03T11:28:39Z - **Tasks:** 2 - **Files created:** 11 ## Accomplishments - Animation engine with 50fps FreeRTOS task using vTaskDelayUntil for drift-free timing - ANIM_REGISTRY mapping 8 exact name strings to AnimTickFn pointers (used by Pi JSON "animation" field) - W-channel auto-derive from RGB (D-02): `min(r, g, b) / 2` warm-white approximation - All 8 animations implementing AnimTickFn signature with isRGBW dual-strip support - Startup breathing animation (D-11): both zones breathe at 15% brightness before WiFi connects ## Animation Function Signatures and Parameters All animations implement: `void animFoo(uint32_t tick, const AnimParams& params, uint16_t ledCount, bool isRGBW)` | Animation | params.speed | params.intensity | params.colors | Special | |-----------|-------------|-----------------|--------------|---------| | chase | window position advance per tick | pixel brightness | colors[0] for window | window = ledCount/10 | | pulse | sine wave tempo (0.1x factor) | brightness envelope amplitude | colors[0] solid fill | range 0-100% intensity | | rainbow | hue offset advance rate | applied to HsbColor brightness | ignored (generates own) | uses HsbColor HSV | | strobe | period = 20/speed ticks | on-state brightness | colors[0] for on state | min period = 2 ticks | | color_wash | hold time = 50/speed per color | blend brightness | all colors, linear fade | wraps palette cyclically | | breathe | sine envelope rate (0.05x factor) | envelope amplitude | colors[0] solid fill | 5% min brightness always | | sparkle | not used directly | sparkle_count = ledCount*intensity*0.1 | colors[0] for sparkle | esp_random() hardware RNG | | gradient_sweep | offset = tick*speed*3 | pixel brightness | colors[0], colors[1] blend | wraps if colorCount<2 | ## ANIM_REGISTRY — Exact Name Strings These strings are used by the Pi-side app when sending JSON `"animation"` commands: ``` "chase" → animChase "pulse" → animPulse "rainbow" → animRainbow "strobe" → animStrobe "color_wash" → animColorWash "breathe" → animBreathe "sparkle" → animSparkle "gradient_sweep" → animGradientSweep ``` ## W Auto-Derive Formula (D-02) ```cpp uint8_t autoWhite(uint8_t r, uint8_t g, uint8_t b) { return std::min(r, std::min(g, b)) / 2; } ``` - Only triggered when `params.colors[i][3] == 0` on RGBW strips (isRGBW=true) - Explicit W values (non-zero) always honored — user can override - Formula: warm-white approximation — only produces white for near-neutral RGB inputs - Phase 2 Pi-side: send `[R, G, B, 0]` to use auto-derive; send `[R, G, B, W]` to override ## Task Commits | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | animation_base.h + animation_engine.h/.cpp | `230be99` (included in Plan 02 commit) | 3 files | | 2 | 8 animation implementations | `cba4b17` | 8 files | **Note:** Task 1 files were committed as part of Plan 02's final commit (`230be99`) because Plan 02's agent staged all untracked files in firmware/src/. The content is correct and matches the plan spec. ## Files Created | File | Purpose | |------|---------| | `firmware/src/animations/animation_base.h` | AnimTickFn typedef, AnimEntry struct, 8 function declarations, ANIM_REGISTRY extern | | `firmware/src/animation_engine.h` | ZoneState struct, g_brightness global, autoWhite() declaration, initAnimationEngine() | | `firmware/src/animation_engine.cpp` | ANIM_REGISTRY[8], animTickTask (50fps vTaskDelayUntil), applyCommand, autoWhite impl, initAnimationEngine | | `firmware/src/animations/chase.cpp` | Moving window animation, speed scales position advance, wraps strip end | | `firmware/src/animations/pulse.cpp` | Sine wave brightness pulse on solid palette color | | `firmware/src/animations/rainbow.cpp` | HSV rainbow via HsbColor, hue offset advances with tick, ignores palette | | `firmware/src/animations/strobe.cpp` | On/off flicker, period = 20/speed ticks | | `firmware/src/animations/color_wash.cpp` | Linear fade between palette colors, hold_ticks = 50/speed | | `firmware/src/animations/breathe.cpp` | Slow sine envelope, 5% minimum brightness, D-11 alive indicator | | `firmware/src/animations/sparkle.cpp` | Random pixel flash with esp_random() hardware RNG | | `firmware/src/animations/gradient_sweep.cpp` | 2-color gradient sweeps with offset advance | ## Decisions Made - **vTaskDelayUntil over vTaskDelay:** Corrects for tick processing time — ensures exactly 50fps regardless of how long animations take to compute. vTaskDelay would drift by the compute time each frame. - **Non-blocking commandQueue drain:** `xQueueReceive(..., 0)` with timeout=0 — never stalls the animation frame for an incoming command. Commands apply on the next frame, acceptable for animation purposes. - **Direct applyCommand at init (not via queue):** `initAnimationEngine()` sets breathing animation directly on zone state, not via commandQueue, because the queue may not exist yet when init runs (before WiFi server starts). - **sparkle uses esp_random():** ESP32 hardware RNG, no seeding required, better entropy than pseudo-random for visual effects. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] wifi_server.h existed with correct function signature** Plan 03 expected to find `wifi_server.h` missing (Plan 02 is "parallel"). In practice, Plan 02 had already run and created `wifi_server.h` with `startUdpServer()` and `commandQueue` extern. No stub was needed — used the actual Plan 02 file. The initial stub creation via bash heredoc was overwritten by the already-committed Plan 02 version. **2. [Rule 1 - Context] Task 1 files committed in Plan 02's commit** Plan 02's agent staged and committed animation_engine.h/.cpp and animation_base.h as part of its commit `230be99`. The content matches Plan 03's spec exactly (no overwrite occurred). This is a parallel execution artifact — the files exist correctly in git with correct content. --- **Total deviations:** 2 (both non-impactful parallel execution artifacts) **Impact:** Zero — all files contain correct content and are properly committed. ## Known Stubs None — all animation functions are fully implemented. No placeholder data flows to LEDs. ## Next Phase Readiness - Plan 04 (integration) can wire `initAnimationEngine()` into `main.cpp` alongside `startUdpServer()` - Animation engine is ready to consume LedCommand structs from commandQueue immediately - Phase 2 Pi-side needs: ANIM_REGISTRY name strings (documented above), W auto-derive behavior (documented above), UDP port 4210, JSON protocol from protocol.h - Startup breathing animation provides visual feedback — Plan 04 should restore BRIGHTNESS_CAP after WiFi connects --- *Phase: 01-esp32-firmware* *Completed: 2026-04-03*