Skip to content

Release 4 — Platform, Networking, and Library Foundation

Theme: Release 3 validated the pixel pipeline. Release 4 makes it production-ready and opens it to external consumers. PSRAM-backed pixel buffers, true dual-core dispatch, and per-module timing bring performance visibility. Device discovery and Art-Net networking connect nodes. The PAL abstraction eliminates all #ifdef ARDUINO from module code. A smarter module-creation UI, five new effects, and optional system utilities extend the runtime. Capping it off: the FastLED-MM bootstrap and ProducerModule/ConsumerModule base classes make projectMM a clean, domain-independent library. 261 unit tests, three build targets, and a full CI pipeline.


Release Overview

What was delivered in Release 3 — build on this

Strength Notes
Clean layer model EffectsLayer / DriverLayer / GridLayout hierarchy stable and well-tested
Driver-sized effects onChildrenReady() + onSizeChanged() propagate layout changes automatically
Coord3D mapping requestMappings() / serpentine / index_to_coords foundation in place
StatefulModule audit Hotpath vs non-hotpath fields classified; meta_ store introduced
Live system tests deploy/live_suite.py validates end-to-end pipelines on real hardware without USB
Python CI pipeline all.py covers build → test → flash → live → summarise in one command

What Release 4 addresses

Problem Sprint
#ifdef ARDUINO scattered through modules; devices can't see each other Sprint 1 (PAL v1 + Device module)
Pixel buffers in internal RAM; grid size limited Sprint 2 (PSRAM + dual-core)
Scheduler fps shown, not per-stage cost Sprint 3 (hotpath tuning + fps/ms)
Flat type list for adding modules Sprint 4 (advanced module-creation UI)
Missing Game of Life, Noise, Distortion effects Sprint 5 (2D effects)
No real output protocol Sprint 6 (Art-Net out)
Optional system utilities not loadable at runtime Sprint 7 (optional modules)
Info-level logging clutters serial; no community links Sprint 8 (observability + branding)
UI tech-debt and naming revisits Sprint 9 (revisits)
FastLED-MM bootstrap; main.cpp boilerplate Sprint 10
projectMM as a domain-independent library Sprint 11

Sprint 1 — PAL v1 + Device Discovery

Goal: eliminate all #ifdef ARDUINO from src/modules/ behind a clean pal:: namespace, and make projectMM nodes discoverable on the local network. Doing PAL first means every later sprint writes platform-clean code from day one.

Key deliverables:

  • src/pal/Pal.h — three-way platform switch (Arduino / IDF / PC) for timing, PSRAM, GPIO, logging, 18 system-info functions, filesystem, FreeRTOS task helpers, reboot.
  • src/pal/FileSystem.h, src/pal/MemoryStats.h — moved from src/core/. Timing.h reduced to a forwarding stub → pal::micros().
  • EffectsLayer and DriverLayer switched to pal::psram_malloc() / pal::psram_free() (delivers Sprint 2 Part A early).
  • pal::udp_bind / udp_broadcast / udp_recv / udp_close — Arduino uses a static pool of 4 WiFiUDP slots so PAL can return an integer index; IDF stubs; PC POSIX non-blocking socket.
  • DeviceDiscoveryModule (src/modules/system/): UDP broadcast on port 23452 every 1–30 s, up to 8 discovered peers as display controls, immediate broadcast on setup(), self-packet filtering. Child of NetworkModule.
  • live_suite.py test4_device_discovery waits up to 10 s for devices >= 1. Verified PC discovers MM-70BC; ESP32 discovers PC.
  • Deploy hardening: flashfs.py switched from subprocess.run(capture_output=True) (was hanging on 4-min flash) to streaming Popen; devicelist.py captures ssid=, version=, ip= in a single probe window.
  • AppVersion.h → 1.3.0; version added to hello line for devicelist.py verification without flashing.
Metric Value
Tests 212/212 ✅
#ifdef ARDUINO in src/modules/ 0
Live tests 5/5 suites ✅ (PC + MM-70BC)
Discovery PC↔MM-70BC, both devices=1

Retrospective:

  • Scope expanded beyond the 7 PAL functions originally listed: full 18-function system-info layer, filesystem abstraction, FreeRTOS wrappers, IDF migration path. Right call — established a clean PAL foundation and gave a concrete #elif defined(IDF_VER) migration target.
  • LittleFS disk version mismatch (PlatformIO Python builder defaults to v2.1, ESP32 Arduino lfs.h defines v2.0) was silently wiping all state via format_on_fail=true on every boot. Fixed with board_build.littlefs_version = 2.0 and changing fs_begin() to try without format first.
  • Arduino's WiFiUDP is a stateful object — can't be represented as an integer handle. The 4-slot static pool lets PAL return an index while the real object lives in static storage; IDF and PC use file descriptors natively (zero-cost there).
  • "Open device UI" link descoped from the original DoD: it touches frontend wiring outside PAL+module scope. Replaced with a display control showing name @ ip (version) per discovered peer.

Sprint 2 — PSRAM + Dual-Core + Buffer Audit

Goal: move pixel buffers to PSRAM on S3, dispatch effects and drivers to separate cores, audit the double-buffer model.

Key deliverables:

  • DriverLayer::healthReport() reports buffer_kb=N psram=1/0 sources=N WxHxD — PSRAM use directly observable via GET /api/system. Allocation already used pal::psram_malloc from Sprint 1.
  • Module::preferredCore() virtual (default 1 = App core): EffectsLayer returns 0, DriverLayer returns 1. ModuleManager always derives core from preferredCore(); the "core" field in modulemanager.json is informational only.
  • UI core badge per module card: C0 (blue) for Net core, C1 (green) for App core. Children inherit their top-level parent's badge via propagateCore() in the frontend.
  • Scheduler::loopCore(core) runs only modules assigned to that core. tickPeriodic() handles 20 ms / 1 s / 10 s timed dispatch and health checks. PC loop() calls loopCore(0) + loopCore(1) + tickPeriodic() sequentially — PC behaviour unchanged.
  • pal::sem_binary_create/give/take/delete for FreeRTOS binary semaphore on ESP32; no-ops on PC.
  • main.cpp (ESP32): single schedulerTask replaced by effectsTask (Core 0, prio 1) + driverTask (Core 1, prio 2), connected via s_frameSem.
  • Part C (single-buffer audit) deferred — current double-buffer model is safe under semaphore handshake; reducing requires concurrency analysis and a stress test.
Metric Value
Tests 219/219 ✅ (+7 new: 4 layer, 3 scheduler)
Live tests 42/42 checks ✅
Dual-core dispatch effectsTask Core 0, driverTask Core 1, binary semaphore

Retrospective:

  • PSRAM was already done in Sprint 1's PAL work. This sprint's Part A became "make it observable via healthReport()" — right outcome: the architecture made the feature land earlier than planned.
  • preferredCore() always wins over the saved core field. Avoids stale saved values silently overriding correct type-level affinity. Field is written for observability (UI badge, REST) but never read back.
  • sem_take(s_frameSem, 100) uses a 100 ms timeout, not portMAX_DELAY. If Core 0 stalls, Core 1 still yields and the watchdog stays quiet — one extra WDT-safe yield per stall is acceptable for a 20 ms frame budget.
  • Driver task gets higher priority (2) than effects task (1) so it preempts as soon as the semaphore is given, minimising blend-complete-to-LED-output latency.

Sprint 3 — Hotpath Tuning + fps/ms Accuracy

Goal: measure true per-frame cost; fix the per-stage fps/ms display.

Key deliverables:

  • Per-module fps, avg_ms, min_ms, max_ms in all three serialisation paths (getStateJson for WS, getModulesJson for /api/modules, getSystemModulesJson for /api/system).
  • Frontend 4-mode toggle (fps / avg / min / max) with localStorage persistence.
  • Root timing = wall-clock from Scheduler (own + children); child timing = self-only from StatefulModule::timing_.
  • EffectsLayer::runLoop() gets its own timing block — it's the only module that overrides runLoop() (to call publish() after children).
  • Live invariant verified on PC 16×16×1: effects1 0.0004 ms = own + ripples1 0.000324 ms self-only.
  • Part A (cycle counters) deferred — pal::micros() µs precision is 1000× the ~20 ms frame budget.
  • Part C (heap-delta assertion) deferred — no-allocation invariant is enforced by code review.
Metric Value
Tests 222/222 ✅ (+3 new)
Per-module timing fps + avg/min/max ms in all three API paths

Retrospective:

  • Three serialisation paths needed the timing fields added separately. A future refactor could share a serializeTimingInto(obj, t) helper.
  • Tests must call addChild() manually before runSetup() — production wires children at runtime via ModuleManager, but it's easy to forget when writing new tests.

Sprint 4 — Advanced Module-Creation UI

Goal: replace the flat type list with a context-aware, searchable, tagged picker.

Key deliverables:

  • Module::allowedChildCategories() virtual: each module declares what children it accepts. Frontend looks up the parent type in knownTypes at picker open. No hardcoded type names in JS.
  • tags() moved to Module.h (was duplicated on StatefulModule.h).
  • TypeRegistry::getTypesDetailed: instantiates a temporary, queries category() / tags() / allowedChildCategories(), deletes it. /api/types now returns [{name, category, tags, allowedChildCategories}].
  • Frontend: emoji chips collected from filtered types; chips toggle and AND-compose; case-insensitive name search composes with chips; double-click row to create.
  • Domain modules tagged: SineEffect 🔥🧊💫, LinesEffect 🔥🧊💫, RipplesEffect 🔥🟦💫, BrightnessModifier 💎💡, GridLayout 🚥💡, PreviewModule ☸️💡.
Metric Value
Tests 222/222 ✅
/api/types object array with category + tags + allowedChildCategories
Picker contexts root excludes effect/modifier/layout; EffectsLayer shows effect/modifier; DriverLayer shows layout/driver

Retrospective:

  • One new/delete per type at /api/types call time is negligible — endpoint is hit once on page load.
  • Root-level picker shows network/system/layer modules, most untagged today, so the chip row is empty for root. The chip row immediately becomes useful for the EffectsLayer picker as more effects land in Sprint 5.
  • name() returns "SineEffect" but REGISTER_MODULE registers "SineEffectModule". Picker shows the registered name. Historical inconsistency; align in a future sprint.

Sprint 5 — 2D Effects: Game of Life, Noise, Distortion

Goal: three more 2D effects.

Key deliverables:

  • GameOfLifeEffect: Conway's rules, one generation per tick, resets when grid dies or stalls. Glider test passes (Gen4 = {(2,1),(3,2),(1,3),(2,3),(3,3)}, count = 5). Public setPattern / getCell / liveCount / stepGeneration helpers enable deterministic testing on the module itself. Tags 🔥🟦💫.
  • NoiseEffect2D: hash-based bilinear value noise with smoothstep — no FastLED, ~20 lines, works on PC and ESP32. Tags 🔥🟦.
  • DistortionWaves2DEffect: WLED-port interfering sine waves on both axes. Tags 🔥🟦🐙.
  • All three use pal::psram_malloc in setup() / pal::psram_free in teardown() — confirmed pattern for heap-sized per-module buffers.
Metric Value
Tests 229/229 ✅ (+7 new)

Retrospective:

  • Hash-based value noise replaces inoise8 (FastLED): platform-independent, embeds in ~20 lines, visually equivalent. Pattern for all future noise-using effects.
  • Test-helper methods on the module itself (setPattern / getCell / liveCount) work cleanly for cellular-automaton effects — no separate test fixture needed.
  • Sprint 4's emoji picker context filter immediately surfaced the new effects with 🔥🟦 chips, validating the tagging infrastructure.

Sprint 6 — Art-Net In and Out

Goal: first real I/O protocol, working on PC and ESP32 with no GPIO required. Two devices wired together is the end-to-end live test.

Key deliverables:

  • ArtNetOutModule (driver): DMX-512 frames via UDP. Multi-universe with auto-count at 170 pixels/universe. Controls universe, ip (broadcast or unicast), enabled.
  • ArtNetInModule (effect, category "effect", wires like SineEffect): receives Art-Net DMX into an EffectsLayer. Pipeline ArtNetIn → EffectsLayer → DriverLayer → PreviewModule is the receive path.
  • pal::udp_send(ip, port, buf, len) — three-way. PC sets SO_BROADCAST unconditionally so one call serves broadcast and unicast.
  • Two-device test: PC sender → ESP32 MM-70BC receiver via broadcast UDP over LAN: packets_tx=46722, packets_rx=238.
  • livetest.py restructured: PC server stays up while ESP32 tests run, so test4_device_discovery finds a live peer on both sides simultaneously.
  • Test complexity classification (smoke / format / behavioral / integration) added to unittest.py keyword-matching, surfaced in test-results.md and deploy-summary.md. Breakdown: 11 smoke / 33 format / 164 behavioral / 28 integration.
Metric Value
Tests 236/236 ✅ (+7 Art-Net)
PC loopback tx=32239 rx=5065 (~3 s settle)
Two-device tx=46722 rx=238 (broadcast UDP over LAN)
Live tests PC 6/6, ESP32 MM-70BC 6/6, two-device 4/4

Retrospective:

  • UDP broadcast loopback doesn't work on ESP32 WiFi — the device doesn't deliver its own broadcast to bound sockets. test5 (Art-Net loopback) is skipped on non-PC; the two-device cross-test in livetest.py covers the receive path on hardware.
  • 238 / 46722 ≈ 0.5 % delivery is normal: sender ticks much faster than ESP32 WiFi receive rate. packets_rx > 0 is the right assertion.
  • Running ArtNetOut + ArtNetIn on the same ESP32 at frame rate destabilises the Arduino WiFi stack (UDP interrupt overload). Two-device test covers ESP32 receive instead.
  • StatefulModule already registers enabled_ for every module; both Art-Net modules had a duplicate control, removed in this sprint.

Sprint 7 — Optional System Modules

Goal: load-on-demand utilities; OTA deferred to R5.

Key deliverables:

  • TasksModule: read-only display of FreeRTOS tasks (name, core, priority, stack watermark). Useful for diagnosing core affinity after Sprint 2.
  • FileManagerModule: lists LittleFS / state/ files with sizes; delete by typing a filename and pressing the delete button. First module to use EditStr ("text" UI control); delete button marked sensitive=true to skip WS broadcasts.
  • PAL additions: state_dir, task_list, fs_list, fs_remove — Arduino / IDF stub / PC std::filesystem.
  • FirmwareUpdateModule deferred — needs binary HTTP upload, Update.h (real hardware only), and a CI path that can verify OTA without bricking a device. Tracked for R5 Sprint 1.
Metric Value
Tests 246/246 ✅ (+10 new)
Footprint TasksModule ~1064 B, FileManagerModule ~2244 B

Sprint 8 — Logging Refactor + Community Branding

Goal: push logs to WebSocket clients in real time (no ring buffer); add MoonModules community links; integrate CodeRabbit.

Key deliverables:

  • g_logWsPushFn function pointer in Logger.h (null by default). Every LOG_* macro formats a 128-byte stack buffer, calls printf, then calls the push fn if wired. Zero device-side persistent allocation — the rejected ring buffer was 3 KB.
  • wsPushLog() in main.cpp wraps each line as {"t":"log","m":"<msg>"} and calls ws.broadcastText(). g_logWsPushFn = wsPushLog after ws.begin().
  • GET /api/log returns current level; POST /api/log {"level":"debug"} sets it at runtime.
  • Frontend collapsible log panel below the health panel; wsConn.onmessage routes msg.t === 'log' to appendLogLine(); trims to 100 lines; clear button.
  • Bare printf in DeviceDiscovery.h (4 calls) migrated to LOG_SETUP(). WifiSta.h / SystemStatus.h deferred to Sprint 9.
  • Discord / Reddit / YouTube / GitHub pill buttons in side-nav footer below + add module.
  • .coderabbit.yaml at repo root: per-path review instructions (hot-path constraints, behavioral test requirement, doc style, PAL three-way guard). standards.md updated with AI attribution table and CodeRabbit notes.
Metric Value
Tests 252/252 ✅ (+6 logger)
Persistent device alloc 0 B (vs. rejected 3 KB ring buffer)

Retrospective:

  • WS push beats a ring buffer on memory cost: zero device-side bytes. Lines produced before the browser connects are lost — acceptable for diagnostic use during development.
  • CodeRabbit's GitHub App must be installed manually by the repo owner; can't be automated. Noted in standards.md.

Sprint 9 — Revisits

Goal: four open design questions, each closed with a documented decision.

Key deliverables:

  • A — defVal sourcing. Confirmed correct: defaults come from compile-time setup() values, not state-file initial values. Two new behavioral tests in test_stateful_module.cpp lock the invariant ("defVal is immutable after setup()"). Decision in architecture.md#defval-sourcing.
  • B — loop vs runLoop naming. Convention confirmed consistent across Module / StatefulModule / Scheduler. Decision in architecture.md#loop-vs-runloop.
  • C — ProducerModule / ConsumerModule for core affinity. Existing core field wins; decision reconfirmed. R5S5 re-open condition added (eventually relocated to R4S11).
  • D — Auto-setup guard. Verified unchanged after Sprint 2 dual-core; Sprint 4 tests still pass. Decision in architecture.md#auto-setup-guard.
  • Sprint 8 carry-over: 4 bare printf in WifiSta.h + 1 in SystemStatus.h migrated to LOG_SETUP. Logging audit complete across all system modules.
  • Periodic scheduler.printTimings() / printSizes() removed from driverTask (embedded) and main() (PC). REST /api/system covers it on demand. End-of-run prints kept for CI --count mode.
  • Logo (docs/assets/moonlight-logo.png) downsampled 320×320 → 20×20 via Lanczos. Embedded base64 shrunk from ~31 425 to 1 564 chars.
  • display controls render multi-line via white-space: pre-wrap (TasksModule task list, FileManagerModule file list).
Metric Value
Tests 254/254 ✅ (+2 defVal)
Frontend bundle ~70 KB → ~40 KB

Retrospective:

  • All four revisits closed with documents, not code changes. architecture.md becomes the durable record so future contributors don't re-derive the reasoning.
  • Writing the Revisit A tests required checking the load order carefully (addControl() in setup(), then loadState() separately). The two new tests lock the invariant against future refactoring.

Sprint 10 — FastLED-MM Bootstrap + main.cpp Refactor

Goal: create MoonModules/FastLED-MM as the first library consumer; refactor main.cpp so library consumers need only a minimal entry point.

Key deliverables:

  • Part A — FastLED-MM repo. PlatformIO project + library; imports projectMM and FastLED via lib_deps. WaveRainbow2DEffect + FastLEDDriverModule + a shared CRGB flm_leds[] array as the pixel handoff (bypasses EffectsLayer/Channel/DriverLayer — the pattern FastLED builders already know). Files: FlmConfig.h, FlmPixels.h, WaveRainbow2D.h, FastLEDDriver.h, main.cpp, platformio.ini, library.json, README.md.
  • Part B — main.cpp refactor. Routes extracted to src/core/AppRoutes.{h,cpp} (registerCoreRoutes(), single source of truth). FreeRTOS task setup extracted to src/core/AppSetup.{h,cpp} (pal::embeddedSetup(mm, scheduler, server, ws)).
  • src/main.cpp 497 → 151 lines (-70 %). After Part B a FastLED-MM-style consumer needs only globals + REGISTER_MODULE + setup() calling pal::embeddedSetup() + loop() calling pal::suspend_forever().
Metric Value
Tests 254/254 ✅ (refactor; no new tests)
main.cpp size 497 → 151 lines
ESP32-S3 build Flash 28.8 %, RAM 14.7 %

Retrospective:

  • The two registerRoutes() copies (one per #if section) had drifted: PC /api/test read from a file; embedded didn't. Unifying with the file-reading variant works on both because fopen returns null for missing LittleFS files, falling through to live health. Zero branch needed.
  • wsPushLog stays in main.cpp by design: it captures s_wsLog (the WsServer global). Both platforms need it; moving it to AppSetup would require a PC-specific path. Twelve lines of glue beats a cross-platform abstraction for a minor helper.

Sprint 11 — projectMM as a Domain-Independent Library

Pulled forward from Release 5 Sprint 5 after R4S10's FastLED-MM bootstrap made the library separation the immediate next step.

Goal: ProducerModule / ConsumerModule base classes in src/core/; library packaging with replaceable LED registrations.

Key deliverables:

  • src/core/ProducerModule.h: declareBuffer(buf, len, elemSize) + bufferPtr() / bufferLen() / bufferBytes() accessors.
  • src/core/ConsumerModule.h: setInput("producer", &p) wiring; producer_ protected pointer.
  • EffectsLayer and DriverLayer deliberately do NOT extend the new base classes. EffectsLayer owns a double-buffered atomic Channel*, not a flat void*; DriverLayer has eight sources, not one. The new bases are for new single-buffer single-producer consumers — not for retrofitting the LED pipeline.
  • src/core/CoreRegistrations.cpp (always compiled into the library): infrastructure modules (Network, WiFi, SystemStatus, DeviceDiscovery, Tasks, FileManager).
  • src/modules/ModuleRegistrations.cpp stripped to LED-domain only (13 LED modules); excluded by library.json srcFilter. Consumers provide their own.
  • FastLED-MM updated: WaveRainbow2DEffect : ProducerModule calls declareBuffer(flm_leds, FLM_NUM_LEDS, sizeof(CRGB)) in setup(); FastLEDDriverModule : ConsumerModule. ESP32-S3 build PASS (Flash 47.6 %, RAM 16.1 %).
  • 7 behavioral tests cover the full API surface (null before setup, correct accessors after declareBuffer, wiring via setInput, unknown-key ignore, end-to-end read).
  • Post-delivery hardening: pal::ensureNetworkModules(mm) and pal::hasModuleType(mm, type) exposed as public utilities. firstBootFn(ModuleManager&) callback added to pal::embeddedSetup (called once when state files are absent — FastLED-MM uses it to register its modules without hardcoding in AppSetup). WIFI_LOLIN_FIX extended to WifiAp.h (WIFI_AP_STA + setTxPower(8.5dBm) before softAP()). FastLED.setMaxRefreshRate(30) in FastLEDDriverModule keeps the WiFi beacon scheduler alive — at 90 fps the WS2812B RMT was occupying ~70 % of S3 radio time, hiding the AP. WaveRainbow2DEffect::category() "effect" → "source" so the frontend doesn't block it from the top-level add list. ModuleManager::loadModules() guarded against double-registration crash. CI: WiFiUDP.h include moved into the UDP section of Pal.h; contents: write permission added to docs deploy job.
Metric Value
Tests 261/261 ✅ (+7 ProducerModule/ConsumerModule behavioral)
ESP32dev build Flash 93.0 %
ESP32-S3 build Flash 28.8 %, RAM 14.7 %
FastLED-MM ESP32-S3 build Flash 47.6 %, RAM 16.1 %

Retrospective:

  • The decision NOT to retrofit EffectsLayer / DriverLayer onto the new bases was the right call. Wrapping Channel* behind void* bufferPtr() would strip the type info the dual-core handoff relies on. The new bases are for new consumers; the LED pipeline stays separate.
  • CoreRegistrations.cpp is the key library-packaging deliverable. A downstream project that excludes ModuleRegistrations.cpp still gets WiFi / network / system / discovery for free. Without this, excluding LED modules would silently remove the networking stack.
  • FastLED-MM platformio.ini needs three matched settings to build cleanly: pin pioarduino espressif32 55.03.37 (default espressif32 has a different TaskStatus_t layout missing xCoreID, and an older GCC that rejects C++17 aggregate init with default members); -DBUILD_TARGET=$PIOENV (required by Pal.h's platform_version()); lib_compat_mode = strict (avoids silent dependency surprises). Use ~/.platformio/penv/bin/pio — system Homebrew pio ships an outdated fatfs package that fails with cannot import name 'create_extended_partition'.
  • WS2812B RMT at 90 fps occupies ~70 % of ESP32-S3 radio time, starving the WiFi beacon scheduler so the AP becomes invisible. 30 fps cap drops occupation to ~23 % and the AP becomes joinable.

Sprints 12–14 — Moved to Release 5

Sprints originally planned here were moved to Release 5 to keep Release 4 focused. Sprint 11 (Library packaging) was pulled back from Release 5 in exchange.

Sprint Release 5 equivalent
Sprint 12 (Rough ideas + ESP32-P4) R5 Sprint 2
Sprint 13 (PPA blending) R5 Sprint 3
Sprint 14 (Windows build) R5 Sprint 4

Release 4 Backlog

  • Palette system. PaletteModule with 7 FastLED presets; global palette + per-effect override. Pick up in R5 alongside modifiers.
  • Multi-panel grid. LayoutMultiPanel composing multiple panels on separate pins. Pick up when multi-panel hardware is available for testing.
  • Per-layer brightness / fade. Each EffectsLayer gets a brightness slider; adding/removing a layer fades over 500 ms. From the original moonlight-scope plan — pick up in R5.
  • Solo / mute / rename / collapse per layer. DAW-style layer controls. From the original moonlight-scope plan — pick up in R5.
  • Rings + Wheel layouts. For concentric and radial fixtures. Pick up for R5 fixture work.
  • Art-Net in as an effect. NetworkInEffect fills pixels from incoming Art-Net/DDP frames. Requires Sprint 6 to land first.
  • Bounded-layer efficient pixel iteration. Pick up if Sprint 3 profiling shows the full-canvas clip loop is hot.
  • PPA SRM (Scale/Rotate/Mirror). Hardware-accelerated transform for EffectsLayer outputs; limited to 1/16th-pixel rounding on scale. Pick up after Sprint 11 foundation lands.
  • PPA dual-task post-processing. Split the pipeline into a frame-render task (effects on Core 0) and a pixel post-processing task (PPA Fill + Blend + SRM on a dedicated core). Requires Sprint 11 PAL foundation and a concurrency analysis. Coordinate with the Sprint 2 double-buffer audit (Part C deferred).
  • JPEG decoder handle. Register esp_jpeg_decode_one_picture handle at boot as a PAL global (same pattern as PPA clients). Prerequisite for any media-playback or image-display effect.