diff --git a/README.md b/README.md index 5d92e9e3..8219df95 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you like projectMM, give it a ⭐️, fork it, or open an issue or pull reque 🎨 **Plug in, open a browser, see lights**: a live 3D preview of every effect, modifier, and layout, controllable from the same tab. The interface renders any module from its declared controls, so adding a module needs zero UI code. -πŸŒ— **MoonBase, the second boot image (4 MB boards)**: instead of spending half a small flash on a second firmware copy, a ~750 KB maintenance image sits in the factory slot and installs updates into one large app slot, one click in the UI covers the whole reboot-install-reboot cycle, and a power cut mid-update lands back in MoonBase, never in a half-written app. See [architecture.md Β§ MoonBase](docs/architecture.md#moonbase-the-second-boot-image-4-mb-boards). +πŸŒ— **MoonBase, the second boot image**: instead of spending half the flash on a second firmware copy, a ~750 KB maintenance image sits in the factory slot and installs updates into one large app slot, one click in the UI covers the whole reboot-install-reboot cycle, and a power cut mid-update lands back in MoonBase, never in a half-written app. Forced on a 4 MB board, which has room for one application and not two, and chosen on the larger ones, where the freed slot goes to the filesystem instead. See [architecture.md Β§ MoonBase](docs/architecture.md#moonbase-the-second-boot-image). ⚑ **Flash from your browser in seconds**: the web installer picks your device, flashes the matching firmware, and hands WiFi credentials to the device over USB via Improv. No serial monitor, no recompile. @@ -179,7 +179,7 @@ Specific people whose work directly shaped parts of projectMM. We study their th - **The [Improv Wi-Fi](https://github.com/improv-wifi) project**: the open Improv serial provisioning standard ([sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js)) that the projectMM web installer uses to provision a freshly-flashed device over USB. - **[FastLED](https://github.com/FastLED/FastLED)**: the canonical LED-effects library whose conventions the LED-effect world shares. projectMM links no part of FastLED, but it carries forward FastLED's recognisable *names and models* for the color/animation primitives (`scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), the `beatsin8` / `inoise8` / `qadd8` family), so a contributor recognises them on sight. The implementations are projectMM's own, integer-only and hot-path-tuned for our render loop; FastLED is the prior art behind the convention, credited here and in each primitive's notes. - **[FPP](https://github.com/FalconChristmas/fpp) (Falcon Player)**: the show player that drives LED panel receiver cards from a Raspberry Pi. Seeing an FPP rig feed a wall of HUB75 panels is what prompted [PanelCardDriver](docs/moonmodules/light/drivers.md#panelcard): if a Linux host can send those frames, so can a board that is already rendering them, which removes the host from the installation entirely. FPP is the inspiration, and the reference point for what good looks like here: it sustains 50 fps. -- **[Tasmota](https://github.com/arendst/Tasmota) and Mathieu Carbou's [MycilaSafeBoot](https://github.com/mathieucarbou/MycilaSafeBoot)**: the safeboot pattern behind [MoonBase](docs/architecture.md#moonbase-the-second-boot-image-4-mb-boards): replacing a small board's second OTA slot with a minimal boot image that installs into one large app slot. Tasmota proved the scheme at scale; MycilaSafeBoot distilled it to a standalone image and set the size bar. MoonBase is our from-scratch minimal take, written directly against ESP-IDF. +- **[Tasmota](https://github.com/arendst/Tasmota) and Mathieu Carbou's [MycilaSafeBoot](https://github.com/mathieucarbou/MycilaSafeBoot)**: the safeboot pattern behind [MoonBase](docs/architecture.md#moonbase-the-second-boot-image): replacing a small board's second OTA slot with a minimal boot image that installs into one large app slot. Tasmota proved the scheme at scale; MycilaSafeBoot distilled it to a standalone image and set the size bar. MoonBase is our from-scratch minimal take, written directly against ESP-IDF. - **Damian Schneider ([dedehai](https://github.com/DedeHai))**: author of the WLED Particle System, whose emitters, forces and walls over one shared pool are the shape our [particle kernel](docs/moonmodules/light/power-functions.md#particles) and the scripted `pool` / `emit` / `step` builtins follow, in our own fixed-point implementation. - **wladi ([myhome-control](https://shop.myhome-control.de))**: designer of the [MHC-WLED ESP32-P4 shield](https://shop.myhome-control.de/en/ABC-WLED-ESP32-P4-shield/HW10027), and the source of the hardware and the pinout details that got its **line-in audio** working in [AudioService](docs/moonmodules/core/moxygen/AudioService.md): the onboard PCM1808 I2S ADC (WS 26 / SD 33 / SCK 32 / MCLK 36), the PCM1808's stereo wiring, and its `FMT` format-select jumper (open = I2S/Philips, our default; tie to 3V3 for left-justified), which is what confirmed the standard-I2S path the ADC needs. diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 7c540a95..d870cb64 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -73,6 +73,24 @@ calling it sound, so it is renamed rather than left as the odd one out. A restored config maps the old name to the new one and carries its value. On a device upgraded in place the control returns to its default (off); switch it back on where you had it. +### AudioVolume is gone + +**Action: pick another effect.** Affects any device with an AudioVolume effect on a layer. + +It drew one bar from the audio level, which every audio-reactive effect does as a side effect of +what it actually draws. There is no successor to map it onto, so a restored config carrying an +`AudioVolumeEffect` node finds no such type and the layer comes up without it. `GEQ` is the nearest +thing if a literal meter is what you want. + +### The Firmware card describes one image at a time + +**Action: none.** Affects nothing a user has set: every control involved is read-only. + +`firmwarePartition` is now `partition`, and `update_pct` is gone (an install's progress belongs in +the overlay the UI raises while it runs, not in a row that sits at zero for the life of a device +that is not mid-install). Where a device carries two images, a new `image` control selects whether +those rows describe the running app or MoonBase in the factory slot. + ### Noise2D is gone; Noise renders it **Action: re-set one control.** Affects any device with a Noise2D effect on a layer. diff --git a/docs/architecture.md b/docs/architecture.md index fd08747e..3d16a23a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -291,6 +291,28 @@ network, a stronger power-fail story than dual-OTA's. A failed install deliberat MoonBase, visibly, rather than silently reverting to the old app; the way back is its explicit "Boot the app" action, which only boots an image that validates. +**Updating MoonBase itself** runs the same cycle backwards: the app writes the factory slot while +running from `ota_0`, exactly as MoonBase writes the app slot while running from factory. Neither +image can rewrite the partition it executes from, so each installs the other and the app is the +only thing that can repair a broken recovery image. Without it a bad MoonBase means a cable, which +is the failure MoonBase exists to prevent. + +Two things make that safe enough to offer. `esp_ota_*` refuses a factory partition, so this is a +raw `esp_partition_erase_range` + `esp_partition_write`, which also forfeits the validation +`esp_ota_end` performs: `esp_image_verify` replaces it after the write. And because a 4 MB board +has nowhere to stage 743 KB before erasing, the image streams straight in, so everything that can +reject it is decided from its FIRST CHUNK, before a byte is erased: the image magic, the chip id +(one MoonBase per chip, one paste apart, and a checksum does not catch a swap), and the descriptor +naming `projectMM-moonbase` rather than the app. Those rules live in `src/core/FirmwareImage.h` so +a host test can drive them. What remains is a window, during the write, in which the device holds +no recovery image; the app keeps running throughout, so the answer to a failure is to retry. + +Each image reports its version from the app descriptor IDF puts in every binary, `PROJECT_VER` +being set to the same computed version for both, so the app can read the factory partition's +version without booting it and say when the two were built apart. A device that cannot name its +own recovery image cannot be diagnosed: two boards that looked identical, one of which could not +install firmware, took a bisect of the git log to tell apart. + MoonBase is a standalone ESP-IDF project (`moonbase/`, ~750 KB against an 896 KB slot) sharing no sources with the app, the deliberate trade for an image that must stay small and, once working, hardly change. `moondeck/build/build_esp32.py` builds it alongside every variant that opts in @@ -324,7 +346,7 @@ The defining line is the **data relationship, not the connector**: *does the mod Services are **user-add/deletable children of the `Services` container** β€” the core-domain twin of the light pipeline's `Effects`/`Drivers`: a top-level container holding user-added children of one role. The firmware is identical whether or not the hardware is wired, so the user adds the module when they solder a gyro on and removes it later, reusing the generic child add/replace/delete + persistence machinery (`Services` declares `acceptsChildRoles("service")`). Fixed device infrastructure (identity, network, the inspection tools Tasks/I2cScan) lives under **System** instead, wired by code, not user-added β€” that is the System/Services split. Direction is per-module, not a role: a service may read (gyro), write (relay), or both, so one `Service` role spans the category. Each is a header-only or `.h`+`.cpp` core module under `src/core/`, reaches hardware only through a domain-neutral platform primitive (`platform::i2c*`, `platform::audioMic*`, …), and gets a spec in `docs/moonmodules/core/services.md` (enforced by `check_specs.py`). Most poll in `tick20ms`/`tick1s`; the exception is a service whose data an effect consumes *every frame*: [AudioService](moonmodules/core/moxygen/AudioService.md) reads + analyses its IΒ²S microphone in `tick()` because the audio effects react per render tick, and its per-tick cost (one FFT) is part of the render budget. Automatic bus-probe detection is out of scope; the manual path is the foundation. -**An effect reads a service's data** via the shared-struct pull pattern from [Β§ Data exchange](#data-exchange-between-modules), no new mechanism: the service owns a small POD struct overwritten in place each poll/tick, and the consuming effect holds a `const` pointer to it. The first concrete case is audio: AudioService produces an `AudioFrame` (level + 16-band spectrum + peak) that [AudioVolumeEffect](moonmodules/light/effects.md) and [AudioSpectrumEffect](moonmodules/light/effects.md) consume. It reaches the frame through a static `AudioService::latestFrame()` rather than a boot-time setter, a small variation on the pattern, because an audio effect can be added through the UI *after* boot and must still find the one live mic (a setter only wired the boot instance). The active mic registers itself in `setup()` and clears the pointer in `release()`, so add/remove in any order returns either the live frame or a static silent one, never null. A service that only *displays* its readings (the gyro today) skips the consumer side entirely. +**An effect reads a service's data** via the shared-struct pull pattern from [Β§ Data exchange](#data-exchange-between-modules), no new mechanism: the service owns a small POD struct overwritten in place each poll/tick, and the consuming effect holds a `const` pointer to it. The first concrete case is audio: AudioService produces an `AudioFrame` (level + 16-band spectrum + peak) that [AudioSpectrumEffect](moonmodules/light/effects.md) and the other audio effects consume. It reaches the frame through a static `AudioService::latestFrame()` rather than a boot-time setter, a small variation on the pattern, because an audio effect can be added through the UI *after* boot and must still find the one live mic (a setter only wired the boot instance). The active mic registers itself in `setup()` and clears the pointer in `release()`, so add/remove in any order returns either the live frame or a static silent one, never null. A service that only *displays* its readings (the gyro today) skips the consumer side entirely. ## Multi-device runtime diff --git a/docs/assets/light/effects/AudioVolumeEffect.gif b/docs/assets/light/effects/AudioVolumeEffect.gif deleted file mode 100644 index fdc93033..00000000 Binary files a/docs/assets/light/effects/AudioVolumeEffect.gif and /dev/null differ diff --git a/docs/assets/light/effects/AudioVolumeEffect.png b/docs/assets/light/effects/AudioVolumeEffect.png deleted file mode 100644 index 00889855..00000000 Binary files a/docs/assets/light/effects/AudioVolumeEffect.png and /dev/null differ diff --git a/docs/backlog/backlog-core.md b/docs/backlog/backlog-core.md index 882da853..d7356d09 100644 --- a/docs/backlog/backlog-core.md +++ b/docs/backlog/backlog-core.md @@ -128,7 +128,7 @@ declared rather than for a buffer to fill, and to time out on stall rather than RISC-V coprocessor-context save on INTERRUPT ENTRY. That is a symptom of something faulting inside an ISR context rather than a bug in the kernel itself, and the P4 is the only RISC-V target with a coprocessor, which is why no other board shows it. The prior art at - [Plan-20260718](../history/plans/archive/Plan-20260718%20-%20MoonI80%20lapping-v2%20clock-oracle%20ring%20(shipped).md) + `Plan-20260718 - MoonI80 lapping-v2 clock-oracle ring` (in the plans archive) is a DIFFERENT cause with the same panic name (an ISR reading PSRAM while a flash write disabled the cache, fixed with a `spi_flash_cache_enabled()` defer guard) and is worth re-reading first: the same shape on another ISR would present exactly like this. @@ -153,7 +153,7 @@ DevicesModule discovers via **passive UDP presence** (UDP 65506) feeding a [`Dev - **Live peer state** β€” a discovered peer's brightness / on-off shown in our list, refreshed by polling its REST `/json` after discovery gives the IP (discovery = UDP/mDNS, state = REST). The read-side complement to the command half. - **Non-IP transports (board-gated, far future)** β€” Tasmota-MQTT / zigbee2mqtt need an MQTT client; **direct Zigbee/Thread** (S31/C6/H2 802.15.4 radio) makes projectMM the *hub itself*, driving bulbs over the mesh with no gateway β€” the standout differentiator, the biggest lift. Same plugin philosophy, a transport addition + board gate. -Full design + the reasoned transport split: [Plan-20260629 β€” UDP device discovery + mDNS advertise-only (shipped)](../history/plans/archive/Plan-20260629%20-%20UDP%20device%20discovery%20%2B%20mDNS%20advertise-only%20%28shipped%29.md). +Full design + the reasoned transport split: `Plan-20260629 - UDP device discovery + mDNS advertise-only` (in the plans archive). ## MoonBase follow-ups @@ -1073,14 +1073,20 @@ They are **not** CI failures (CI is Debug) and each one inspected so far is a fa Not done with the multi-destination/tab-UI merge because 17 warnings across four core files is its own change, not a tail on someone else's. -## MoonLive core/platform layering + JIT sdkconfig scoping (CodeRabbit #29, 4 findings) +## MoonLive core/platform layering + JIT sdkconfig scoping (CodeRabbit #29, 3 findings left) -Four 🟠 Major boundary findings from the PR #29 review are real but each is its own scoped change, not a tail on the ring branch. The Critical sibling (a `cpl<3` overflow guard in the MoonLive effect's `tick`) landed with the branch it was found on; these four are backlogged: +Four 🟠 Major boundary findings from the PR #29 review are real but each is its own scoped change, not a tail on the ring branch. The Critical sibling (a `cpl<3` overflow guard in the MoonLive effect's `tick`) landed with the branch it was found on. + +**One of the four is CLOSED (2026-09-07):** the scenario now uses `PreviewDriver`, the in-process +sink, instead of `NetworkSendDriver`. Its `tick_us` half was reviewed and DISMISSED rather than +fixed: a `measure` step asserts nothing, it records, and that recording is what feeds repo-health's +per-commit performance trend. A reviewer reading the file could not see that; deleting the +baselines would have blinded the trend to fix nothing. Recorded here so it is not re-raised. + +The three that remain: - **Core includes platform, compiled core in `mm_core`.** `src/core/moonlive/MoonLive.cpp` `#include`s `platform/platform.h` and calls the exec-memory API directly, and the root `CMakeLists.txt` compiles `MoonLive.cpp`/`MoonLiveCompiler.cpp` into `mm_core` and links `mm_core β†’ mm_platform` β€” violating the header-only-core / no-platform-includes contract both files declare. The runtime exec-memory placement layer wants a core-neutral injected interface (or to move out of `src/core`), so the compiled/platform-dependent surface sits behind `mm_platform` and `mm_core` stays INTERFACE-only. These two are one change (same boundary). - **W^X disabled in the board default.** `esp32/sdkconfig.defaults.esp32s3-n16r8` turns off `CONFIG_ESP_SYSTEM_MEMPROT_FEATURE` and enables `CONFIG_HEAP_HAS_EXEC_HEAP` for *every* build on that board, even with no MoonLive effect installed. The JIT genuinely needs a writable-then-executable heap, but that belongs in a dedicated MoonLive/JIT opt-in overlay or an explicit build profile, not the board default β€” so a stock build keeps memory protection on. -- **A scenario rides timing + network.** `test/scenarios/light/scenario_modifier_chain.json` carries `tick_us` baselines (host-performance dependent) and routes a modifier-chain-composition test through `NetworkSendDriver` (pulls network-path behavior into a test that is not about the network). It wants an in-process sink and structural assertions so it stays hermetic, per the `test/**` "no timing or network dependence" rule. - ## MoonI80 prime-only ring: no stall backstop (sibling-path gap) **Found:** πŸ‘Ύ Reviewer, pre-commit on the whole-frame stall fix (2026-07-22). @@ -1342,7 +1348,7 @@ Lower risk than the RGMII case (six pins rather than twelve, and nothing of ours ## Input transports: foot pedals, USB game controllers, and MoonLive at the pins (2026-09-01) -`ButtonService` shipped with the [GPIO seam](../history/plans/Plan-20260901%20-%20Input%20services%20and%20the%20GPIO%20seam.md) +`ButtonService` shipped with the [GPIO seam](../history/plans/Plan-20260901%20-%20Input%20mapping%20and%20scripted%20sensors.md) (`gpioInputBegin` / `gpioRead` / `gpioWrite`). It names a target as `Module.control` and writes it through `Scheduler::setControl`, so a press and an OSC message are indistinguishable downstream. Three follow-ups build on that seam rather than beside it. diff --git a/docs/backlog/backlog-light.md b/docs/backlog/backlog-light.md index f096df52..1da07234 100644 --- a/docs/backlog/backlog-light.md +++ b/docs/backlog/backlog-light.md @@ -368,7 +368,7 @@ So the fix is scoped to the preview, and the open question is where travel is de fixture someone plugged in. The leanest thing that stops the lie. - **The light preset** β€” correct once two different heads run at once, but a preset is a *channel-role* layout today, and carrying physical travel widens what a preset means. Belongs - with the [fixture model](#fixture-model--moving-heads-beams-long-term), not before it. + with the [fixture model](#fixture-model-moving-heads-beams-long-term), not before it. **Positioning is 8-bit while the fixture offers 16.** The bench head has a fine channel for each axis ([light fixtures reference](../reference/light-fixtures.md)); both sit unused, so pan resolves @@ -450,7 +450,7 @@ writes are sequential: for each output row, walk its source row once and emit `s each light's colour, then `memcpy` that finished row to the remaining `scale - 1` rows of the block. Same output, one pass through the destination in address order. -### Sprite follow-ups (draw::sprite + FlyingToasters shipped; [spec + plan](../history/plans/Plan-20260827%20-%20Sprites%20and%20flying%20toasters.md)) +### Sprite follow-ups (draw::sprite + FlyingToasters shipped; spec + plan in the plans archive) Deliberately deferred when sprites landed: P4 PPA acceleration behind the same `draw::sprite` signature (the 2D-DMA blitter the WLED-MM-P4 world uses via LovyanGFX; ours would sit in the @@ -826,3 +826,44 @@ The LED-driver increments **shipped**: increment 1 (RMT/WS2812B single-strand on I2S0 first. (The shared lane-driver scaffolding extraction β€” when a 3rd parallel backend lands β€” is tracked separately under [Β§ Extract shared lane-driver scaffolding](#extract-shared-lane-driver-scaffolding-when-the-3rd-parallel-backend-lands-deferred) above.) + +## FixedPoint fades outside the Layer's aggregation (2026-09-07) + +`FixedPointEffect` calls `draw::fade` directly where every other fading effect calls +`layer()->fadeToBlackBy`, so it skips what the Layer adds: the per-frame aggregation (N effects on +one layer cost one buffer pass, gentlest rate wins), the framerate scaling, the sub-unit carry, and +the buffer-generation bump. Invisible until a second effect shares the layer. + +The swap is one line and was tried: it makes the effect **3.1x brighter at high framerate than at +low**, against the 1.35x band `unit_Effects_framerate` enforces. The two calls mean different +things. `draw::fade(cv, n)` applies n every frame; `fadeToBlackBy(n)` is a RATE per reference frame +that Layer scales by elapsed time. The `fade` control (default 70, range 0..255) was tuned against +the first meaning, so moving it needs the default re-tuned and the trail looked at on a device, not +a silent swap. + +Worth doing, as its own change: it is the last effect outside the shared fade path, and while it +stays outside, "every fade goes through the Layer" is not true. + +## Downloading a scripted palette can repoint the active one (2026-09-06) + +Found while testing the palette-download fixes. A `.mlp` that sorts BEFORE an already-installed one +silently changes what the current selection points at: with `fire.mlp` active at index 60, +downloading `beat-flash.mlp` re-sorts the scripted tail and index 60 becomes `beat-flash.mlp`. The +stored value never moved; what it means did. + +[Palette.h](../../src/light/Palette.h) already reasons about exactly this and solves half of it. A +palette selection is an index: it persists, it rides `seg[0].pal` over the WLED API, and Home +Assistant renders `paletteNames` positionally, so scripted palettes sort AFTER the built-ins and the +sixty built-in indices are fixed forever. What is not solved is the scripted tail among itself, and +that tail now grows at runtime, which is what the download path made reachable. + +Nobody has reported it, and the blast radius is small: a rig with one scripted palette cannot hit it, +and the value re-reads correctly the moment the user picks again. It matters most where a preset or +an MQTT/HA automation stores the index and replays it later, which is the case where nobody is +watching the LEDs when it changes. + +**What it would take:** persist the scripted palette by NAME alongside the index and re-resolve the +index on load, so a stored selection survives a re-sort. `paletteScript` already holds the resolved +file name for the editor, so the value exists; what is missing is using it as the authority when the +list changes. The alternative, appending new scripts rather than sorting them, keeps indices stable +but makes the picker unreadable as the list grows, which is the trade the sort was chosen over. diff --git a/docs/backlog/generative-fields-analysis-bottom-up.md b/docs/backlog/generative-fields-analysis-bottom-up.md index 777ebdc2..c8296656 100644 --- a/docs/backlog/generative-fields-analysis-bottom-up.md +++ b/docs/backlog/generative-fields-analysis-bottom-up.md @@ -156,7 +156,7 @@ Against Part 1's blocks, measured on this tree: | Particles | pool, gravity, drag, bounce, collide, splat render | βœ… | | Stored-field simulation | none | ⬜ (out of scope unless the top-down wants fluid) | -The persistence contract advection needs already exists: the Layer does not clear between frames, and "a read-prior effect reads last frame's pixels via `draw::get` / `draw::blur`; the persistence *is* its state" ([architecture.md Β§ Buffer persistence](../architecture.md#buffer-persistence--the-layer-does-not-clear-each-frame)). What is missing is the resampler and the bit depth. +The persistence contract advection needs already exists: the Layer does not clear between frames, and "a read-prior effect reads last frame's pixels via `draw::get` / `draw::blur`; the persistence *is* its state" ([architecture.md Β§ Buffer persistence](../architecture.md#buffer-persistence-the-layer-does-not-clear-each-frame)). What is missing is the resampler and the bit depth. `PolarNoiseEffect` is the Part 1 shader already: polar addressing, `warp8` in polar space (the angle warped by noise), `kaleido`, a palette, with `octaves` and `warp` exposed as the cost knobs and the header stating the cost ("~4 samples/pixel at octaves=2 … on a large wall drop `octaves` to 1"). @@ -194,7 +194,7 @@ What each shipped target brings, from the IDF SoC capability headers and our own | desktop | GHz class | double and single | NEON / SSE / AVX | unbounded | 20-40Γ— an S3 per core, plus SIMD ([performance.md](../performance.md), the `collide` measurement) | | Teensy 4.x (Cortex-M7), a future target | 1 Γ— 600 MHz | single and double | none (DSP instructions) | 1 MB internal, no PSRAM | not measured; listed in [architecture.md Β§ Scaling to available memory](../architecture.md#scaling-to-available-memory) as a supported class | -**Can the FPU help?** Every target has one, so a float kernel is legal everywhere, and the repo already has the precedent: `raymarch.h` is compiled only where the SoC declares an FPU, as "the one bounded exception to the integer-only render path", while `shader.h` stays fixed point and runs everywhere ([power-functions.md Β§ Raymarching](../moonmodules/light/power-functions.md#raymarching--one-technique-inside-a-shader)). The honest expectation: on these cores a float multiply costs about what an integer multiply costs, so an FPU does not make a noise sample cheaper; it makes square roots, arctangents and trig cheap enough to skip the tables, and it lets a float reference algorithm run unconverted where an exact fixed-point port is not worth writing yet. The portable contract stays fixed point; the FPU is a per-target acceleration behind it, per the standing decision. +**Can the FPU help?** Every target has one, so a float kernel is legal everywhere, and the repo already has the precedent: `raymarch.h` is compiled only where the SoC declares an FPU, as "the one bounded exception to the integer-only render path", while `shader.h` stays fixed point and runs everywhere ([power-functions.md Β§ Raymarching](../moonmodules/light/power-functions.md#raymarching-one-technique-inside-a-shader)). The honest expectation: on these cores a float multiply costs about what an integer multiply costs, so an FPU does not make a noise sample cheaper; it makes square roots, arctangents and trig cheap enough to skip the tables, and it lets a float reference algorithm run unconverted where an exact fixed-point port is not worth writing yet. The portable contract stays fixed point; the FPU is a per-target acceleration behind it, per the standing decision. **Where the family shines on an MCU.** The S3 is the baseline this document measures against because it is the bench board with numbers, not because it is the target. The P4 is the natural home of the shader half: the highest clock, four-lane SIMD, hardware loops and 32 MB of PSRAM put a 64Β² composition and a 128Β² single-layer field inside its budget, and the S31 sits next to it on every axis. The classic is the portable floor, the target that keeps the contract honest. The top-down should size the showcases for the P4 and S31, keep them running on the S3, and let the classic degrade by the cost knobs. diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 94966a51..1e5aa9bb 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -20,7 +20,17 @@ Decided once; not re-derived per file. - **Consider extending before creating.** When adding a feature, check whether an existing module extends cleanly; a new file is fine if genuinely cleaner, but justify it. - **Do not remove comments** unless they are outdated or factually wrong. Comments document intent and context; removing them silently loses knowledge. - **Reference, don't copy.** Prior art (friend repos, datasheets, our own prototype branches) holds proven approaches: study it, take the ideas, write our own code, never copy or trace the structure. Credits live in the [friend-repo digests](friend-repos/README.md) and per-module prior-art sections. -- **Minimal comments in MoonLive scripts.** A `.mle`/`.mll`/`.mlm` is a user-facing artifact shown in an editor on the device's own card, not a C++ source file: the reader is looking at the effect, and a comment block longer than the code buries it. One or two lines at the top saying what the effect IS, and a short note only where a line would otherwise read as a mistake. Everything else, the reasoning behind a formulation, the measured numbers, the language limits it works around, belongs in the commit message or the roadmap. This is the one place the "do not remove comments" rule above yields: on these files, trim. +- **Minimal comments in MoonLive scripts, in exactly three places.** A `.mle`/`.mll`/`.mlm`/`.mls`/`.mlp` is a user-facing artifact shown in an editor on the device's own card, not a C++ source file: the reader is looking at the effect, and a comment block longer than the code buries it. So the budget is fixed, and it is one line each: + + | where | what it says | + |---|---| + | **one line at the top** | what the script IS, in a phrase a user would recognize | + | **one line after each `addControl`** | what that knob does, from the user's side | + | **one line before each function the script defines itself** | what that helper does, so a reader need not decode it | + + The third is the one to get right as scripts grow helpers: a named function is a promise about what it does, and the line is where that promise is written. Lifecycle functions (`tick`, `defineControls`, `placeLights`, `modifyLogical`, …) need no line: their names are the contract and the reader already knows them. + + Everything else, the reasoning behind a formulation, the measured numbers, the language limits it works around, belongs in the commit message or the roadmap. This is the one place the "do not remove comments" rule above yields: on these files, trim. - **Present-tense litmus.** "There is no MCLK pin" states a property (keep); "no X anymore" narrates a removal (cut it; describe the path that exists). ## Prefer integers, store values in their native shape @@ -198,7 +208,7 @@ Two corollaries. **Reference a module generically** in prose outside its own hom - `light/{effects,modifiers,layouts,drivers,supporting}.md` β€” the light-catalog + light-supporting pages (a type may later split by library into `effects_wled.md` / `effects_moonmodules.md`, still flat). - `core/{services,supporting,ui}.md` β€” the core-services (user-facing modules), core-supporting, and web-UI summary pages. - **The Links column** is assembled by the hook in a fixed order β€” **πŸ§ͺ Tests Β· πŸ“„ Technical Β· attribution Β· βŒ„ details** β€” each with a Material icon (`:material-…:`, rendered as inline SVG by `pymdownx.emoji`, the same mechanism as the tag emoji in the Name column) so a link's *type* is scannable. A card carries these lines: a `[Tests](../../../tests/unit-tests.md#)` line (omitted when the module has no unit test β€” a missing Tests link truthfully means "untested"), a **`Detail: [technical](../moxygen/.md)`** line pointing at the generated technical page, and an `Origin:` attribution line. `check_specs` matches each block to its `.h` via that `moxygen/.md` link, so keep the link's target on it. + **The Links column** is assembled by the hook in a fixed order (**πŸ§ͺ Tests Β· πŸ“„ Technical Β· attribution Β· βŒ„ details**), each with a Material icon (`:material-…:`, rendered as inline SVG by `pymdownx.emoji`, the same mechanism as the tag emoji in the Name column) so a link's *type* is scannable. A card carries these lines: a `[Tests](../../tests/unit-tests.md#)` line (omitted when the module has no unit test, so a missing Tests link truthfully means "untested"), a **`Detail: [technical](../moxygen/.md)`** line pointing at the generated technical page, and an `Origin:` attribution line. `check_specs` matches each block to its `.h` via that `moxygen/.md` link, so keep the link's target on it. Cross-file design rationale that no single `.h` owns (module interactions, buffer-lifecycle coupling) is a prose section beneath a summary page's table β€” a `## β€” details` section the hook links from the row as `βŒ„ details`. That's the only home for it, so a module needs no page of its own. diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 8adcdf3d..5d23369c 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -85,7 +85,7 @@ Leave **Release** and **Firmware** at their suggested values (the newest stable build, and the firmware that matches your device). Tick **Erase chip first** only if you're starting clean, switching firmware, or updating a 4 MB classic board (esp32 / wrover / eth) from a release before v4.0. That last update must erase: -its partition layout changed ([MIGRATING](../MIGRATING.md)), and if the device already holds +its partition layout changed ([MIGRATING](MIGRATING.md)), and if the device already holds config you care about, back it up first ("Back up a device's config first" on the installer page): erasing wipes WiFi credentials and all settings, and the backup brings them back after the flash (its report lists anything it could not carry). @@ -304,6 +304,43 @@ keep going. --- +### If your device shows MoonBase + +**MoonBase** is a small recovery image built into your device. If a firmware update +is interrupted, or an installed firmware does not start, your device boots MoonBase +instead of going dark, and its page offers you three ways out: + +- **Boot the app** puts you straight back if the firmware is still fine. Try this + first: it changes nothing on the device. +- **From a file** installs a firmware you have already downloaded. Get the + `firmware-...bin` matching your device from the + [releases page](https://github.com/MoonModules/projectMM/releases). +- **From a URL** downloads and installs in one step. The releases page gives you a + link to each file; paste it here and your device fetches it directly. + +Installing takes a few minutes, and the page reports its progress as it downloads. +Your device reboots into the new firmware on its own when it finishes. + +Two things worth knowing. A failed install **stays** in MoonBase rather than +pretending to have worked, so you can simply try again. And you cannot break a +device this way: MoonBase is never overwritten by an update, so it is still there +for the next attempt, including after a power cut in the middle of one. + +If your device is not on your network at all, MoonBase opens its own WiFi access +point and is reachable at **4.3.2.1** once you join it. + +MoonBase shows its own version on its page, and your device's Firmware card shows +which MoonBase it carries. If that version is marked outdated, the same card installs +a newer one over the network, so keeping the recovery image current needs no cable. + +That update runs from the app, because only the running app can write the partition +MoonBase lives in. So it is a way to keep MoonBase fresh, not a way back from a +device that will not start: if the app cannot run, or MoonBase itself will not boot, +that still takes a cable. Your device checks the image first, refusing anything whose +magic bytes, chip or description say it is not a MoonBase image for this chip. + +--- + ### Where to go next - **Understand the pipeline** β€” how layouts, layers, effects, modifiers and diff --git a/docs/history/plans/OPEN-WORK.md b/docs/history/plans/OPEN-WORK.md new file mode 100644 index 00000000..c39920f3 --- /dev/null +++ b/docs/history/plans/OPEN-WORK.md @@ -0,0 +1,39 @@ +# Open work in the unarchived plans + +What is left in each plan still sitting in this folder, audited against the tree on 2026-09-07. One +line per item, pointing rather than restating: the plan itself is the description, this is the +worklist. A plan leaves this file and moves to `archive/` when its last line here is struck. + +Everything not listed here shipped and was archived. + +## Small, and closable in a sitting + +| plan | what is left | +|---|---| +| **MoonLive palettes** | The promised scenario: a scripted palette driving a real effect end to end. Everything else landed. | +| **OSC control ingest** | `unit_OscModule` and an OSC scenario, both promised. Only `unit_OscPacket` exists. | +| **Two-way control surfaces** | `unit_ControlSurface` + `unit_OscModule`, a scenario, and the seam section in `docs/reference/control-surfaces.md`. The code is complete. | +| **Config backup and restore** | One sentence: the installer's erase-confirm should point at backing up config first (`mooninstaller/install.js`). | + +## Bench-gated: needs hardware, not keyboard time + +| plan | what is left | +|---|---| +| **Input mapping and scripted sensors** | Steps 2 and 3 are host-verified but NOT bench-verified. Also open: the `.mls` picker confirmation and the `services.md` MoonLiveService card. | + +## Larger, and its own effort + +| plan | what is left | +|---|---| +| **Input mapping and scripted sensors** | Steps 4 / 4b / 5 / 6 not started: I2C sensors (MPU6050), VL53L8CX zone grid, pulse timing + `EncoderService`, PIR level events. | +| **MoonLight migration (multi-stage)** | Stage 5's transport (wired DMX-512 in/out) is the one Must-class item for the rename. Also 3 installation-specific layouts, ~31 effects (mostly few-line ports), and LightsControl. Its own Status section has the detail. | + +## Worth knowing + +**Desktop audio capture is shipped** and archived: its text still says "remaining before shipped", +but the fleet test it names was later marked verified, leaving only a post-merge run by a Windows +tester, which is not a deliverable. + +**Four of the six partials are missing only tests or a doc line.** That is the pattern worth acting +on: the features landed and the pinning did not, which is exactly the gap that goes unnoticed until +something breaks. Closing all four is a sitting's work and would archive three more plans. diff --git a/docs/history/plans/Plan-20260630 - MoonLight migration (multi-stage).md b/docs/history/plans/Plan-20260630 - MoonLight migration (multi-stage).md index 515984ba..a5dd53bf 100644 --- a/docs/history/plans/Plan-20260630 - MoonLight migration (multi-stage).md +++ b/docs/history/plans/Plan-20260630 - MoonLight migration (multi-stage).md @@ -33,19 +33,91 @@ Two cross-cutting rules govern every stage, from [CLAUDE.md](../../../CLAUDE.md) No other hidden hard dependencies: our `EffectBase` + extrude (now 1D-along-Y, matching MoonLight) + `Buffer` already provide the render context. -## Status β€” verified 2026-08-24 +## Status β€” verified 2026-09-07 -Measured against the tree, not inferred from the stages below. +Measured against both trees on 2026-09-07 (96 commits after the previous status), by counting +MoonLight's `name()` declarations against our registered modules and script library. Counts are +what the trees say, not what the stages below predicted. + +| | MoonLight | projectMM | Gap | +|---|---|---|---| +| Effects | 88 | 66 compiled + 32 scripted | see below | +| Modifiers | 9 (+1 template) | 11 | **none: complete, plus 2 of our own** | +| Layouts | 16 (+1 template) | 17 | **3 absent, all installation-specific** | +| Drivers | 11 (+1 template) | 17 | **3 absent: DMX in/out, HUB75, IMU** | | Stage | State | |---|---| -| 1 β€” Foundations | **shipped.** `src/light/Palette.h` (16-entry CRGBPalette16 model, gradient stops from MoonLight's palettes.h), `src/light/draw.h`, the FastLED-named primitives, GoL re-port. | -| 2 β€” Doc model | **not started.** `docs/moonmodules/` is still `core/` + `light/`; no `effects_.md` pages, `check_specs.py` still on the per-module contract. | -| 3+ β€” Effect batches | **partial.** 52 effects, 12 modifiers, 18 layouts registered (baseline was ~21 / 5 / 3). 44 MoonLive scripts, a delivery route this plan did not anticipate. | -| 4 β€” Modifiers + layouts | **partial**, counted above. | -| 5 β€” Moving heads / DMX | **not started**, and the largest remaining gap. No DMX-512 output driver exists; RS-485 is backlog-only ([backlog-light](../../backlog/backlog-light.md#rs-485-dmx-512-wired-output-future-the-physical-dmx-driver)) and the fixture model is marked long-term and **undesigned** ([backlog-light](../../backlog/backlog-light.md#fixture-model-moving-heads-beams-long-term)). Design precedes the driver here. | - -**Release positioning (product owner, 2026-08-24):** v4.0.0 ships as the scripting-and-desktop release and does **not** take the MoonLight name. Replacing MoonLight moves to v5.0.0, gated on four things beyond effect breadth: DMX light bars and moving heads (tested on hardware), the LightsControl module reaching maturity, MoonLive palettes, and a documentation pass. +| 1 β€” Foundations | **shipped.** Palette, draw primitives, the FastLED-named set, GoL re-port. | +| 2 β€” Doc model | **shipped, differently.** The per-library `effects_.md` split was NOT built; the catalog is one page per TYPE (`effects.md`, `layouts.md`, `modifiers.md`, `drivers.md`) with a table row per module. That solves the same problem the stage existed for (no per-module explosion) with fewer pages, and `check_specs.py` enforces it. **The stage as written is obsolete: what shipped is better and the ADR's premise (library as a doc split) went unused.** | +| 3 β€” Effect batches | **substantially done.** 66 compiled effects and 32 scripted, against a ~21 baseline. | +| 4 β€” Modifiers + layouts | **done for modifiers** (all 9 ported). **Layouts: 14 of 16**, the three absent ones being specific installations rather than shapes. | +| 5 β€” Moving heads / DMX | **partial, and now the largest gap.** The EFFECT side shipped (`MovingHeadEffect` plus 5 `mh-*.mle` scripts, with pan/tilt/zoom/rotate/gobo reachable from both C++ and script). The TRANSPORT did not: there is still no DMX-512 output driver, and no DMX input. | + +### What is genuinely missing (2026-09-07) + +**Drivers, the real gap.** Three of MoonLight's have no counterpart here: + +- **DMX Out** (and **DMX In**). The fixture model, the channel roles and the moving-head effects all + landed, so a head can be driven over Art-Net today; what is missing is WIRED DMX-512 over RS-485. + Tracked in [backlog-light Β§ RS-485](../../backlog/backlog-light.md), where the analysis notes the + channel-mapping half is already solved and what remains is the transport (a UART in RS-485 mode, + break/mark timing) plus a physical transceiver. **This is the one Must-class gap for the rename.** +- ~~**HUB75.**~~ **Out of scope, decided 2026-09-07.** MoonLight drives these panels; projectMM + will not. No longer a gap: a choice. +- **IMU.** Sensor input beyond the microphone. The rename doc already files this as a Could. + +**Layouts (3):** `16 Rings`, `SE16`, `LightCrafter16`. Each is one installation's wiring rather than +a reusable shape. **The product owner owns all three (2026-09-07)**, so they are real parity items +and each is bench-verifiable once written: a layout is a coordinate iterator, so these are small, +and the hardware to check them against is on hand. + +**Effects: 57 of MoonLight's 88 do not match ours by NAME, but a behavior-by-behavior read of both +trees puts the real gap at 31.** The other 26 exist here under a different name or in a reduced form: + +- **15 covered.** Fixed-Point Canvas Demo is our `FixedPointEffect`, Scrolling Text is `TextEffect`, + Noise 2D and Noise Move are two `motion` settings of one `NoiseEffect`, Waterfall and Freq Wave + are both `FreqMatrixEffect`, Julia is `fractal.mle`, the Troy / Wowi / Ambient heads are the + `mh-*.mle` scripts, and our `VuMetersEffect` is richer than the original. +- **11 partial**, where something related exists but is meaningfully less: Audio Rings vs + `RadialSpectrum` (no per-band ring history), Meteor vs `comet-trail.mle` (no randomized per-pixel + trail decay), Drip vs `rain.mle` (no bounce physics), Popcorn vs `Ballpit` (no per-kernel pop), + Puddles vs `Blurz`, Noise Fire vs `Fire`, and the Troy / Freq Colors audio-band-to-gobo mapping. +- **31 absent**, and the shape of that list is the useful finding: + +| Theme | Count | Effort | +|---|---|---| +| Audio-reactive (Grav*, DJ Light, Freq Map/Pixels, Rocktaves, Ripple Peak, Waverly, Funky Plank) | 11 | A few lines each. Our `AudioFrame` is a SUPERSET of MoonLight's `sharedData`, so these are mechanical. The `Grav*` trio wants one shared ~15-line gravity/peak helper. | +| Geometric / oscillator (Blackhole, DNA, Frizzles, Oscillate, Radar, Pixel Map, Blink Rainbow) | 7 | A few lines each on `BeatPhase` + `draw::line` + `blur`. Radar wants a ~25-line perimeter walk. | +| Noise (Phased Noise, Plasmoid) | 2 | A few lines: 1D phase accumulators. | +| 1D strip (Flow, Police) | 2 | A few lines each. | +| Fire / volumetric (Spiral Fire) | 1 | Moderate, ~50 lines: a real 3D cone-surface test on the existing `PolarLut`. | +| Particle agents (Ants) | 1 | Substantial, ~150 lines: a food-gathering agent model with no analogue here. | +| Content-bound (Mario Test, Moon Man) | 2 | Mario is a few lines on the existing sprite path. **Moon Man is not portable**: it needs M5GFX PNG decoding and an embedded blob. | +| Other (Heartbeat, FLAudio, and the partials above) | 5 | Heartbeat is a few lines. **FLAudio is not an effect gap but an AUDIO-PIPELINE one**: it visualizes `fl_kick`/`fl_snare`/`fl_bpm`/`fl_vocalConfidence`, fields our analyzer does not produce. | + +**So the effect work is mostly small and unblocked.** About 25 of the 31 are few-line ports against +primitives that already exist; two are genuinely not portable as-is (Moon Man's PNG dependency, +FLAudio's missing audio fields), and two are real work (Ants, Spiral Fire). Nothing structural +blocks any of it: the palette, primitives, audio pipeline and draw set all exist. + +### The v5.0.0 gates, re-checked + +The previous status recorded four gates beyond effect breadth. Two have since shipped: + +- **MoonLive palettes β€” SHIPPED.** The research below is now history rather than a design note: the + `setPalEntry`/`setPalEntryHSV` builtins exist, `.mlp` is the palette script kind, and 5 factory + palettes ship. The open design questions it lists were answered by the implementation (a palette + script is a module ticked by its binding, and the picker lists `.mlp` files from both the user and + factory directories). **Keep the section for its record of where the design came from; do not + read it as outstanding work.** +- **DMX light bars and moving heads β€” HALF.** Effects and the fixture/channel model shipped and are + bench-verified over Art-Net; wired DMX output has not. See the driver gap above. +- **LightsControl maturity β€” NOT STARTED.** No such module exists. `LightPresetsModule` is the + fixture-preset library, a different thing. Still backlogged + ([backlog-mixed](../../backlog/backlog-mixed.md)). +- **Documentation pass β€” OPEN**, and cheaper than it was: the catalog pages exist and + `check_specs.py` keeps them honest, so what remains is a read-through rather than a build-out. ### MoonLive palettes β€” how MoonLight does it (research, 2026-08-24) @@ -86,6 +158,55 @@ void loop() { Checkout note: the MoonLight tree read for this research was at `65869217` (2026-05-26) and may lag upstream; re-fetch before implementing. +## What is left to replace MoonLight (the product owner's decision list) + +The question this plan now answers is not "how do we migrate" but "what is still missing before +projectMM can take the name". Grouped by whether it BLOCKS the rename, on the evidence above. + +**Blocking, in the sense that a predecessor user would notice it missing:** + +1. **Wired DMX-512 output.** The only Must-class gap. Everything above the wire exists (fixture + model, channel roles, moving-head effects, bench-verified over Art-Net); what is missing is the + RS-485 transport and a board that carries a transceiver. This is also the item with a hardware + dependency, so it has the longest lead time: worth starting before the smaller work. +2. ~~A decision on HUB75.~~ **DECIDED 2026-09-07: OUT OF SCOPE.** MoonLight drives HUB75 panels and + projectMM will not. Recorded here so it stays a decision rather than resurfacing as an unknown. + +**Not blocking, and mostly small:** + +3. **The 31 absent effects**, of which roughly 25 are few-line ports on primitives that already + exist. This is the "does the library feel thin" gate, and at 66 compiled plus 32 scripted against + MoonLight's 88 it is arguably already met on count. Worth picking the ones that close CATEGORY + gaps a user would feel (the audio-reactive eleven) rather than working the list top to bottom. +4. **The 11 partials**, each a refinement of something that already works. +5. **Three layouts** (`16 Rings`, `SE16`, `LightCrafter16`), each one installation's wiring. + **The product owner owns this hardware (2026-09-07), so all three are real parity items** rather + than speculative ports, and each can be verified on the bench once written. +6. **LightsControl**, the one v5 gate that has not started. Still a backlog design question. +7. **DMX In and IMU**, both filed as Could in the rename doc and unchanged by this review. + +**Moon Man is NOT portable as-is**: it needs M5GFX PNG decoding plus an embedded image blob. + +**FLAudio is an audio-pipeline question, not an effect one**, and it splits cleanly. It draws nine +columns; four are already expressible with what we have, five are not: + +- **Already ours.** Bass / mid / treble levels are aggregates of our `bands[]`. Its beat flag and + beat confidence are our `onset` and `flux`, where `onset` is arguably better: it fires on the one + block a hit is detected rather than staying latched. +- **Per-instrument onsets** (`kick`, `snare`, `tom`, `hihat`): spectral flux computed PER FREQUENCY + REGION rather than across the whole spectrum, plus a per-drum debounce. Textbook, and the per-band + data it needs already exists. **Worth building on its own merit**: per-band onset is a capability + many effects would use, not just this one. +- **BPM**: tempo estimation from inter-onset intervals (autocorrelation over several seconds of + history). Real DSP with a memory budget. Useful for anything that locks to tempo; a bigger job. +- **Vocal detection** (`vocalsActive`, `vocalConfidence`): formant-band energy against the total, a + spectral-shape classifier. Highest cost, least reliable, and one effect consumes it. + **Recommended: skip**, and let FLAudio ship without those two columns if it ships at all. + +**What is already done and no longer needs tracking:** modifiers (all of them, plus two of ours), +14 of 16 layouts, the palette and primitive foundation, MoonLive palettes, the moving-head effect +and fixture model, and the doc model (in a better shape than this plan proposed). + ## Stages ### Stage 1 β€” Foundations (palette + primitives + GoL re-port) @@ -106,7 +227,15 @@ The proving-ground stage: build the shared tools, prove them on one hard effect. Stage-1 exit: palette + primitives compile (-Werror), are unit-tested (each primitive pinned: `beatsin8` range, `inoise8` determinism, `qadd8` saturation, `drawLine` endpoints in 1D/2D/3D), GoL re-port renders correctly + has a scenario, tags legend documented. **No doc explosion yet** (GoL keeps its existing single `.md`; the doc-model change is Stage 2). -### Stage 2 β€” Doc model: per-library pages ← next +### Stage 2 β€” Doc model: per-library pages ← SUPERSEDED (see Status) + +**What shipped instead is one page per TYPE**, not per library: `effects.md`, `layouts.md`, +`modifiers.md`, `drivers.md`, each a table of module rows. It solves the doc-explosion problem +this stage existed for, with four pages rather than a dozen, and `check_specs.py` enforces a +row per registered module. The plan below is kept as the record of what was considered; the +per-library page names and the `registerType` remapping it describes were not built and are not +wanted. [ADR-0015](../../adr/0015-library-is-a-tag-not-a-folder.md)'s conclusion still holds for +`src`/`assets`/`tests` (library is a tag, not a folder); only its doc-page half went unused. Before migrating dozens of effects (which would create dozens of `.md`s), switch the doc model. The naming + structure is fixed by the [folder-structure decision](../../adr/0015-library-is-a-tag-not-a-folder.md): **`src`/`assets`/`tests` are `domain/type` folders, flat β€” library is NOT a folder there**, only a `tags()` emoji; **docs** are the one place library splits, as a **page name** (type-first, underscore-joined, matching how you'd read the folder path): `effects_moonlight.md`, `effects_wled.md`, `effects_projectmm.md`, … (and `modifiers_.md` etc. only where a library has that type β€” most libraries are effects-only). @@ -129,11 +258,22 @@ With foundations + doc model in place, migrate MoonLight effects in **themed bat The batch order below is by dependency/complexity (refine per batch), and **cuts ACROSS the source files** (an audio-reactive batch pulls GEQ3D+PaintBrush from E_MoonModules and the GEQ/Blurz family from E_WLED together) rather than migrating one file at a time β€” themed batches keep each commit coherent: -- **3a β€” simple 2D/3D non-audio** (the `E_MoonLight` / `E_WLED` geometric ones: lines, scrolling, lissajous, distortion, starfield…). -- **3b β€” palette-heavy** (now that palettes exist: the gradient/noise/plasma family not yet ported). -- **3c β€” particle/physics** (bouncing balls, popcorn, blackhole β€” build on the draw primitives + PRNG). -- **3d β€” audio-reactive (β™«)** (GEQ, Blurz, Waverly, FreqMatrix… β€” depend on `AudioModule::latestFrame()`; a shared audio-read helper may be its own small sub-stage). -- **3e β€” text/scrolling** (scrolling text needs a font + glyph blitter β€” its own primitive). +**Superseded by the 2026-09-07 status: 3a, 3b, 3c and 3e are substantially done.** What remains, +re-derived from the two trees rather than from the original guess at themes: + +- **3d β€” audio-reactive**, and it is now the LARGEST and CHEAPEST remaining batch: eleven effects + (`Grav Center`, `Grav Centric`, `Grav Freq`, `DJ Light`, `Freq Map`, `Freq Pixels`, `Rocktaves`, + `Ripple Peak`, `Waverly`, `Funky Plank`, and the `Puddles`/`Puddle Peak` partials). Our + `AudioFrame` already exposes more than MoonLight's `sharedData`, so these are mechanical ports on + an existing pipeline. The three `Grav*` share one small gravity/peak helper, which is the only new + primitive the batch needs. **Recommended next batch if effect breadth is the goal.** +- **3f β€” geometric leftovers**: `Blackhole`, `DNA`, `Frizzles`, `Oscillate`, `Radar`, `Pixel Map`, + `Blink Rainbow`, `Phased Noise`, `Plasmoid`, `Flow`, `Police`, `Heartbeat`. A few lines each. +- **3g β€” the genuinely substantial two**: `Ants` (an agent model with food-gathering rules, no + analogue here) and `Spiral Fire` (a 3D cone-surface test on the existing `PolarLut`). Worth their + own commit rather than being buried in a batch. +- **Not portable, do not batch**: `Moon Man` (M5GFX PNG dependency) and `FLAudio` (needs audio + fields our analyzer does not produce). See the status section. ### Stage 4 β€” Modifiers + layouts migration @@ -141,7 +281,20 @@ The MoonLight modifiers (mirror/tile/kaleidoscope/pinwheel/transpose…) and lay ### Stage 5 β€” Moving heads / DMX fixtures (last) -`E_MovingHeads` + fixture layouts + Art-Net moving-head control. Most specialised, fewest dependencies on the rest; deferred to last. +**Split in two by what actually happened, and only half is left.** + +**Done (2026-09):** the fixture model (`FixtureChannels`, the `ChannelRole` vocabulary), +`MovingHeadEffect` with formations and audio reactivity, the five role setters reachable from +both C++ and MoonLive scripts (pan, tilt, zoom, rotate, gobo), and five `mh-*.mle` scripts +carrying MoonLight's Troy / Wowi / Ambient looks. A head is drivable today over Art-Net. + +**Left:** the WIRED transport. A DMX-512 output driver over RS-485 (UART, break/mark-after-break +timing, a transceiver on the board) and, if wanted, DMX input. The channel-mapping half is +already solved by the per-light channel model, so this is a transport and a hardware question +rather than a domain one: [backlog-light Β§ RS-485](../../backlog/backlog-light.md) has the +analysis. **This is the one remaining Must-class item for the rename**, and since 2026-09-07 it is this +plan's alone: the Release 4 scope plan also listed it, shipped without it, and closed pointing here. +One home for it now. ## Riskiest parts diff --git a/docs/history/plans/Plan-20260722 - Release 4 scope - effect breadth + rename runway.md b/docs/history/plans/Plan-20260722 - Release 4 scope - effect breadth + rename runway.md deleted file mode 100644 index 9bd21226..00000000 --- a/docs/history/plans/Plan-20260722 - Release 4 scope - effect breadth + rename runway.md +++ /dev/null @@ -1,44 +0,0 @@ -# Plan β€” Release 4 scope: effect breadth + the rename runway - -## Context - -Release 3 is being cut now. This plan captures the **Release 4** candidates β€” the next strategic thread after R3 β€” so the direction is recorded before the work starts. The product owner's steer: the items below are R4, not R3. - -The backlog has one dominant strategic thread that most other items orbit: the **projectMM β†’ MoonLight rename** ([backlog rename plan](../../backlog/rename-to-moonlight.md)). Its gate is *"the effect library must not feel thin next to the predecessor's 60+ effects."* Two in-flight plans feed that gate, and R4 is where they land. The shape of R4 is therefore **"the effects release + the rename runway"**: grow visible feature breadth while moving the single most important strategic gate (rename readiness), and leave the hardware-verification-bound driver work to its own dedicated push. - -This is a roadmap/scope plan, not a single-feature `/plan`. Each item below gets its own `/plan` + commit when reached; this document is the *map* and the *why*. - -## The spine β€” effect-breadth parity (headline) - -**MoonLight migration, Stage 1 + the next effect batch.** ([Plan-20260630 - MoonLight migration (multi-stage)](Plan-20260630%20-%20MoonLight%20migration%20(multi-stage).md).) - -This is the biggest lever and the explicit *"execution vehicle for the effect-breadth parity gate."* ~21 of the predecessor's 60+ effects are ported. Stage 1's prerequisites are the highest-value core work available, because every future effect leans on them: - -- **Shared palette** β€” hard prerequisite; many effects color via `ColorFromPalette`. Generalize the pattern `PlasmaPaletteEffect` hard-codes today. -- **The shared primitive library** β€” FastLED-named, our own implementation, hot-path-tuned integer-only: `beatsin8`, `inoise8`, `qadd8`, `nscale8`, `random8`/`random16`, `ColorFromPalette`, and the dimension-agnostic draw set. Extends the existing `color.h` (`scale8`, `sin8`). -- **Tag/emoji legend** β€” settle before batch-migrating so every module is consistent from batch one. -- **Per-library doc model** β€” `effects_.md` compact table rows (per [ADR 0015](../../adr/0015-library-is-a-tag-not-a-folder.md)); changes the `check_specs.py` contract. - -Then the next migration batch on top. This is the R4 headline: it unblocks the rename *and* is pure user-visible feature growth. - -## Two quick wins β€” scoped and ready - -- **Active-instance election primitive.** ([Plan-20260710 - Active-instance election primitive](Plan-20260710%20-%20Active-instance%20election%20primitive.md).) A core `ActiveInstance` that removes duplicated singleton-election bookkeeping from `AudioService` + `DevicesModule` (both had real dangling-static bugs). Textbook *Complexity-lives-in-core* subtraction; small; in flight. -- **CodeRabbit #29 boundary findings (4).** ([backlog-core Β§ MoonLive core/platform layering](../../backlog/backlog-core.md#moonlive-coreplatform-layering-jit-sdkconfig-scoping-coderabbit-29-4-findings).) MoonLive core-includes-platform + compiled-into-`mm_core`, W^X disabled in the board default, a scenario riding timing + network. Real, already scoped; good hygiene to close before a named release. - -## The RS-485 / DMX-512 opportunity (candidate, larger) - -The [P4-shield RS-485/DMX hardware is now well documented](../../reference/mhc-wled-esp32-p4-shield.md) (the builder's schematics landed 2026-07-16). The **RS-485 / DMX-512 wired-output driver** + its **`platform::` UART-RS485 seam** ([backlog-light](../../backlog/backlog-light.md#rs-485-dmx-512-wired-output-future-the-physical-dmx-driver)) is demand-driven and self-contained. It is a meaty new capability β€” a flagship candidate if R4 wants a headline new-hardware feature alongside the effects work, but it is larger than the two quick wins and should be its own `/plan`. - -## High-light-count driver work (in R4 β€” hardware-verified) - -The streaming-ring / lane-driver work is **in R4**. It is hardware-verification-heavy β€” each item needs the expander wall (and the relevant board) to prove, so these land with bench sign-off, not blind: - -- **Classic-ESP32 shift-register ring on raw I2S** ([backlog-light "WANTED"](../../backlog/backlog-light.md#drivers)) β€” the high-light-count classic driver. -- **P4 Parlio streaming ring** ([backlog-light "WANTED"](../../backlog/backlog-light.md#drivers)) β€” lift the P4 Parlio ceiling past ~21K to light-count-independent. -- **Shared lane-driver scaffolding** β€” extract when the 3rd parallel backend lands (deferred until then, but that 3rd backend is one of the two above). -- **MoonI80 prime-only ring stall backstop** ([backlog-core](../../backlog/backlog-core.md#mooni80-prime-only-ring-no-stall-backstop-sibling-path-gap)) + the whole-frame late-EOF serialization hardening β€” the sibling-path recovery gaps; verify on the expander wall. - -## Success shape - -R4 ships when: the migration Stage-1 primitives + the next effect batch have landed (moving the rename's breadth gate forward), the `ActiveInstance` primitive and the CodeRabbit #29 boundary fixes are in, the RS-485/DMX driver reaches a verified first output, and the high-light-count driver work above is bench-verified. The rename itself is a *separate* cutover (its own plan); R4 is the runway that makes the name not a downgrade, not the switch. diff --git a/docs/history/plans/archive/Plan-20260522 - UI rewrite to ui-spec.md baseline (item 12) (shipped).md b/docs/history/plans/archive/Plan-20260522 - UI rewrite to ui-spec.md baseline (item 12) (shipped).md index bd2283bf..82cc974c 100644 --- a/docs/history/plans/archive/Plan-20260522 - UI rewrite to ui-spec.md baseline (item 12) (shipped).md +++ b/docs/history/plans/archive/Plan-20260522 - UI rewrite to ui-spec.md baseline (item 12) (shipped).md @@ -193,10 +193,10 @@ Three small additions in `test/`: - [test/CMakeLists.txt](test/CMakeLists.txt) β€” register **Docs:** -- [docs/moonmodules/core/SystemModule.md](docs/moonmodules/core/SystemModule.md) -- [docs/moonmodules/core/HttpServerModule.md](docs/moonmodules/core/HttpServerModule.md) -- [docs/moonmodules/core/MoonModule.md](docs/moonmodules/core/MoonModule.md) -- [docs/testing.md](docs/testing.md) +- [docs/moonmodules/core/SystemModule.md](../../../moonmodules/core/moxygen/SystemModule.md) +- [docs/moonmodules/core/HttpServerModule.md](../../../moonmodules/core/moxygen/HttpServerModule.md) +- [docs/moonmodules/core/MoonModule.md](../../../moonmodules/core/moxygen/MoonModule.md) +- [docs/testing.md](../../../testing.md) - `git mv docs/moonmodules_draft/core/ui-spec.md docs/moonmodules/core/ui-spec.md` - [docs/plan.md](docs/plan.md) β€” remove step 12 - [docs/history/plan-11.md](docs/history/plan-11.md) β€” new diff --git a/docs/history/plans/archive/Plan-20260523 - Top-level shape change to `Layouts`, `Layers`, `Drivers` (shipped).md b/docs/history/plans/archive/Plan-20260523 - Top-level shape change to `Layouts`, `Layers`, `Drivers` (shipped).md index dd243bcb..60e7ae80 100644 --- a/docs/history/plans/archive/Plan-20260523 - Top-level shape change to `Layouts`, `Layers`, `Drivers` (shipped).md +++ b/docs/history/plans/archive/Plan-20260523 - Top-level shape change to `Layouts`, `Layers`, `Drivers` (shipped).md @@ -184,12 +184,12 @@ Wait β€” that adds noise. Let me reconsider: ### Spec updates -- **[docs/moonmodules/light/Layer.md](docs/moonmodules/light/Layer.md)** β€” update intro to "renders into a buffer sized by either the full Layouts extent or a carved region (start/end controls)." Document the new `setLayouts` method. -- **[docs/moonmodules/light/Layouts.md](docs/moonmodules/light/Layouts.md)** β€” rename from `LayoutGroup.md`; class is `Layouts`. Body mostly unchanged (still describes the index-stitching). -- **[docs/moonmodules/light/Layers.md](docs/moonmodules/light/Layers.md)** β€” NEW. Describes the container: holds N Layers, runs each in order in `loop()`, future home of the composed-buffer logic. Single-line forward-reference to the composition follow-up. -- **[docs/moonmodules/light/drivers/Drivers.md](docs/moonmodules/light/drivers/Drivers.md)** β€” rename from `DriverGroup.md`; class is `Drivers`. +- **[docs/moonmodules/light/Layer.md](../../../moonmodules/light/moxygen/Layer.md)** β€” update intro to "renders into a buffer sized by either the full Layouts extent or a carved region (start/end controls)." Document the new `setLayouts` method. +- **[docs/moonmodules/light/Layouts.md](../../../moonmodules/light/moxygen/Layouts.md)** β€” rename from `LayoutGroup.md`; class is `Layouts`. Body mostly unchanged (still describes the index-stitching). +- **[docs/moonmodules/light/Layers.md](../../../moonmodules/light/moxygen/Layers.md)** β€” NEW. Describes the container: holds N Layers, runs each in order in `loop()`, future home of the composed-buffer logic. Single-line forward-reference to the composition follow-up. +- **[docs/moonmodules/light/drivers/Drivers.md](../../../moonmodules/light/moxygen/Drivers.md)** β€” rename from `DriverGroup.md`; class is `Drivers`. - **[docs/architecture-light.md](docs/architecture-light.md)** β€” update the pipeline diagram and any prose that names `LayoutGroup`/`DriverGroup`/singular `Layer`. The "UI integration (light domain)" tree shape gets `Layouts β†’ Layers β†’ Drivers` at the top level. -- **[docs/moonmodules/light/EffectBase.md](docs/moonmodules/light/EffectBase.md)** β€” passing reference: parent is still `Layer`, no change. +- **[docs/moonmodules/light/EffectBase.md](../../../moonmodules/light/moxygen/EffectBase.md)** β€” passing reference: parent is still `Layer`, no change. - **[docs/plan.md](docs/plan.md)** β€” add a `Multi-Layer composition (pending)` entry covering (a) compose, (b) per-Layer start/end carving activation. - **[README.md](README.md)** β€” scan for module type names; update if any examples use `LayoutGroup`/`DriverGroup`. @@ -218,7 +218,7 @@ Wait β€” that adds noise. Let me reconsider: 4. **Add `class Layers`** in [src/light/Layers.h](src/light/Layers.h). Add `setLayouts()` to `Layer`. main.cpp creates `Layers` containing one `Layer`. Run all tests; live-verify with a desktop run that the pipeline still produces frames. 5. **Add `start/end` controls to `Layer`** β€” uint16 (or int16 if available) with sensible bounds. Default = whole layout. `rebuildLUT()` honours them when not at default. Update `test_layer*.cpp` and add a test asserting "Layer with default start/end matches old Layer behaviour byte-for-byte." 6. **UI emoji pick** for `ModuleRole::Layer` β€” ask the product owner. Add to `ROLE_EMOJI` map in [src/ui/app.js](src/ui/app.js). -7. **Update specs** ([Layer.md](docs/moonmodules/light/Layer.md), new [Layouts.md](docs/moonmodules/light/Layouts.md), new [Layers.md](docs/moonmodules/light/Layers.md), new [Drivers.md](docs/moonmodules/light/drivers/Drivers.md), [architecture-light.md](docs/architecture-light.md), [plan.md](docs/plan.md), [README.md](README.md) if needed). Run [check_specs.py](moondeck/check/check_specs.py). +7. **Update specs** ([Layer.md](../../../moonmodules/light/moxygen/Layer.md), new [Layouts.md](../../../moonmodules/light/moxygen/Layouts.md), new [Layers.md](../../../moonmodules/light/moxygen/Layers.md), new [Drivers.md](../../../moonmodules/light/moxygen/Drivers.md), [architecture-light.md](docs/architecture-light.md), [plan.md](docs/plan.md), [README.md](README.md) if needed). Run [check_specs.py](moondeck/check/check_specs.py). 8. **Migration**: FilesystemModule deletes `.config/LayoutGroup.json` and `.config/DriverGroup.json` if present, logs a warning. 9. **All pre-commit gates 1–6** (build, ctest, scenarios, platform boundary, specs, ESP32). Reviewer agent (gate 7) after. diff --git a/docs/history/plans/archive/Plan-20260525 - Release-channel picker + first-boot WiFi provisioning (shipped).md b/docs/history/plans/archive/Plan-20260525 - Release-channel picker + first-boot WiFi provisioning (shipped).md index e32ed1cc..bf5a8f51 100644 --- a/docs/history/plans/archive/Plan-20260525 - Release-channel picker + first-boot WiFi provisioning (shipped).md +++ b/docs/history/plans/archive/Plan-20260525 - Release-channel picker + first-boot WiFi provisioning (shipped).md @@ -418,7 +418,7 @@ Track 3 total: **3.0 h**. Sequential within the track. Hardware verification (st - [src/core/FirmwareUpdateModule.h](src/core/FirmwareUpdateModule.h) β€” on-device OTA MoonModule (header-only). - [src/core/ImprovProvisioningModule.h](src/core/ImprovProvisioningModule.h) β€” Improv listener MoonModule (header-only). **Track 3.** - [moondeck/build/improv_provision.py](moondeck/build/improv_provision.py) β€” pyserial CLI for headless / rack provisioning. **Track 3.** -- [docs/moonmodules/core/ImprovProvisioningModule.md](docs/moonmodules/core/ImprovProvisioningModule.md) β€” spec page. **Track 3.** +- [docs/moonmodules/core/ImprovProvisioningModule.md](../../../moonmodules/core/moxygen/ImprovProvisioningModule.md) β€” spec page. **Track 3.** - [docs/history/plan-18.md](docs/history/plan-18.md) β€” this plan's archive. **Edited:** diff --git a/docs/history/plans/archive/Plan-20260626 - MoonLive Stage 0 (native codegen spike) (shipped).md b/docs/history/plans/archive/Plan-20260626 - MoonLive Stage 0 (native codegen spike) (shipped).md index 538adf24..da93babe 100644 --- a/docs/history/plans/archive/Plan-20260626 - MoonLive Stage 0 (native codegen spike) (shipped).md +++ b/docs/history/plans/archive/Plan-20260626 - MoonLive Stage 0 (native codegen spike) (shipped).md @@ -1,6 +1,6 @@ # Plan β€” MoonLive Stage 0: native-codegen load-bearing spike -> Approved plan record (CLAUDE.md *Plan before implementing*). Implements the first, smallest step of [livescripts-analysis-top-down.md](../../backlog/livescripts-analysis-top-down.md) β€” its Stage 0 "load-bearing spike", split one notch finer so the single novel hardware risk is isolated and proven before any compiler front-end is written. S3-only, bare-minimum assembler, near-zero language. +> Approved plan record (CLAUDE.md *Plan before implementing*). Implements the first, smallest step of [livescripts-analysis-top-down.md](../../../backlog/livescripts-analysis-top-down.md) β€” its Stage 0 "load-bearing spike", split one notch finer so the single novel hardware risk is isolated and proven before any compiler front-end is written. S3-only, bare-minimum assembler, near-zero language. ## Goal @@ -30,7 +30,7 @@ This is "small in depth AND broad": depth = one statement; broad = the whole ver ## Architecture placement (respecting the boundaries) -Per [Β§3.9](../../backlog/livescripts-analysis-top-down.md) (domain-neutral engine core, thin binding) and the **platform boundary** hard rule (ISA codegen lives only in `src/platform//`): +Per [Β§3.9](../../../backlog/livescripts-analysis-top-down.md) (domain-neutral engine core, thin binding) and the **platform boundary** hard rule (ISA codegen lives only in `src/platform//`): ``` src/platform/platform.h ← + allocExec/freeExec seam (declaration only) diff --git a/docs/history/plans/archive/Plan-20260627 - MoonLive Stage 3 (IR seam + assembler, second statement) (shipped).md b/docs/history/plans/archive/Plan-20260627 - MoonLive Stage 3 (IR seam + assembler, second statement) (shipped).md index 2d1822ed..55590693 100644 --- a/docs/history/plans/archive/Plan-20260627 - MoonLive Stage 3 (IR seam + assembler, second statement) (shipped).md +++ b/docs/history/plans/archive/Plan-20260627 - MoonLive Stage 3 (IR seam + assembler, second statement) (shipped).md @@ -1,6 +1,6 @@ # Plan β€” MoonLive Stage 3: the IR seam + a tiny assembler (second statement) -> Approved plan record (CLAUDE.md *Plan before implementing*). The next rung of MoonLive after the shipped 1aβ†’1bβ†’2 + P4 spike: add the **second statement kind** to the language, which forces the **typed IR** and a **per-ISA assembler** to earn their place (the current ASTβ†’emitFill shortcut only works for one fixed routine). Builds on [livescripts-analysis-top-down.md](../../backlog/livescripts-analysis-top-down.md) Β§3.2 (IR seam), Β§4 (bounds-check at the IR), Β§3.4 (host built-ins). +> Approved plan record (CLAUDE.md *Plan before implementing*). The next rung of MoonLive after the shipped 1aβ†’1bβ†’2 + P4 spike: add the **second statement kind** to the language, which forces the **typed IR** and a **per-ISA assembler** to earn their place (the current ASTβ†’emitFill shortcut only works for one fixed routine). Builds on [livescripts-analysis-top-down.md](../../../backlog/livescripts-analysis-top-down.md) Β§3.2 (IR seam), Β§4 (bounds-check at the IR), Β§3.4 (host built-ins). ## Goal diff --git a/docs/history/plans/archive/Plan-20260630 - Stage 1 palette (shipped).md b/docs/history/plans/archive/Plan-20260630 - Stage 1 palette (shipped).md index fd852a6c..6e533a0a 100644 --- a/docs/history/plans/archive/Plan-20260630 - Stage 1 palette (shipped).md +++ b/docs/history/plans/archive/Plan-20260630 - Stage 1 palette (shipped).md @@ -1,6 +1,6 @@ # Plan β€” Stage 1 (palette) of the MoonLight migration -The first executable slice of the [migration plan](Plan-20260630%20-%20MoonLight%20migration%20(multi-stage).md): the palette foundation. Design already decided in moonlight-palettes-data.md; this plan is the file split + the implementation specifics. The shared **primitive library** (beat/noise/blend/draw) and the **GoL re-port** are *separate* slices of Stage 1, planned + committed after this β€” palette is the load-bearing one, done first and alone so it's reviewable. +The first executable slice of the [migration plan](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md): the palette foundation. Design already decided in moonlight-palettes-data.md; this plan is the file split + the implementation specifics. The shared **primitive library** (beat/noise/blend/draw) and the **GoL re-port** are *separate* slices of Stage 1, planned + committed after this β€” palette is the load-bearing one, done first and alone so it's reviewable. ## What ships diff --git a/docs/history/plans/archive/Plan-20260630 - Stage 1 primitive library (math8 + noise + draw + blend) (shipped).md b/docs/history/plans/archive/Plan-20260630 - Stage 1 primitive library (math8 + noise + draw + blend) (shipped).md index c1918033..b3c0e469 100644 --- a/docs/history/plans/archive/Plan-20260630 - Stage 1 primitive library (math8 + noise + draw + blend) (shipped).md +++ b/docs/history/plans/archive/Plan-20260630 - Stage 1 primitive library (math8 + noise + draw + blend) (shipped).md @@ -1,6 +1,6 @@ # Plan β€” Stage 1 primitive library (math8 + noise + draw + blend) -The remaining foundation of [MoonLight migration Stage 1](./Plan-20260630%20-%20MoonLight%20migration%20(multi-stage).md) (palette + tags-legend already shipped in `d00559c`). Builds the shared, hot-path-tuned integer primitives every migrated effect (Stage 3+) will call, so each effect stays short by leaning on one recognisable set instead of re-rolling beat/noise/blend/draw per effect. +The remaining foundation of [MoonLight migration Stage 1](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md) (palette + tags-legend already shipped in `d00559c`). Builds the shared, hot-path-tuned integer primitives every migrated effect (Stage 3+) will call, so each effect stays short by leaning on one recognisable set instead of re-rolling beat/noise/blend/draw per effect. **Prior art:** FastLED β€” the canonical 8-bit-fixed-point LED library. We carry its *ideas and recognisable names* (`beatsin8`, `inoise8`, `qadd8`, `nscale8`, `random8`, `fadeToBlackBy`, `blend`) and write our own implementation against our architecture, crediting FastLED at each file's header. FastLED's own split is the model: `lib8tion` (math+timing+random), `noise` (inoise), `colorutils` (blend/fade), `hsv2rgb` (color) β€” draw/Bresenham lives in its 2D/matrix add-ons, not core. diff --git a/docs/history/plans/archive/Plan-20260630 - Stage 3 E_MoonModules batch (GameOfLife + GEQ3D + PaintBrush) (shipped).md b/docs/history/plans/archive/Plan-20260630 - Stage 3 E_MoonModules batch (GameOfLife + GEQ3D + PaintBrush) (shipped).md index b0c950b9..ea03e0d4 100644 --- a/docs/history/plans/archive/Plan-20260630 - Stage 3 E_MoonModules batch (GameOfLife + GEQ3D + PaintBrush) (shipped).md +++ b/docs/history/plans/archive/Plan-20260630 - Stage 3 E_MoonModules batch (GameOfLife + GEQ3D + PaintBrush) (shipped).md @@ -1,6 +1,6 @@ # Plan β€” Stage 3 E_MoonModules batch (GameOfLife + GEQ3D + PaintBrush) -The first effect-migration batch of [MoonLight migration Stage 3](./Plan-20260630%20-%20MoonLight%20migration%20(multi-stage).md): port all three effects in MoonLight's `Nodes/Effects/E_MoonModules.h` (MoonModules-authored set), built fresh on the Stage-1 primitives (palette, `math8`, `noise`, `draw`). One commit (PO decision). Each effect: study behaviour β†’ reimplement against EffectBase β†’ unit + scenario test β†’ `effects.md` row. +The first effect-migration batch of [MoonLight migration Stage 3](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md): port all three effects in MoonLight's `Nodes/Effects/E_MoonModules.h` (MoonModules-authored set), built fresh on the Stage-1 primitives (palette, `math8`, `noise`, `draw`). One commit (PO decision). Each effect: study behaviour β†’ reimplement against EffectBase β†’ unit + scenario test β†’ `effects.md` row. **Method (CLAUDE.md):** study the MoonLight source for *behaviour* (controls, algorithm, state), then write our own code on our architecture β€” never trace/copy. FastLED/MoonLight credited as prior art in each `tags()` + the effect's header + the `effects.md` row. diff --git a/docs/history/plans/archive/Plan-20260702 - Docs system overhaul (Phase 0 through Docs v2) (shipped).md b/docs/history/plans/archive/Plan-20260702 - Docs system overhaul (Phase 0 through Docs v2) (shipped).md index 7a904abc..14774c64 100644 --- a/docs/history/plans/archive/Plan-20260702 - Docs system overhaul (Phase 0 through Docs v2) (shipped).md +++ b/docs/history/plans/archive/Plan-20260702 - Docs system overhaul (Phase 0 through Docs v2) (shipped).md @@ -34,7 +34,7 @@ but excluded from the published nav. ~104 `docs/install/`β†’`web-installer/` ref projectMM-native), one table row per module. `check_specs.py` moved from file-scoped to **page-scoped** control-name validation. Drivers stayed per-file at the time (later folded into `drivers.md` + per-driver moxygen pages by Docs v2). Library stays a *tag* + a *doc split*, never a folder axis (the -[folder-structure decision](../../adr/0015-library-is-a-tag-not-a-folder.md)). +[folder-structure decision](../../../adr/0015-library-is-a-tag-not-a-folder.md)). ### Phase 1+2 β€” nav fold + generated tests in the build Phase 1 (audience-split nav) was mostly delivered by Phase 0. Phase 2: the test-inventory pages @@ -104,5 +104,5 @@ built fresh) from its `.h` `///` comments; each catalog summary row links to it. - **Doxide** β€” abandoned (above); moxygen delivered the goal. - **Per-library page splits** (`effects_wled.md`) β€” future growth, a lift-not-rewrite when a library section outgrows its page; the flat filenames + sections are already in place for it. -- **assets/ and test/ type-splits** β€” the [folder-structure decision](../../adr/0015-library-is-a-tag-not-a-folder.md)'s +- **assets/ and test/ type-splits** β€” the [folder-structure decision](../../../adr/0015-library-is-a-tag-not-a-folder.md)'s remaining "mirror src's domain/type shape" work; independent of the doc-content overhaul. diff --git a/docs/history/plans/archive/Plan-20260704 - IrModule brightness (shipped).md b/docs/history/plans/archive/Plan-20260704 - IrModule brightness (shipped).md index a011ef28..d294833d 100644 --- a/docs/history/plans/archive/Plan-20260704 - IrModule brightness (shipped).md +++ b/docs/history/plans/archive/Plan-20260704 - IrModule brightness (shipped).md @@ -15,7 +15,7 @@ Confirmed with the product owner: per board via `deviceModels.json`; NOT a hardcoded child of System. - **Start minimal, grow later** β€” brightness up/down now; richer remote mapping is a follow-up. -Backlog alignment ([backlog-mixed.md](../../backlog/backlog-mixed.md)): IR is named as an input +Backlog alignment ([backlog-mixed.md](../../../backlog/backlog-mixed.md)): IR is named as an input for the eventual **LightsControl** hub. This module is the thin IR *input* peripheral; when LightsControl is built it consumes IR via the same static seam (the `AudioModule::latestFrame()` pattern). This does not build LightsControl β€” it builds the IR input and one concrete action. diff --git a/docs/history/plans/archive/Plan-20260706 - Home Assistant MQTT Discovery (shipped).md b/docs/history/plans/archive/Plan-20260706 - Home Assistant MQTT Discovery (shipped).md index 3ba1a25f..dfb7c986 100644 --- a/docs/history/plans/archive/Plan-20260706 - Home Assistant MQTT Discovery (shipped).md +++ b/docs/history/plans/archive/Plan-20260706 - Home Assistant MQTT Discovery (shipped).md @@ -23,7 +23,7 @@ Feature branch `ha-mqtt-discovery` (already created; carries the earlier backlog ## Decisions locked (PO) - **JSON schema** discovery, not default schema. -- **`unique_id` = ``** (the stable id per [ADR-0010](../../adr/0010-integration-identity-stable-hardware-id.md)), `name` = `SystemModule::deviceName()`. Never the editable name as identity. +- **`unique_id` = ``** (the stable id per [ADR-0010](../../../adr/0010-integration-identity-stable-hardware-id.md)), `name` = `SystemModule::deviceName()`. Never the editable name as identity. - Gated on a new **`haDiscovery`** bool control (default on where MQTT ships); toggling re-announces / retracts. - Existing mqttthing topics unchanged; Discovery is additive. diff --git a/docs/history/plans/archive/Plan-20260708 - TasksModule (nested task view) (shipped).md b/docs/history/plans/archive/Plan-20260708 - TasksModule (nested task view) (shipped).md index 6cc82be8..dde2183f 100644 --- a/docs/history/plans/archive/Plan-20260708 - TasksModule (nested task view) (shipped).md +++ b/docs/history/plans/archive/Plan-20260708 - TasksModule (nested task view) (shipped).md @@ -2,9 +2,9 @@ ## Context -projectMM's architecture already commits (🚧, not yet built) to per-module core affinity: "each MoonModule can declare a core affinity; the scheduler respects this when pinning tasks" ([architecture.md Β§ Parallelism](../../architecture.md#parallelism)), and the backlog holds *Task core-pinning* and a *core-1 driver task*. None of the *optimization* exists yet β€” and you can't optimize what you can't see. This module is the **observability foundation**: show every FreeRTOS task and the projectMM modules that run inside each, with cost. Inspired by MoonLight's [`ModuleTasks`](https://github.com/MoonModules/MoonLight/blob/main/src/MoonBase/Modules/ModuleTasks.h) (a flat task table); projectMM's version nests modules under their task and leans on projectMM's *already-free* per-module self-report. +projectMM's architecture already commits (🚧, not yet built) to per-module core affinity: "each MoonModule can declare a core affinity; the scheduler respects this when pinning tasks" ([architecture.md Β§ Parallelism](../../../architecture.md#parallelism)), and the backlog holds *Task core-pinning* and a *core-1 driver task*. None of the *optimization* exists yet β€” and you can't optimize what you can't see. This module is the **observability foundation**: show every FreeRTOS task and the projectMM modules that run inside each, with cost. Inspired by MoonLight's [`ModuleTasks`](https://github.com/MoonModules/MoonLight/blob/main/src/MoonBase/Modules/ModuleTasks.h) (a flat task table); projectMM's version nests modules under their task and leans on projectMM's *already-free* per-module self-report. -Critical framing + the System-Modules taxonomy this fits into: [docs/backlog/system-modules.md](../../backlog/system-modules.md). (The original pre-implementation spec draft was deleted once the module shipped β€” its final spec is [core/system.md Β§ Tasks](../../moonmodules/core/system.md#tasks) + the `TasksModule.h` `///`.) +Critical framing + the System-Modules taxonomy this fits into: [docs/backlog/system-modules.md](../../../backlog/system-modules.md). (The original pre-implementation spec draft was deleted once the module shipped β€” its final spec is [core/system.md Β§ Tasks](../../../moonmodules/core/system.md#tasks) + the `TasksModule.h` `///`.) ## Decisions locked (PO) diff --git a/docs/history/plans/archive/Plan-20260709 - PinsModule (ownership map, strap-conflict flags, live-state; 4 increments) (shipped).md b/docs/history/plans/archive/Plan-20260709 - PinsModule (ownership map, strap-conflict flags, live-state; 4 increments) (shipped).md index 735c2e10..5bfce326 100644 --- a/docs/history/plans/archive/Plan-20260709 - PinsModule (ownership map, strap-conflict flags, live-state; 4 increments) (shipped).md +++ b/docs/history/plans/archive/Plan-20260709 - PinsModule (ownership map, strap-conflict flags, live-state; 4 increments) (shipped).md @@ -1,6 +1,6 @@ # Plan β€” PinsModule: the GPIO pin map, four increments (shipped) -Consolidated record of the four-increment Pins effort (per CLAUDE.md *Plan before implementing* β€” a multi-phase effort's per-phase plans may be merged into one `(shipped)` record once the whole effort lands, preserving each phase's design-intent arc). All four shipped 2026-07-09. The forward-looking source is the [top-down study](../../backlog/pins-analysis-top-down.md), Β§8 of which defines the increments. +Consolidated record of the four-increment Pins effort (per CLAUDE.md *Plan before implementing* β€” a multi-phase effort's per-phase plans may be merged into one `(shipped)` record once the whole effort lands, preserving each phase's design-intent arc). All four shipped 2026-07-09. The forward-looking source is the [top-down study](../../../backlog/pins-analysis-top-down.md), Β§8 of which defines the increments. ## Why (the problem the whole effort solves) @@ -26,4 +26,4 @@ Adds the **second axis** β€” *what is GPIO N doing right now.* Per Β§6 this is * ## The through-line (why these four cohere) -Each increment is read-only and additive on the last, and the recurring discipline is the **UI-sidestep rule**: every richer-UI need was met by a *generic* list affordance (the `severity`β†’color convention in #2, reused unchanged by #3; the scalar-fields path needing nothing in #4), never a pins-specific control. The map surfaces ownership (#1), safety (#2), conflicts (#3), and live electrical state (#4) β€” and at no point becomes an allocation subsystem or a policy engine, exactly the scope guard the top-down study draws. Later phases (reject-on-add on the installer path, the `pinConflicts()` validator authority, output-suppression, the board-diagram view, ADC/continuity live-state) remain in the [top-down study](../../backlog/pins-analysis-top-down.md) as the forward scope. +Each increment is read-only and additive on the last, and the recurring discipline is the **UI-sidestep rule**: every richer-UI need was met by a *generic* list affordance (the `severity`β†’color convention in #2, reused unchanged by #3; the scalar-fields path needing nothing in #4), never a pins-specific control. The map surfaces ownership (#1), safety (#2), conflicts (#3), and live electrical state (#4) β€” and at no point becomes an allocation subsystem or a policy engine, exactly the scope guard the top-down study draws. Later phases (reject-on-add on the installer path, the `pinConflicts()` validator authority, output-suppression, the board-diagram view, ADC/continuity live-state) remain in the [top-down study](../../../backlog/pins-analysis-top-down.md) as the forward scope. diff --git a/docs/history/plans/archive/Plan-20260709 - Split System into System + Services (shipped).md b/docs/history/plans/archive/Plan-20260709 - Split System into System + Services (shipped).md index 24e81161..589efc3d 100644 --- a/docs/history/plans/archive/Plan-20260709 - Split System into System + Services (shipped).md +++ b/docs/history/plans/archive/Plan-20260709 - Split System into System + Services (shipped).md @@ -2,7 +2,7 @@ ## Context -Today `ModuleRole::Peripheral` conflates two categories that both parent under **System**: the genuinely **user-added capability bridges** (Audio, IR β€” optional, add/delete), and **fixed things that borrow the role only to render a delete button** (TasksModule was given `Peripheral`+delete for exactly that; I2cScan and FileManager carry it while being always-there). Network's own children (MQTT, Devices) are a third, separate thing β€” always-there infra, wired-by-code, never user-added. The design note [docs/backlog/system-modules.md](../../backlog/system-modules.md) settles the split: +Today `ModuleRole::Peripheral` conflates two categories that both parent under **System**: the genuinely **user-added capability bridges** (Audio, IR β€” optional, add/delete), and **fixed things that borrow the role only to render a delete button** (TasksModule was given `Peripheral`+delete for exactly that; I2cScan and FileManager carry it while being always-there). Network's own children (MQTT, Devices) are a third, separate thing β€” always-there infra, wired-by-code, never user-added. The design note [docs/backlog/system-modules.md](../../../backlog/system-modules.md) settles the split: - **System Modules** β€” fixed, wired-by-code, no add/delete: System's vitals + the fixed inspection modules (Tasks, I2cScan; later Memory, Pins) + always-there infra (Network, Firmware, Improv). - **Service Modules** (a new top-level **Services** container) β€” user-added, add/delete/replace, `ModuleRole::Service`: Audio, IR. (I2cScan β†’ a fixed System Module β€” it inspects this-device hardware; MQTT/Improv/Devices stay code-wired β€” see Β§3.) diff --git a/docs/history/plans/archive/Plan-20260709 - Unify lifecycle - onBuildState is the sole enabled-gate (shipped).md b/docs/history/plans/archive/Plan-20260709 - Unify lifecycle - onBuildState is the sole enabled-gate (shipped).md index 888d1111..7b3dc9de 100644 --- a/docs/history/plans/archive/Plan-20260709 - Unify lifecycle - onBuildState is the sole enabled-gate (shipped).md +++ b/docs/history/plans/archive/Plan-20260709 - Unify lifecycle - onBuildState is the sole enabled-gate (shipped).md @@ -1,10 +1,10 @@ # Plan β€” Unify the module lifecycle: `onBuildState` is the sole enabled-gate -> **As implemented (3e37987) β€” the design evolved during build.** This plan proposed "Option B": `onBuildState()` stays the single hook and *builds the empty state* (releases everything) when `!effectivelyEnabled()`, with `buildState()` always calling it on every node (decisions #1 and #3 below). During implementation that was sharpened one step further into a cleaner central router: **`MoonModule::applyState()`** is the sole orchestration point β€” it calls `onBuildState()` (a pure *build*, no `enabled()` check) on an effectively-enabled node and **`teardown()`** (release) on a disabled one, recursing the tree. So `onBuildState()` is NOT the "sole gate" the title says and does NOT build-empty-when-disabled; the *release* lives in `teardown()`, and `applyState()` β€” not the caller β€” decides which runs. The Scheduler's boot Phase-4 sweep and `buildState()` call `applyState()` (not `onBuildState()` directly). Everything else below (effective-enabled cascade #2, CLASS-1 vs CLASS-2, the goal of zero per-module `enabled()` gates) shipped as written. Kept as the intent record; read the code + [lessons.md](../lessons.md) for the final shape. +> **As implemented (3e37987) β€” the design evolved during build.** This plan proposed "Option B": `onBuildState()` stays the single hook and *builds the empty state* (releases everything) when `!effectivelyEnabled()`, with `buildState()` always calling it on every node (decisions #1 and #3 below). During implementation that was sharpened one step further into a cleaner central router: **`MoonModule::applyState()`** is the sole orchestration point β€” it calls `onBuildState()` (a pure *build*, no `enabled()` check) on an effectively-enabled node and **`teardown()`** (release) on a disabled one, recursing the tree. So `onBuildState()` is NOT the "sole gate" the title says and does NOT build-empty-when-disabled; the *release* lives in `teardown()`, and `applyState()` β€” not the caller β€” decides which runs. The Scheduler's boot Phase-4 sweep and `buildState()` call `applyState()` (not `onBuildState()` directly). Everything else below (effective-enabled cascade #2, CLASS-1 vs CLASS-2, the goal of zero per-module `enabled()` gates) shipped as written. Kept as the intent record; read the code + [lessons.md](../../lessons.md) for the final shape. ## Context -The [disable-releases-resources commit](Plan-20260709%20-%20Disabling%20releases%20resources%20(onEnabled%20per%20module)%20(shipped,%20superseded).md) left the same idea β€” "a disabled module holds no resources" β€” expressed in **two mechanisms** across ~7 modules, plus ~24 self-`enabled()` gates scattered through `setup()`/`onBuildState()`/`onCorrectionChanged()`/setters. The product owner's read (correct, verified in code): this is **sharpening, not a rewrite** β€” the orchestration already exists and runs; 10+ effects already release-on-disable through `onBuildState`; the delete cascade is already correct. (The full design study that fed this plan, `docs/backlog/lifecycle-unification-analysis.md`, was retired once the work shipped β€” its analysis is folded into this plan and [lessons.md](../lessons.md).) +The [disable-releases-resources commit](Plan-20260709%20-%20Disabling%20releases%20resources%20(onEnabled%20per%20module)%20(shipped,%20superseded).md) left the same idea β€” "a disabled module holds no resources" β€” expressed in **two mechanisms** across ~7 modules, plus ~24 self-`enabled()` gates scattered through `setup()`/`onBuildState()`/`onCorrectionChanged()`/setters. The product owner's read (correct, verified in code): this is **sharpening, not a rewrite** β€” the orchestration already exists and runs; 10+ effects already release-on-disable through `onBuildState`; the delete cascade is already correct. (The full design study that fed this plan, `docs/backlog/lifecycle-unification-analysis.md`, was retired once the work shipped β€” its analysis is folded into this plan and [lessons.md](../../lessons.md).) **Decisions (product owner, this session):** 1. **Option B** β€” `onBuildState()` is the single "(re)build my derived state for my current (controls, enabled)" entry point, and **`enabled==false` builds the *empty* state (release everything β€” memory AND hardware)**. Retire `onEnabled` for resource acquire/release. @@ -63,7 +63,7 @@ No β€” decision 3 is always-call. `buildState()` keeps calling every node; the ` - **Core:** `MoonModule.h` (`effectivelyEnabled()` + the two doc contracts), `AudioService.h`, `DevicesModule.h`, `IrService.h` (fold release into onBuildState, drop onEnabled). `MqttModule.h` β€” **no change** (its onEnabled is the sanctioned edge-trigger; add a one-line doc noting why it's exempt). - **Light:** `RmtLedDriver.h`, `ParallelLedDriver.h`, `NetworkSendDriver.h`, `NetworkReceiveEffect.h` (fold + drop onEnabled + remove setup gate); the ~10 effects (`enabled()`β†’`effectivelyEnabled()` in onBuildState). `DriverBase.h` β€” `releaseOnDisable` helper is no longer called from onEnabled; either repurpose it as the release helper onBuildState's disabled branch calls, or delete if each driver's teardown suffices (decide during impl β€” subtraction preferred). - **Tests:** extend the three existing boot-gate tests to assert via `effectivelyEnabled`; **add cascade tests** (Β§ Verification). -- **Docs:** `architecture.md` (the lifecycle section β€” one gate, the contract), `lessons.md` (fold the arc: the three traps collapse into "onBuildState is the release gate, keyed on effective-enabled"). Retire the now-shipped study (`backlog/lifecycle-unification-analysis.md`) and the [backlog entry](../../backlog/backlog-core.md) per *Mandatory subtraction*. Mark this plan `(shipped)`. +- **Docs:** `architecture.md` (the lifecycle section β€” one gate, the contract), `lessons.md` (fold the arc: the three traps collapse into "onBuildState is the release gate, keyed on effective-enabled"). Retire the now-shipped study (`backlog/lifecycle-unification-analysis.md`) and the [backlog entry](../../../backlog/backlog-core.md) per *Mandatory subtraction*. Mark this plan `(shipped)`. ## Verification diff --git a/docs/history/plans/archive/Plan-20260710 - Rename module hooks to prepare-tick-release (shipped).md b/docs/history/plans/archive/Plan-20260710 - Rename module hooks to prepare-tick-release (shipped).md index 9ef5ea5e..6bdaec9d 100644 --- a/docs/history/plans/archive/Plan-20260710 - Rename module hooks to prepare-tick-release (shipped).md +++ b/docs/history/plans/archive/Plan-20260710 - Rename module hooks to prepare-tick-release (shipped).md @@ -20,7 +20,7 @@ ## Context -Writing the ["Build your own MoonModules" guide](../../usecases/build-your-own-moonmodules.md) surfaced a naming concern (product-owner remark R4): the hook vocabulary a module author must learn isn't as friendly as it could be. +Writing the ["Build your own MoonModules" guide](../../../usecases/build-your-own-moonmodules.md) surfaced a naming concern (product-owner remark R4): the hook vocabulary a module author must learn isn't as friendly as it could be. 1. **`onBuildControls` vs `onBuildState` read almost identically** (both `onBuild…`) yet do very different things β€” declare UI controls vs build derived state/memory. The shared prefix *causes* the "wait, are these the same?" confusion the remark names. 2. **`onBuildState` (acquire) and `teardown` (release) are opposites but don't read as a pair** β€” nothing in the names signals "these two are the build/unbuild halves." diff --git a/docs/history/plans/archive/Plan-20260710 - Scratch buffer helper for memory-holding effects (shipped).md b/docs/history/plans/archive/Plan-20260710 - Scratch buffer helper for memory-holding effects (shipped).md index b3110823..51f93428 100644 --- a/docs/history/plans/archive/Plan-20260710 - Scratch buffer helper for memory-holding effects (shipped).md +++ b/docs/history/plans/archive/Plan-20260710 - Scratch buffer helper for memory-holding effects (shipped).md @@ -2,7 +2,7 @@ ## Context -Writing the ["Build your own MoonModules" guide](../../usecases/build-your-own-moonmodules.md) surfaced a recurring boilerplate across memory-holding effects: allocate a heap buffer in `onBuildState()` (sized to the grid, re-alloc only if the count changed), free it in `teardown()`, free it again in the destructor, hand-write a private `release()` helper, and guard `loop()` with `if (!buf_) return;`. This is the pattern in **~11 effects** (Fire, GameOfLife, GEQ, Tetrix, Particles, StarField, StarSky, BouncingBalls, Solid, NetworkReceive, Wave) β€” several hold *multiple* buffers (GameOfLife allocates 3 planes, StarSky 5, StarField 3), so the bookkeeping is real. It's textbook *[Complexity lives in core](../../../CLAUDE.md#principles)*: the same non-trivial lifecycle wants to live once in a core primitive, so each effect drops to "declare the buffer, use it." +Writing the ["Build your own MoonModules" guide](../../../usecases/build-your-own-moonmodules.md) surfaced a recurring boilerplate across memory-holding effects: allocate a heap buffer in `onBuildState()` (sized to the grid, re-alloc only if the count changed), free it in `teardown()`, free it again in the destructor, hand-write a private `release()` helper, and guard `loop()` with `if (!buf_) return;`. This is the pattern in **~11 effects** (Fire, GameOfLife, GEQ, Tetrix, Particles, StarField, StarSky, BouncingBalls, Solid, NetworkReceive, Wave) β€” several hold *multiple* buffers (GameOfLife allocates 3 planes, StarSky 5, StarField 3), so the bookkeeping is real. It's textbook *[Complexity lives in core](../../../CLAUDE.md#principles)*: the same non-trivial lifecycle wants to live once in a core primitive, so each effect drops to "declare the buffer, use it." Three product-owner remarks on the guide drove this (R6 "should `release()` be a hook", R7 "`if (!heat_) return` sounds like orchestration, hide it", and β€” writing the memory example β€” "`static_cast` should not be used by module makers"). The answer to all three is the same primitive. diff --git a/docs/history/plans/archive/Plan-20260712 - Step 1.5 async transmit double-buffer (shipped).md b/docs/history/plans/archive/Plan-20260712 - Step 1.5 async transmit double-buffer (shipped).md index 2a9de453..4b22cafc 100644 --- a/docs/history/plans/archive/Plan-20260712 - Step 1.5 async transmit double-buffer (shipped).md +++ b/docs/history/plans/archive/Plan-20260712 - Step 1.5 async transmit double-buffer (shipped).md @@ -9,13 +9,13 @@ Shipped on both parallel peripherals (LCD/S3 + Parlio/P4) as the `asyncTransmit` | **P4 / Parlio** (16Γ—256) | ~10,820 Β΅s | ~3,790 Β΅s | **48 β†’ 76 fps** | | **S3 / LCD_CAM** (16Γ—144, SE16) | ~17,200 Β΅s | ~11,700 Β΅s | ~15 β†’ ~16 (masked) | -Proven on **both** parallel peripherals β€” the double-buffer overlaps the WS2812 wire wait on each (P4 driver βˆ’65%, S3 driver βˆ’32%, both matching the measured `wireUs`). The whole-board fps win is clean on the P4 (+58%); on the SE16 the driver gain is real but *masked* by ~50 ms of other per-tick overhead on that board (a heavy 128Γ—128 render), so its system fps barely moves β€” the driver-level proof stands, the board-level demo doesn't. A git-worktree baseline at the previous commit confirmed async-OFF reproduces the prior P4 behavior exactly (10,820 vs 10,787 Β΅s) β€” **provably no regression**. The design cut vs the original plan: (1) `tick()` is two explicit branches (`tickSync` = the literal original path, `tickAsync` = deferred-wait), so OFF is byte-for-byte the pre-change timing; (2) allocation follows the flag β€” OFF allocates ONE DMA buffer (costs nothing), ON requests the second (degrades to sync if it won't fit); (3) Parlio uses the **same whole-frame double-buffer as LCD**, not a ring (the KPI 4096-light frame fits one transfer β€” the ring stays Step 4, and per the "65K is a network problem" call is now deferred-indefinitely). Added a **`wireUs` read-only KPI** = the measured DMA wire time (start-of-transmit β†’ done-callback, an in-order completion FIFO pairs each done with its start), live "7474 Β΅s (133 fps max)" on the P4 β€” the pure output floor, so the fps ceiling is measured not assumed (and it tracks an overclocked slot rate directly). The remaining gap (system 76 fps β†’ wire 133 fps) is the render loop; the effect (~7.3 ms) is the next bottleneck β†’ Step 2 multicore. Full story + the measurement trap that nearly buried the win: [lessons.md](../lessons.md). +Proven on **both** parallel peripherals β€” the double-buffer overlaps the WS2812 wire wait on each (P4 driver βˆ’65%, S3 driver βˆ’32%, both matching the measured `wireUs`). The whole-board fps win is clean on the P4 (+58%); on the SE16 the driver gain is real but *masked* by ~50 ms of other per-tick overhead on that board (a heavy 128Γ—128 render), so its system fps barely moves β€” the driver-level proof stands, the board-level demo doesn't. A git-worktree baseline at the previous commit confirmed async-OFF reproduces the prior P4 behavior exactly (10,820 vs 10,787 Β΅s) β€” **provably no regression**. The design cut vs the original plan: (1) `tick()` is two explicit branches (`tickSync` = the literal original path, `tickAsync` = deferred-wait), so OFF is byte-for-byte the pre-change timing; (2) allocation follows the flag β€” OFF allocates ONE DMA buffer (costs nothing), ON requests the second (degrades to sync if it won't fit); (3) Parlio uses the **same whole-frame double-buffer as LCD**, not a ring (the KPI 4096-light frame fits one transfer β€” the ring stays Step 4, and per the "65K is a network problem" call is now deferred-indefinitely). Added a **`wireUs` read-only KPI** = the measured DMA wire time (start-of-transmit β†’ done-callback, an in-order completion FIFO pairs each done with its start), live "7474 Β΅s (133 fps max)" on the P4 β€” the pure output floor, so the fps ceiling is measured not assumed (and it tracks an overclocked slot rate directly). The remaining gap (system 76 fps β†’ wire 133 fps) is the render loop; the effect (~7.3 ms) is the next bottleneck β†’ Step 2 multicore. Full story + the measurement trap that nearly buried the win: [lessons.md](../../lessons.md). RMT stayed **deferred** (its shared per-pin symbol buffer isn't a small double-buffer delta). Everything below is the original plan as approved. ## Context -The measured fact (Parlio 16-lane sweep, [performance.md Β§ Multi-pin](../../performance.md#multi-pin-led-driving-all-three-peripherals-128128-grid); [multicore top-down Β§ Step 1.5](Plan-20260713%20-%20Multicore%20Step%202%20render-encode%20pipeline%20(shipped).md)): the driver `tick()` runs **encode β†’ transmit β†’ wait, serially, on one buffer**: +The measured fact (Parlio 16-lane sweep, [performance.md Β§ Multi-pin](../../../performance.md#multi-pin-led-driving-all-three-peripherals-128128-grid); [multicore top-down Β§ Step 1.5](Plan-20260713%20-%20Multicore%20Step%202%20render-encode%20pipeline%20(shipped).md)): the driver `tick()` runs **encode β†’ transmit β†’ wait, serially, on one buffer**: ``` tick() { // ParallelLedDriver.h:178 @@ -75,7 +75,7 @@ Ship as **one coherent change per peripheral seam + the shared base**, but built ### Increment 1 β€” LCD (S3) + Parlio (P4): whole-frame double-buffer (`platform_esp32_lcd.cpp`, `platform_esp32_parlio.cpp`) Both peripherals get the **same** whole-frame ping-pong (each already does one autonomous single-transfer today, so a second buffer + alternating is identical for both). `LcdState`/`ParlioState` grow `buf[2]` + `done[2]` (a binary semaphore per buffer β€” each transfer gives its own; the recognizable ping-pong handshake). - `busBuffer(i)` returns buffer i; `busTransmit(i, bytes)` transmits from buffer i and arms `done[i]`; `busWait(i, ms)` waits on `done[i]`. i80 (`esp_lcd_panel_io_tx_color`) and Parlio (`parlio_tx_unit_transmit`) both take the buffer pointer per transmit, so alternating is a pointer choice, not a bus rebuild. (Parlio's `trans_queue_depth` grows 1β†’2 so a second transfer can be queued while the first drains.) -- **Memory β€” TRY-ALLOC the second buffer, degrade to single if it won't fit (PO-confirmed, matches Step 2's memory rule, [top-down Β§ Step 2 design](Plan-20260713%20-%20Multicore%20Step%202%20render-encode%20pipeline%20(shipped).md)).** The second DMA buffer is *large* β€” the same size as the first (up to a whole 16-bit 16-lane frame), so on a memory-tight board it may not allocate. The seam **attempts** the second-buffer alloc (same PSRAM-first-else-internal path as the first) and checks the result: success β†’ double-buffer mode; **null β†’ single-buffer mode** (`buf[1] == nullptr`), base loop behaves exactly as today (wait-after-every-transmit). No `if constexpr (hasPsram)` gate β€” decide from the *actual* allocation outcome (a PSRAM board can still be too full; a no-PSRAM board can still fit two small frames). Per-board, per-config, at runtime: a tight board keeps the current fps rather than failing to init. The *Robust to any input* + adaptive-memory-degradation rule ([ADR 0002](../../adr/0002-adaptive-memory-degradation-cascade.md)): never *require* the second buffer. +- **Memory β€” TRY-ALLOC the second buffer, degrade to single if it won't fit (PO-confirmed, matches Step 2's memory rule, [top-down Β§ Step 2 design](Plan-20260713%20-%20Multicore%20Step%202%20render-encode%20pipeline%20(shipped).md)).** The second DMA buffer is *large* β€” the same size as the first (up to a whole 16-bit 16-lane frame), so on a memory-tight board it may not allocate. The seam **attempts** the second-buffer alloc (same PSRAM-first-else-internal path as the first) and checks the result: success β†’ double-buffer mode; **null β†’ single-buffer mode** (`buf[1] == nullptr`), base loop behaves exactly as today (wait-after-every-transmit). No `if constexpr (hasPsram)` gate β€” decide from the *actual* allocation outcome (a PSRAM board can still be too full; a no-PSRAM board can still fit two small frames). Per-board, per-config, at runtime: a tight board keeps the current fps rather than failing to init. The *Robust to any input* + adaptive-memory-degradation rule ([ADR 0002](../../../adr/0002-adaptive-memory-degradation-cascade.md)): never *require* the second buffer. - The contiguous-DMA ceiling is **per-buffer**, so double-buffering ~halves the max frame a board can hold; the degrade-to-single path covers the board that fits one but not two (keeps its old ceiling at the old fps). ### Increment 2 β€” RMT (RmtLedDriver.h / platform_esp32_rmt.cpp) β€” include only if a small delta diff --git a/docs/history/plans/archive/Plan-20260714 - Shift-register LED driver (shipped).md b/docs/history/plans/archive/Plan-20260714 - Shift-register LED driver (shipped).md index f1dd0915..c037401e 100644 --- a/docs/history/plans/archive/Plan-20260714 - Shift-register LED driver (shipped).md +++ b/docs/history/plans/archive/Plan-20260714 - Shift-register LED driver (shipped).md @@ -4,7 +4,7 @@ A **74HCT595 shift-register expander board** turns each physical data GPIO into **8 outputs**. The PO owns two S3-N16R8-driven panels β€” **15Γ—256** (3,840 lights) and **48Γ—256** (12,288 lights) β€” not yet wired. Goal: drive them from the drivers we already have, not a new driver class. -The feasibility research is [`docs/history/shift-register-driver-analysis.md`](../shift-register-driver-analysis.md). The two facts that shape this plan: +The feasibility research is [`docs/history/shift-register-driver-analysis.md`](../../shift-register-driver-analysis.md). The two facts that shape this plan: 1. **The Γ—8 fan-out costs 8Γ— the DMA frame** (~145 KB for *both* targets β€” extra strands ride the bus width and are free; the Γ—8 rides the serial shift and is not). Confirmed from hpwit's sizing expressions and his 8.00Γ— clock ratio. 2. **The 8Γ— bus clock is granted by `esp_lcd`** β€” bench-confirmed 2026-07-14 on S3 **and** P4: `request 20000000 Hz -> GRANTED (prescale 4 -> granted 20000000 Hz)`. diff --git a/docs/history/plans/archive/Plan-20260722 - Black pixels (dark gaps) in Layouts (shipped).md b/docs/history/plans/archive/Plan-20260722 - Black pixels (dark gaps) in Layouts (shipped).md index 9bd8d98c..303ac106 100644 --- a/docs/history/plans/archive/Plan-20260722 - Black pixels (dark gaps) in Layouts (shipped).md +++ b/docs/history/plans/archive/Plan-20260722 - Black pixels (dark gaps) in Layouts (shipped).md @@ -71,7 +71,7 @@ Remove the `g`/`+` gap syntax entirely, as its own commit/step so the diff reads - **[ParallelLedDriver.h](src/light/drivers/ParallelLedDriver.h):** remove `laneGaps_`, the gap semantics of `laneWire_`, the gap branch of `laneRowLit`, `laneNextBoundary`'s gap logic, the `laneGapCount`/`laneRowLitForTest`/`laneWire` test accessors, the `assignCounts(..., laneGaps_)` argument, the wire-length gap accumulation, and the `if (laneGaps_[i].n) return false` in `uniformLaneCounts`. Restore `laneRowLit` to the pre-gap `row < laneCounts_` test. - **[RmtLedDriver.h](src/light/drivers/RmtLedDriver.h) / [NetworkSendDriver.h](src/light/drivers/NetworkSendDriver.h):** confirm the `assignCounts` call sites compile after the trailing param drops. - **Tests:** remove the gap TEST_CASEs from [unit_RmtLedDriver_pins.cpp](test/unit/light/unit_RmtLedDriver_pins.cpp) and [unit_MultiPinLedDriver.cpp](test/unit/light/unit_MultiPinLedDriver.cpp). -- **Docs:** revert the `ledsPerPin` gap paragraph in [drivers.md](docs/moonmodules/light/drivers.md) and the backlog item edit in [backlog-light.md](docs/backlog/backlog-light.md). +- **Docs:** revert the `ledsPerPin` gap paragraph in [drivers.md](../../../moonmodules/light/drivers.md) and the backlog item edit in [backlog-light.md](../../../backlog/backlog-light.md). ## Tests (pin behavior) diff --git a/docs/history/plans/archive/Plan-20260722 - Release 4 scope - effect breadth + rename runway (shipped).md b/docs/history/plans/archive/Plan-20260722 - Release 4 scope - effect breadth + rename runway (shipped).md new file mode 100644 index 00000000..76a42a04 --- /dev/null +++ b/docs/history/plans/archive/Plan-20260722 - Release 4 scope - effect breadth + rename runway (shipped).md @@ -0,0 +1,105 @@ +# Plan β€” Release 4 scope: effect breadth + the rename runway + +## Status β€” SHIPPED 2026-09-07 (decomposed) + +**The headline SHIPPED and was overachieved; the driver work never started.** Verified against the +tree rather than inferred: this plan asked for Stage-1 primitives plus "the next effect batch" to +move the rename's breadth gate, and the library is now **66 compiled effects and 32 scripted** +against the ~21 this document counted. The gate it existed to serve is met; the remaining blocker +for the rename is DMX, not effects ([the migration plan's status](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md)). + +| item | state | +|---|---| +| Stage-1 primitives (palette, draw, FastLED-named set, tags) | **shipped** | +| The next effect batch | **shipped, overachieved**: 98 effects against a 21 baseline | +| `ActiveInstance` primitive | **shipped** (`src/core/ActiveInstance.h`, used by AudioService + DevicesModule) | +| CodeRabbit #29, the scenario finding | **shipped 2026-09-07**, see below | +| CodeRabbit #29, the other three | **open**, moved to backlog | +| RS-485 / DMX-512 driver | **not started**, and scoped OUT by the product owner 2026-09-07 | +| High-light-count driver work (4 items) | **not started**, moved to backlog | + +**SHIPPED as a release-scope plan.** Its headline landed and was overachieved, which is what this +document existed to do. What did not land was never work this plan owned: each item was a pointer +to something with its own home, and each is still there. + +- **DMX** is Stage 5 of the [MoonLight migration plan](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md), + which describes it in far more detail than this plan ever did, and where it belongs: it is the + transport half of the moving-head work whose effect half already shipped. +- **The high-light-count drivers** and the **CodeRabbit #29 findings** stay in the backlog. Neither + belongs to the migration plan, which is about porting MoonLight's library, not about lane drivers + or a core/platform boundary. + +Nothing is orphaned by closing this, which is the test for whether a plan can be closed at all. + +### The one thing fixed while decomposing + +The CodeRabbit #29 **scenario finding** is closed: `scenario_modifier_chain` routed a +modifier-composition test through `NetworkSendDriver`, pulling socket behavior into a test that is +not about the network. It now uses `PreviewDriver`, the in-process sink (the shape +`scenario_Audio_mutation` already used). + +Its `tick_us` half was NOT a defect and is left alone: a `measure` step asserts nothing, it RECORDS, +and that recording is what feeds the per-commit performance trend (CLAUDE.md, "scenarios record"). +Removing it would have blinded the trend to fix nothing. + +## Context + +Release 3 is being cut now. This plan captures the **Release 4** candidates β€” the next strategic thread after R3 β€” so the direction is recorded before the work starts. The product owner's steer: the items below are R4, not R3. + +The backlog has one dominant strategic thread that most other items orbit: the **projectMM β†’ MoonLight rename** ([backlog rename plan](../../../backlog/rename-to-moonlight.md)). Its gate is *"the effect library must not feel thin next to the predecessor's 60+ effects."* Two in-flight plans feed that gate, and R4 is where they land. The shape of R4 is therefore **"the effects release + the rename runway"**: grow visible feature breadth while moving the single most important strategic gate (rename readiness), and leave the hardware-verification-bound driver work to its own dedicated push. + +This is a roadmap/scope plan, not a single-feature `/plan`. Each item below gets its own `/plan` + commit when reached; this document is the *map* and the *why*. + +## The spine β€” effect-breadth parity (headline) + +**MoonLight migration, Stage 1 + the next effect batch.** ([Plan-20260630 - MoonLight migration (multi-stage)](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md).) + +This is the biggest lever and the explicit *"execution vehicle for the effect-breadth parity gate."* ~21 of the predecessor's 60+ effects are ported. Stage 1's prerequisites are the highest-value core work available, because every future effect leans on them: + +- **Shared palette** β€” hard prerequisite; many effects color via `ColorFromPalette`. Generalize the pattern `PlasmaPaletteEffect` hard-codes today. +- **The shared primitive library** β€” FastLED-named, our own implementation, hot-path-tuned integer-only: `beatsin8`, `inoise8`, `qadd8`, `nscale8`, `random8`/`random16`, `ColorFromPalette`, and the dimension-agnostic draw set. Extends the existing `color.h` (`scale8`, `sin8`). +- **Tag/emoji legend** β€” settle before batch-migrating so every module is consistent from batch one. +- **Per-library doc model** β€” `effects_.md` compact table rows (per [ADR 0015](../../../adr/0015-library-is-a-tag-not-a-folder.md)); changes the `check_specs.py` contract. + +Then the next migration batch on top. This is the R4 headline: it unblocks the rename *and* is pure user-visible feature growth. + +## Two quick wins β€” scoped and ready + +- **Active-instance election primitive.** ([Plan-20260710 - Active-instance election primitive](Plan-20260710%20-%20Active-instance%20election%20primitive%20%28shipped%29.md).) A core `ActiveInstance` that removes duplicated singleton-election bookkeeping from `AudioService` + `DevicesModule` (both had real dangling-static bugs). Textbook *Complexity-lives-in-core* subtraction; small; in flight. +- **CodeRabbit #29 boundary findings (4).** ([backlog-core Β§ MoonLive core/platform layering](../../../backlog/backlog-core.md#moonlive-coreplatform-layering-jit-sdkconfig-scoping-coderabbit-29-3-findings-left).) MoonLive core-includes-platform + compiled-into-`mm_core`, W^X disabled in the board default, a scenario riding timing + network. Real, already scoped; good hygiene to close before a named release. + +## What did not happen, and where it lives now + +Each of these was a POINTER to a backlog item rather than work this plan owned, and each is still +there under its own name. Listed here only so the decomposition is traceable; the backlog is the +one home for what they are and why. + +- **RS-485 / DMX-512 wired output** moved to the [MoonLight migration plan's Stage 5](../Plan-20260630%20-%20MoonLight%20migration%20%28multi-stage%29.md), + which is its real home: the moving-head EFFECTS shipped there, and this is their transport. + Scoped OUT by the product owner on 2026-09-07 ("out of scope for now, will do later"). +- **Classic-ESP32 shift-register ring on raw I2S**, **P4 Parlio streaming ring**, **shared + lane-driver scaffolding** ([backlog-light Β§ Drivers](../../../backlog/backlog-light.md#drivers)) and + the **MoonI80 prime-only ring stall backstop** + ([backlog-core](../../../backlog/backlog-core.md#mooni80-prime-only-ring-no-stall-backstop-sibling-path-gap)). + All four are hardware-verification-bound: each needs the expander wall and the relevant board, so + they land with bench sign-off or not at all. +- **CodeRabbit #29, three findings** + ([backlog-core](../../../backlog/backlog-core.md#moonlive-coreplatform-layering-jit-sdkconfig-scoping-coderabbit-29-3-findings-left)). + The fourth is closed, above. + +## Success shape (as written in July, and what became of it) + +> R4 ships when: the migration Stage-1 primitives + the next effect batch have landed (moving the +> rename's breadth gate forward), the `ActiveInstance` primitive and the CodeRabbit #29 boundary +> fixes are in, the RS-485/DMX driver reaches a verified first output, and the high-light-count +> driver work above is bench-verified. + +The first half happened and then some. The second half did not, and DMX is now deferred, so this +shape is unmeetable as stated: kept verbatim because a scope that was written down and then overtaken +is worth reading next to what actually shipped, not quietly rewritten to match the outcome. + +The lesson worth carrying, and the reason this document is decomposed rather than extended: it +bundled **effect work that needed only a keyboard** with **driver work that needs a wall of LEDs**. +The first raced ahead; the second never started, because it was gated on bench time rather than on +anything this plan could schedule. A release scope that mixes the two makes neither legible. Split +by what a task is BLOCKED ON, not by which release it is wanted for. diff --git a/docs/history/plans/Plan-20260728 - Doc-comment size reporting via clang-query.md b/docs/history/plans/archive/Plan-20260728 - Doc-comment size reporting via clang-query (shipped).md similarity index 99% rename from docs/history/plans/Plan-20260728 - Doc-comment size reporting via clang-query.md rename to docs/history/plans/archive/Plan-20260728 - Doc-comment size reporting via clang-query (shipped).md index 940176b7..0578a08e 100644 --- a/docs/history/plans/Plan-20260728 - Doc-comment size reporting via clang-query.md +++ b/docs/history/plans/archive/Plan-20260728 - Doc-comment size reporting via clang-query (shipped).md @@ -164,7 +164,7 @@ So the host now **emulates** the peripherals rather than declaring itself incapa buses with heap memory instead of returning `false`/`nullptr`. `ParallelLedDriver` runs on macOS against all three real backends, switchable live. -Recorded as a hard rule in [architecture.md Β§ Platform abstraction](../../architecture.md). Its +Recorded as a hard rule in [architecture.md Β§ Platform abstraction](../../../architecture.md). Its limit is deliberate: timing, wire protocol and pin state are NOT emulated, because faking them would let a self-test report on hardware it never touched. diff --git "a/docs/history/plans/archive/Plan-20260813 - MoonLive on a stack machine \342\200\224 the frame is where values live (shipped).md" "b/docs/history/plans/archive/Plan-20260813 - MoonLive on a stack machine \342\200\224 the frame is where values live (shipped).md" index 4aaf5651..5d4f16bf 100644 --- "a/docs/history/plans/archive/Plan-20260813 - MoonLive on a stack machine \342\200\224 the frame is where values live (shipped).md" +++ "b/docs/history/plans/archive/Plan-20260813 - MoonLive on a stack machine \342\200\224 the frame is where values live (shipped).md" @@ -137,7 +137,7 @@ this is removing a distinction the storage layer never made β€” not introducing still decides is which slots it WRITES each frame; reading is uniform. This is a breaking change for any script using a modifier's `x`/`y`/`z`, so it needs its -[MIGRATING.md](../../MIGRATING.md) entry and a sweep of the shipped `moonlive/` scripts. +[MIGRATING.md](../../../MIGRATING.md) entry and a sweep of the shipped `moonlive/` scripts. ### Clean first, with speed decisions made deliberately @@ -435,7 +435,7 @@ window-overflow handler spills a frame's a4..a7 into the frame's OWN top 32 byte reserved 16, so the parked arena pointer of step 3b sat in hardware-owned memory and any interrupt during a host call destroyed it. Frame LAYOUT, not register choice; spatial, not temporal; and invisible to every encoding check because each instruction was correct. See -[lessons Β§ the register-window frame bug](../lessons.md#lessons-from-the-moonlive-on-xtensa-branch-the-register-window-frame-bug). +[lessons Β§ the register-window frame bug](../../lessons.md#lessons-from-the-moonlive-on-xtensa-branch-the-register-window-frame-bug). All four boards (S3, classic, P4, S31) now run scripted layouts and effects. ## Status: CLOSED diff --git a/docs/history/plans/archive/Plan-20260817 - MoonLive scripts are classes (shipped).md b/docs/history/plans/archive/Plan-20260817 - MoonLive scripts are classes (shipped).md index a93ceb97..9dc65ea8 100644 --- a/docs/history/plans/archive/Plan-20260817 - MoonLive scripts are classes (shipped).md +++ b/docs/history/plans/archive/Plan-20260817 - MoonLive scripts are classes (shipped).md @@ -750,7 +750,7 @@ therefore needs a host test that proves the semantics and a bench run that prove The P4 is up and holds its scripts, but it panics with `Cache error` every few minutes while idle. Established as PRE-EXISTING rather than a regression: it runs the default module tree with no MoonLive module at all, and a firmware built from a clean `main` crashes identically. Recorded - in [backlog-core](../../backlog/backlog-core.md). A SEPARATE P4 boot loop found in the same + in [backlog-core](../../../backlog/backlog-core.md). A SEPARATE P4 boot loop found in the same session WAS this branch's regression and is fixed: the engine had grown to 1440 bytes held by value in every scripted module, which `registerType`'s stack probe could not absorb. @@ -765,7 +765,7 @@ therefore needs a host test that proves the semantics and a bench run that prove the compiler. - **`while`, `break`, `continue`.** `for` and `if` cover what an effect does; the rest is language completeness rather than expressiveness, and each one costs a grammar rule and a test surface. -- **Floating point.** The render path is integer by rule ([coding-standards](../../coding-standards.md)), +- **Floating point.** The render path is integer by rule ([coding-standards](../../../coding-standards.md)), and the Xtensa classic has no FPU, so a float in a script would be a silent softfloat call per light. - **A scripted DRIVER as the fourth role.** It is the honest test of step 5's dispatch, but it needs the driver surface to be as settled as the other three are, and that is its own question. diff --git a/docs/history/plans/Plan-20260823 - A Windows installer, and settings that persist.md b/docs/history/plans/archive/Plan-20260823 - A Windows installer, and settings that persist (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260823 - A Windows installer, and settings that persist.md rename to docs/history/plans/archive/Plan-20260823 - A Windows installer, and settings that persist (shipped).md diff --git a/docs/history/plans/Plan-20260823 - Five types for MoonLive scripts.md b/docs/history/plans/archive/Plan-20260823 - Five types for MoonLive scripts (shipped).md similarity index 98% rename from docs/history/plans/Plan-20260823 - Five types for MoonLive scripts.md rename to docs/history/plans/archive/Plan-20260823 - Five types for MoonLive scripts (shipped).md index 0e2c0155..2f9a8d9e 100644 --- a/docs/history/plans/Plan-20260823 - Five types for MoonLive scripts.md +++ b/docs/history/plans/archive/Plan-20260823 - Five types for MoonLive scripts (shipped).md @@ -13,7 +13,7 @@ renders wrong" and never as an error: Not one was a mistake in a script. Each was a script author choosing a storage width and the engine silently disagreeing. The product owner and the agent settled the replacement in -[moonlive-language-roadmap.md](../../backlog/moonlive-language-roadmap.md): **five types β€” `int`, +[moonlive-language-roadmap.md](../../../backlog/moonlive-language-roadmap.md): **five types β€” `int`, `byte`, `bool`, `fixed`, `string` β€” each usable as scalar or array. Every scalar occupies one uniform 4-byte slot; arrays pack by element.** A type becomes a semantic rather than a width, which deletes the machinery instead of patching it a fifth time. diff --git a/docs/history/plans/archive/Plan-20260824 - NDI output (shipped).md b/docs/history/plans/archive/Plan-20260824 - NDI output (shipped).md index 996ae90e..e675dd71 100644 --- a/docs/history/plans/archive/Plan-20260824 - NDI output (shipped).md +++ b/docs/history/plans/archive/Plan-20260824 - NDI output (shipped).md @@ -10,7 +10,7 @@ an NDI source and reach a Spout pipeline through one hop. Input is not the gap: `NetworkReceiveEffect` already binds Art-Net, E1.31/sACN and DDP at once. What is missing is the other direction β€” projectMM's pixels reaching a production visuals rig. -Decision recorded in [backlog-light Β§ Integration with other LED and visuals tools](../../backlog/backlog-light.md): +Decision recorded in [backlog-light Β§ Integration with other LED and visuals tools](../../../backlog/backlog-light.md): **NDI first.** One implementation covers Windows, macOS, Linux and ARM, it discovers by name, and it crosses machines. Spout (Windows) and Syphon (macOS) are lower latency and bit-exact but are same-machine only, are two platform implementations, and leave Linux and the Pi with nothing. At diff --git a/docs/history/plans/Plan-20260825 - Client-driven preview adaptation (superseded).md b/docs/history/plans/archive/Plan-20260825 - Client-driven preview adaptation (superseded).md similarity index 100% rename from docs/history/plans/Plan-20260825 - Client-driven preview adaptation (superseded).md rename to docs/history/plans/archive/Plan-20260825 - Client-driven preview adaptation (superseded).md diff --git a/docs/history/plans/Plan-20260826 - Desktop audio capture.md b/docs/history/plans/archive/Plan-20260826 - Desktop audio capture (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260826 - Desktop audio capture.md rename to docs/history/plans/archive/Plan-20260826 - Desktop audio capture (shipped).md diff --git a/docs/history/plans/Plan-20260826 - MoonBase, a second boot image (shipped).md b/docs/history/plans/archive/Plan-20260826 - MoonBase, a second boot image (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260826 - MoonBase, a second boot image (shipped).md rename to docs/history/plans/archive/Plan-20260826 - MoonBase, a second boot image (shipped).md diff --git a/docs/history/plans/Plan-20260827 - HLS on ESP32-P4.md b/docs/history/plans/archive/Plan-20260827 - HLS on ESP32-P4 (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260827 - HLS on ESP32-P4.md rename to docs/history/plans/archive/Plan-20260827 - HLS on ESP32-P4 (shipped).md diff --git a/docs/history/plans/Plan-20260827 - HLS streaming driver.md b/docs/history/plans/archive/Plan-20260827 - HLS streaming driver (shipped).md similarity index 98% rename from docs/history/plans/Plan-20260827 - HLS streaming driver.md rename to docs/history/plans/archive/Plan-20260827 - HLS streaming driver (shipped).md index 172cd412..407b6acf 100644 --- a/docs/history/plans/Plan-20260827 - HLS streaming driver.md +++ b/docs/history/plans/archive/Plan-20260827 - HLS streaming driver (shipped).md @@ -3,7 +3,7 @@ ## Context The PO wants to watch effects pixel-exact (1:1, no scaling) on a TV, up to 4K transport. Spec: -[docs/backlog/hls-driver-spec.md](../../backlog/hls-driver-spec.md). Decisions already made with +`docs/backlog/hls-driver-spec.md` (deleted when the driver shipped, per the backlog's drain rule). Decisions already made with the PO: pipe raw frames to a spawned **ffmpeg** (runtime dependency like Npcap/NDI, never vendored; one implementation for every desktop OS + Pi), H.264 + HLS served by our own HTTP server, 2-5 s live-tuned latency, ESP32 out of scope. Rides along: GridLayout `width`/`height` diff --git a/docs/history/plans/Plan-20260827 - Raw-L2 interface dropdown.md b/docs/history/plans/archive/Plan-20260827 - Raw-L2 interface dropdown (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260827 - Raw-L2 interface dropdown.md rename to docs/history/plans/archive/Plan-20260827 - Raw-L2 interface dropdown (shipped).md diff --git a/docs/history/plans/Plan-20260827 - Sprites and flying toasters.md b/docs/history/plans/archive/Plan-20260827 - Sprites and flying toasters (shipped).md similarity index 98% rename from docs/history/plans/Plan-20260827 - Sprites and flying toasters.md rename to docs/history/plans/archive/Plan-20260827 - Sprites and flying toasters (shipped).md index 2d424ca7..e81f54ba 100644 --- a/docs/history/plans/Plan-20260827 - Sprites and flying toasters.md +++ b/docs/history/plans/archive/Plan-20260827 - Sprites and flying toasters (shipped).md @@ -3,7 +3,7 @@ Classic screensavers on a light wall (a Discord request): sprites, small movable bitmaps with transparency, and the first consumer, After Dark's flying toasters. The power-functions catalog anticipated this: compositing was deferred "until sprites arrive" -([bottom-up](power-functions-analysis-bottom-up.md) Β§ below-the-cut); this is the arrival. +([bottom-up](../../../backlog/power-functions-analysis-bottom-up.md) Β§ below-the-cut); this is the arrival. ## Division of labor (the design decision) diff --git a/docs/history/plans/Plan-20260830 - Ship the MoonLive script library.md b/docs/history/plans/archive/Plan-20260830 - Ship the MoonLive script library (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260830 - Ship the MoonLive script library.md rename to docs/history/plans/archive/Plan-20260830 - Ship the MoonLive script library (shipped).md diff --git a/docs/history/plans/Plan-20260831 - Scripts declare dimensions and tags.md b/docs/history/plans/archive/Plan-20260831 - Scripts declare dimensions and tags (shipped).md similarity index 100% rename from docs/history/plans/Plan-20260831 - Scripts declare dimensions and tags.md rename to docs/history/plans/archive/Plan-20260831 - Scripts declare dimensions and tags (shipped).md diff --git a/docs/metrics/repo-health.json b/docs/metrics/repo-health.json index 5b41349e..8fc21bd1 100644 --- a/docs/metrics/repo-health.json +++ b/docs/metrics/repo-health.json @@ -1,45 +1,47 @@ { - "commit": "7f46574a", + "commit": "52e03cbe", "flash": { - "esp32s3-n16r8": 2065376, - "desktop": 1855496, - "esp32": 2023776, - "esp32p4rev1-eth": 1955408, + "esp32s3-n16r8": 2100032, + "desktop": 1909016, + "esp32": 2059664, + "esp32p4rev1-eth": 1979232, "esp32p4rev1-eth-wifi": 2019392, - "esp32s3-n8r8": 1971008, + "esp32s3-n8r8": 2087168, "esp32s31": 2348592, "esp32-16mb": 1809472, "esp32-eth": 1397456, "esp32-wrover": 1843760, "qemu": 1383648, "esp32p4rev3-eth": 1643760, - "esp32s3-zero": 1788624, + "esp32s3-zero": 2024192, "esp32-pico": 2071584 }, "measured": { - "esp32p4rev1-eth": "2026-09-06", + "esp32p4rev1-eth": "2026-09-08", "esp32s31": "2026-09-06", - "esp32": "2026-09-06", + "esp32": "2026-09-08", "esp32-pico": "2026-09-06", - "esp32s3-n16r8": "2026-09-06", - "desktop": "2026-09-06" + "esp32s3-n16r8": "2026-09-08", + "desktop": "2026-09-08", + "esp32s3-n8r8": "2026-09-08", + "esp32s3-zero": "2026-09-08" }, "perf": { "desktop": { - "tick_us": 137, - "fps": 7299, + "tick_us": 140, + "fps": 7142, "scenario_p50": { "Layer_base_pipeline": { "p50": 71, - "p95": 179, + "p95": 197, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "Layer_memory_1to1": { "p50": 5, "p95": 40, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" } } }, @@ -50,10 +52,10 @@ "scenario_matrix": { "MoonModule_control_change": { "desktop-macos": { - "p50": 144, + "p50": 143, "p95": 248, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32-eth-wifi": { "p50": 89895, @@ -169,7 +171,7 @@ "p50": 26, "p95": 70, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 40, @@ -192,18 +194,18 @@ }, "Aurora_fps": { "desktop-macos": { - "p50": 1514, + "p50": 1518, "p95": 1828, - "n": 23, - "last": "2026-09-06" + "n": 26, + "last": "2026-09-07" } }, "Driver_mutation": { "desktop-macos": { - "p50": 21, - "p95": 64, + "p50": 20, + "p95": 88, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 42, @@ -226,10 +228,10 @@ }, "Effects_composition": { "desktop-macos": { - "p50": 169, + "p50": 148, "p95": 768, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 549, @@ -240,26 +242,26 @@ }, "Fields_polar_lut": { "desktop-macos": { - "p50": 1234, - "p95": 1402, - "n": 24, - "last": "2026-09-06" + "p50": 1239, + "p95": 1686, + "n": 27, + "last": "2026-09-07" } }, "Fluid_solver": { "desktop-macos": { - "p50": 220, - "p95": 249, - "n": 17, - "last": "2026-09-06" + "p50": 222, + "p95": 259, + "n": 20, + "last": "2026-09-07" } }, "GridBlacks_blackpixel": { "desktop-macos": { "p50": 2, - "p95": 12, + "p95": 16, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32s3-n16r8": { "p50": 267, @@ -285,7 +287,7 @@ "p50": 127, "p95": 311, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32-eth-wifi": { "p50": 82231, @@ -327,9 +329,9 @@ "Layer_base_pipeline": { "desktop-macos": { "p50": 71, - "p95": 179, + "p95": 197, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 118, @@ -343,7 +345,7 @@ "p50": 5, "p95": 40, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 1, @@ -354,10 +356,10 @@ }, "Layouts_mutation": { "desktop-macos": { - "p50": 96, + "p50": 97, "p95": 248, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 111, @@ -407,9 +409,9 @@ "MoonLiveEffect_livescript": { "desktop-macos": { "p50": 6, - "p95": 22, - "n": 32, - "last": "2026-09-06" + "p95": 8, + "n": 16, + "last": "2026-09-08" }, "esp32s3-n16r8": { "p50": 8255, @@ -459,7 +461,7 @@ "p50": 6, "p95": 21, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 1, @@ -471,9 +473,9 @@ "MultiplyModifier_memory_lut": { "desktop-macos": { "p50": 3, - "p95": 21, + "p95": 19, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 3, @@ -487,7 +489,7 @@ "p50": 126, "p95": 283, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 225, @@ -498,18 +500,18 @@ }, "Trails_ladder": { "desktop-macos": { - "p50": 355, - "p95": 608, - "n": 18, - "last": "2026-09-06" + "p50": 360, + "p95": 442, + "n": 21, + "last": "2026-09-07" } }, "modifier_chain": { "desktop-macos": { - "p50": 47, - "p95": 123, + "p50": 44, + "p95": 112, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 69, @@ -529,7 +531,7 @@ "p50": 24, "p95": 87, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32-eth": { "p50": 1010, @@ -567,7 +569,7 @@ "p50": 279, "p95": 1114, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32s3-n16r8": { "p50": 16915, @@ -599,7 +601,7 @@ "p50": 17, "p95": 61, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32s3-n16r8": { "p50": 2485, @@ -640,10 +642,10 @@ "last": "2026-07-25" }, "desktop-macos": { - "p50": 308, + "p50": 296, "p95": 881, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "desktop-windows": { "p50": 649, @@ -667,9 +669,9 @@ }, "desktop-macos": { "p50": 4, - "p95": 16, + "p95": 14, "n": 32, - "last": "2026-09-06" + "last": "2026-09-07" }, "esp32p4rev1-eth": { "p50": 217, @@ -693,54 +695,54 @@ } }, "loc": { - "core": 25516, - "light": 34615, - "platform": 18036, - "ui": 10204, - "test": 56361, - "moondeck": 22570 + "core": 26039, + "light": 35627, + "platform": 18503, + "ui": 10621, + "test": 57455, + "moondeck": 22710 }, "comments": { "core": { - "lines": 10242, - "ratio": 0.434 + "lines": 10452, + "ratio": 0.433 }, "light": { - "lines": 13197, - "ratio": 0.419 + "lines": 13532, + "ratio": 0.417 }, "platform": { - "lines": 6336, + "lines": 6500, "ratio": 0.385 }, "ui": { - "lines": 3014, - "ratio": 0.312 + "lines": 3164, + "ratio": 0.314 }, "test": { - "lines": 10536, - "ratio": 0.214 + "lines": 10784, + "ratio": 0.215 }, "moondeck": { - "lines": 3673, + "lines": 3690, "ratio": 0.186 } }, "tests": { - "cases": 1955, + "cases": 1992, "scenarios": 27 }, "docs": { - "md_files": 219, - "md_lines": 36362, - "plans_files": 115, - "backlog_lines": 6747, + "md_files": 220, + "md_lines": 36832, + "plans_files": 116, + "backlog_lines": 6794, "lessons_lines": 705, "claude_md_lines": 259 }, "complexity": { - "functions": 3441, - "over_threshold": 239, + "functions": 3498, + "over_threshold": 249, "worst_ccn": 108 } } diff --git a/docs/metrics/repo-health.md b/docs/metrics/repo-health.md index 183df585..07314a18 100644 --- a/docs/metrics/repo-health.md +++ b/docs/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `7f46574a`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `52e03cbe`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** Current state only; the trend is this file's git history (`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. @@ -8,19 +8,19 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Flash | Capacity | Used | Built | |---|---:|---:|---:|:--:| -| desktop | 1,812 KB (+0 KB) ⚠ | - | - | yes | -| esp32 | 1,976 KB (+2 KB) ⚠ | 2,496 KB | 79% | yes | +| desktop | 1,864 KB | - | - | yes | +| esp32 | 2,011 KB (+3 KB) ⚠ | 2,496 KB | 81% | yes | | esp32-16mb | 1,767 KB | - | - | carried (age?) | | esp32-eth | 1,365 KB | - | - | carried (age?) | -| esp32-pico | 2,023 KB (+2 KB) ⚠ | 3,072 KB | 66% | yes | +| esp32-pico | 2,023 KB | 3,072 KB | 66% | carried 2d | | esp32-wrover | 1,801 KB | - | - | carried (age?) | -| esp32p4rev1-eth | 1,910 KB | 4,096 KB | 47% | yes | +| esp32p4rev1-eth | 1,933 KB | 4,096 KB | 47% | yes | | esp32p4rev1-eth-wifi | 1,972 KB | - | - | carried (age?) | | esp32p4rev3-eth | 1,605 KB | - | - | carried (age?) | -| esp32s3-n16r8 | 2,017 KB | 4,096 KB | 49% | yes | -| esp32s3-n8r8 | 1,925 KB | - | - | carried (age?) | -| esp32s3-zero | 1,747 KB | - | - | carried (age?) | -| esp32s31 | 2,294 KB | 4,096 KB | 56% | yes | +| esp32s3-n16r8 | 2,051 KB | 4,096 KB | 50% | yes | +| esp32s3-n8r8 | 2,038 KB | 3,072 KB | 66% | yes | +| esp32s3-zero | 1,977 KB | 2,496 KB | 79% | yes | +| esp32s31 | 2,294 KB | 4,096 KB | 56% | carried 2d | | qemu | 1,351 KB | - | - | carried (age?) | `Built: yes` was measured this run. `carried (age?)` was not rebuilt either and predates this record, so its age is unknown: it dates itself on the next build. `carried Nd` was NOT rebuilt and its number is N days old, so an absent delta says nothing about the change. **STALE** marks a carry older than 7 days: the number has gone unchecked long enough that growth will surface later as one jump, blamed on whichever commit happens to rebuild that target. `Used` is against the app slot in the firmware's own partition table. @@ -29,40 +29,40 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Tick | FPS | |---|---:|---:| -| desktop | 137 Β΅s (βˆ’17 Β΅s) βœ“ | 7,299 (+806) βœ“ | +| desktop | 140 Β΅s (βˆ’489 Β΅s) βœ“ | 7,142 (+5,553) βœ“ | | esp32 | 8,354 Β΅s | 119 | ### Scenario tick by target (p50 of each sample window) | Scenario | desktop-macos | desktop-windows | esp32 | esp32s3-n16r8 | esp32p4rev1-eth | esp32s31 | esp32-eth | esp32-eth-wifi | unknown | |---|---|---|---|---|---|---|---|---|---| -| Audio_mutation | 26 (βˆ’2) βœ“ | 40 ? | 13,152 | 47 ? | - | - | - | - | - | -| Aurora_fps | 1,514 (+8) ⚠ | - | - | - | - | - | - | - | - | -| Driver_mutation | 21 (βˆ’11) βœ“ | 42 ? | 12,812 | 39 ? | - | - | - | - | - | -| Effects_composition | 169 (βˆ’197) βœ“ | 549 ? | - | - | - | - | - | - | - | -| Fields_polar_lut | 1,234 (+6) ⚠ | - | - | - | - | - | - | - | - | -| Fluid_solver | 220 (βˆ’2) βœ“ | - | - | - | - | - | - | - | - | -| GridBlacks_blackpixel | 2 (βˆ’3) βœ“ | 8 ? | 269 ? | 267 ? | - | - | - | - | - | -| GridLayout_resize | 127 (βˆ’1) βœ“ | 219 ? | 1,352 ? | 1,011 ? | 1,143 ? | - | 95,771 ? | 82,231 ? | - | -| Layer_base_pipeline | 71 (βˆ’23) βœ“ | 118 ? | - | - | - | - | - | - | - | -| Layer_memory_1to1 | 5 (βˆ’4) βœ“ | 1 ? | - | - | - | - | - | - | - | -| Layouts_mutation | 96 (βˆ’42) βœ“ | 111 ? | 13,692 | 45 ? | - | - | 27 ? | - | - | +| Audio_mutation | 26 | 40 ? | 13,152 | 47 ? | - | - | - | - | - | +| Aurora_fps | 1,518 | - | - | - | - | - | - | - | - | +| Driver_mutation | 20 | 42 ? | 12,812 | 39 ? | - | - | - | - | - | +| Effects_composition | 148 | 549 ? | - | - | - | - | - | - | - | +| Fields_polar_lut | 1,239 | - | - | - | - | - | - | - | - | +| Fluid_solver | 222 | - | - | - | - | - | - | - | - | +| GridBlacks_blackpixel | 2 | 8 ? | 269 ? | 267 ? | - | - | - | - | - | +| GridLayout_resize | 127 | 219 ? | 1,352 ? | 1,011 ? | 1,143 ? | - | 95,771 ? | 82,231 ? | - | +| Layer_base_pipeline | 71 | 118 ? | - | - | - | - | - | - | - | +| Layer_memory_1to1 | 5 | 1 ? | - | - | - | - | - | - | - | +| Layouts_mutation | 97 | 111 ? | 13,692 | 45 ? | - | - | 27 ? | - | - | | MoonLiveEffect_controls | 11 ? | - | 12,901 | 4,624 ? | - | - | - | - | - | -| MoonLiveEffect_livescript | 6 (βˆ’3) βœ“ | - | 13,433 ? | 8,255 ? | 11,336 ? | - | - | - | - | -| MoonLive_pipeline | 6 (βˆ’4) βœ“ | 1 ? | 9,604 ? | 3,278 ? | - | 11,398 ? | - | - | 4,393 ? | -| MoonModule_control_change | 144 (+1) ⚠ | 262 ? | 212 ? | 166 ? | 165 ? | - | 111,731 ? | 89,895 ? | - | +| MoonLiveEffect_livescript | 6 | - | 13,433 ? | 8,255 ? | 11,336 ? | - | - | - | - | +| MoonLive_pipeline | 6 | 1 ? | 9,604 ? | 3,278 ? | - | 11,398 ? | - | - | 4,393 ? | +| MoonModule_control_change | 143 | 262 ? | 212 ? | 166 ? | 165 ? | - | 111,731 ? | 89,895 ? | - | | MqttModule_haDiscovery_toggle | 3 ? | - | 36 ? | 36 ? | - | - | - | - | - | -| MultiplyModifier_memory_lut | 3 (βˆ’1) βœ“ | 3 ? | - | - | - | - | - | - | - | +| MultiplyModifier_memory_lut | 3 | 3 ? | - | - | - | - | - | - | - | | MultiplyModifier_pipeline | 126 | 225 ? | - | - | - | - | - | - | - | | NetworkModule_eth_reconfigure | - | - | 1,169 ? | 97,843 ? | - | - | - | - | - | | NetworkModule_mdns_toggle | 13 ? | - | 36 ? | 36 ? | 21 ? | - | 109,767 ? | 93,963 ? | - | -| Trails_ladder | 355 (+2) ⚠ | - | - | - | - | - | - | - | - | -| modifier_chain | 47 (βˆ’8) βœ“ | 69 ? | 13,337 | - | - | - | - | - | - | -| modifier_swap | 24 (βˆ’9) βœ“ | 41 ? | 12,250 | 354 ? | 362 ? | - | 1,010 ? | - | - | -| perf_full | 279 (βˆ’150) βœ“ | 592 ? | 10,392 | 16,915 ? | 17,433 ? | - | - | - | - | -| perf_light | 17 (βˆ’10) βœ“ | 35 ? | 2,183 | 2,485 ? | 2,038 ? | - | - | - | - | -| peripheral_grid_sweep | 308 (βˆ’177) βœ“ | 649 ? | 6,991 ? | - | 11,495 ? | 12,273 ? | - | - | - | -| peripheral_switch | 4 (βˆ’3) βœ“ | 9 ? | 437 | 46 ? | 217 ? | - | - | - | - | +| Trails_ladder | 360 | - | - | - | - | - | - | - | - | +| modifier_chain | 44 | 69 ? | 13,337 | - | - | - | - | - | - | +| modifier_swap | 24 | 41 ? | 12,250 | 354 ? | 362 ? | - | 1,010 ? | - | - | +| perf_full | 279 | 592 ? | 10,392 | 16,915 ? | 17,433 ? | - | - | - | - | +| perf_light | 17 | 35 ? | 2,183 | 2,485 ? | 2,038 ? | - | - | - | - | +| peripheral_grid_sweep | 296 | 649 ? | 6,991 ? | - | 11,495 ? | 12,273 ? | - | - | - | +| peripheral_switch | 4 | 9 ? | 437 | 46 ? | 217 ? | - | - | - | - | Microseconds. `?` marks a cell backed by fewer than 4 samples, which is a first impression rather than a percentile; several are months old and were captured during a network reconfigure, so they read as whole milliseconds. `-` means that target has never run that scenario. @@ -72,8 +72,8 @@ Microseconds. `?` marks a cell backed by fewer than 4 samples, which is a first | Scenario | p50 | p95 | n | |---|---:|---:|---:| -| Layer_base_pipeline | 71 Β΅s (βˆ’23 Β΅s) βœ“ | 179 Β΅s | 32 | -| Layer_memory_1to1 | 5 Β΅s (βˆ’4 Β΅s) βœ“ | 40 Β΅s | 32 | +| Layer_base_pipeline | 71 Β΅s | 197 Β΅s | 32 | +| Layer_memory_1to1 | 5 Β΅s | 40 Β΅s | 32 | These build a bare pipeline with no optional modules, so a change here is a change in the pipeline itself rather than in what was measured. A new module belongs in an advanced scenario, which keeps its own numbers. @@ -81,36 +81,36 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 25,516 (+30) ⚠ | 10,242 | 43.4 % (+0.1 %) ⚠ | -| light | 34,615 (+57) ⚠ | 13,197 | 41.9 % | -| platform | 18,036 (+173) ⚠ | 6,336 | 38.5 % | -| ui | 10,204 | 3,014 | 31.2 % | -| test | 56,361 (+206) ⚠ | 10,536 | 21.4 % | -| moondeck | 22,570 (+7) ⚠ | 3,673 | 18.6 % | +| core | 26,039 (+20) ⚠ | 10,452 | 43.3 % | +| light | 35,627 (+1) ⚠ | 13,532 | 41.7 % | +| platform | 18,503 (+25) ⚠ | 6,500 | 38.5 % (+0.1 %) ⚠ | +| ui | 10,621 (+40) ⚠ | 3,164 | 31.4 % (+0.1 %) ⚠ | +| test | 57,455 (+53) ⚠ | 10,784 | 21.5 % | +| moondeck | 22,710 (+9) ⚠ | 3,690 | 18.6 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 1,955 (+7) βœ“ | +| unit cases | 1,992 (+2) βœ“ | | scenarios | 27 | ## Complexity | Metric | Value | |---|---:| -| functions | 3,441 (+12) βœ“ | -| over threshold | 239 (+1) ⚠ | +| functions | 3,498 (+1) βœ“ | +| over threshold | 249 | | worst CCN | 108 | ## Documentation | Metric | Value | |---|---:| -| markdown files | 219 | -| markdown lines | 36,362 (βˆ’207) βœ“ | -| plan files | 115 | -| backlog lines | 6,747 (βˆ’232) βœ“ | -| lessons lines | 705 (+27) ⚠ | +| markdown files | 220 (+1) ⚠ | +| markdown lines | 36,832 (+1,940) ⚠ | +| plan files | 116 (+1) ⚠ | +| backlog lines | 6,794 | +| lessons lines | 705 | | CLAUDE.md lines | 259 | diff --git a/docs/moonmodules/core/system.md b/docs/moonmodules/core/system.md index c96638d7..d15f53e5 100644 --- a/docs/moonmodules/core/system.md +++ b/docs/moonmodules/core/system.md @@ -103,11 +103,12 @@ Over-the-air firmware flashing β€” the one operation that swaps the binary and n Firmware update module controls - `firmware` β€” the OTA image to flash. -- read-only: `version`, `build`, `firmwarePartition`, `update_pct` (progress; absent on MoonBase - devices, where the update overlay carries the progress instead), and on 4 MB - boards `moonbase`: the second boot image is present, so installs run through the - reboot-into-MoonBase cycle behind one "updating firmware" overlay, and a **MoonBase** button - opens the maintenance image directly ([architecture.md Β§ MoonBase](../../architecture.md#moonbase-the-second-boot-image)). +- read-only: `version`, `build`, `partition`. Where a device carries two images, `image` selects + which one those describe and which one an install writes: the app it runs, or MoonBase in the + factory slot. Its presence is also what tells the UI that installs run through the + reboot-into-MoonBase cycle, behind one "updating firmware" overlay, and that a **Restart in + MoonBase** button belongs on the card + ([architecture.md Β§ MoonBase](../../architecture.md#moonbase-the-second-boot-image)). Detail: [technical](moxygen/FirmwareUpdateModule.md) diff --git a/docs/moonmodules/light/MoonLiveLayout.md b/docs/moonmodules/light/MoonLiveLayout.md index 51051490..ecf4b2c1 100644 --- a/docs/moonmodules/light/MoonLiveLayout.md +++ b/docs/moonmodules/light/MoonLiveLayout.md @@ -65,7 +65,7 @@ A script reads whatever it declares. `byte cols = 16;` is a member the script ow ### Seeing inside a script `print(v)` logs a value and returns it, so it wraps any part of an expression: `addLight(print(x), y, 0)`. -It is for debugging and comes back out again β€” [what print costs](writing-scripts.md#debugging-print). +It is for debugging and comes back out again: [what print costs](https://github.com/MoonModules/projectMM/blob/main/moonlive/README.md#debugging-print). ## How the count is known diff --git a/docs/moonmodules/light/MoonLiveModifier.md b/docs/moonmodules/light/MoonLiveModifier.md index 824b8133..6dd2b794 100644 --- a/docs/moonmodules/light/MoonLiveModifier.md +++ b/docs/moonmodules/light/MoonLiveModifier.md @@ -37,7 +37,7 @@ setXYZ((width - 1 - xPos) * 2, yPos, zPos); // mirror, then stretch ### Seeing inside a script `print(v)` logs a value and returns it, so it wraps any part of an expression: `setXYZ(print(width - 1 - xPos), yPos, zPos)`. -It is for debugging and comes back out again β€” [what print costs](writing-scripts.md#debugging-print). +It is for debugging and comes back out again: [what print costs](https://github.com/MoonModules/projectMM/blob/main/moonlive/README.md#debugging-print). ## Limits diff --git a/docs/moonmodules/light/effects.md b/docs/moonmodules/light/effects.md index a0a4c500..1c9c3f56 100644 --- a/docs/moonmodules/light/effects.md +++ b/docs/moonmodules/light/effects.md @@ -29,7 +29,7 @@ Three emitters feed it: circles on an orbit, a Lissajous point tracing a figure Compare with [Fluid](#fluid): that one solves for pressure and gets vortices forming out of the flow's own history, at roughly twenty passes over the grid against this one's one. Reach for the solver when the medium is the subject, and for this when the subject is the color being carried. -Origin: MoonLight Β· concept by [Stefan Petrick](https://github.com/StefanPetrick), composition by Jeff (mindful_stone / [4wheeljive](https://github.com/4wheeljive)) in [AuroraPortal](https://github.com/4wheeljive/AuroraPortal/blob/main/src/programs/colorTrails_detail.hpp) Β· via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_FastLED.h) +Origin: MoonLight Β· concept by [Stefan Petrick](https://github.com/StefanPetrick), composition by Jeff (mindful_stone / [4wheeljive](https://github.com/4wheeljive)) in [FlowFields](https://github.com/4wheeljive/FlowFields/blob/main/src/flows/flow_noise.h) Β· via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_FastLED.h) @@ -315,7 +315,7 @@ An aquarium on a light wall: fish of three shapes swim across a dark tank, each - `school`: how many tiny schooling fish (0-8). - `speed`: swim rate in body-lengths, so motion reads the same on any grid; each fish varies around it, and the smaller shapes drift slower, which reads as depth. - `spriteSize`: integer magnification (crisp nearest-neighbor); 0 = auto, scaling with the grid so a fish reads as a fish on a 16x16 matrix and on a 768-wide desktop grid alike. -- `soundReactive`: move to the music: each sprite follows its own frequency band, so the scene breathes rather than surging as one block, and silence stands it still. Without an audio source the sprites keep moving normally. +- `audioReactive`: move to the music: each sprite follows its own frequency band, so the scene breathes rather than surging as one block, and silence stands it still. Without an audio source the sprites keep moving normally. Uses the global palette: every fish takes a body color from it, with its band a paler version of that same color rather than a second pick, which would read as two fish fused together. @@ -333,12 +333,59 @@ The classic screensaver on a light wall: chrome toasters with flapping wings and - `toast`: how many slices trail along (0–8). - `speed`: drift rate in sprite-widths, so flight reads the same on any grid; each flier varies Β±25% around it. - `spriteSize`: integer magnification for toasters AND toast (crisp nearest-neighbor); 0 = auto, scaling with the grid so a toaster reads as a toaster on a big wall. -- `soundReactive`: move to the music: each sprite follows its own frequency band, so the scene breathes rather than surging as one block, and silence stands it still. Without an audio source the sprites keep moving normally. +- `audioReactive`: move to the music: each sprite follows its own frequency band, so the scene breathes rather than surging as one block, and silence stands it still. Without an audio source the sprites keep moving normally. The sprites carry their own colors (chrome, wing, crust), so the global palette does not apply. Needs a grid at least the toaster's size (12Γ—9). Origin: projectMM original; inspired by After Dark's Flying Toasters (Berkeley Systems, 1989), suggested by Frank ([softhack007](https://github.com/softhack007)): the pixel art here is drawn fresh for this effect + + +### FixedPoint πŸ’«πŸ–ŒοΈ Β· 2D + +Shapes placed BETWEEN pixels rather than on them. A clock hand drawn on whole pixels jumps a full +pixel at a time and reads as broken; the same hand placed at a fractional position and antialiased +moves smoothly, because a pixel's brightness carries the fraction its position cannot. + +- `demo`: which figure, or `all` to cycle them. + - **clock**: a rim, twelve tick marks and three hands geared 1:12:144. The hands run on fixed + periods from the clock rather than on `bpm`, accelerated 10x so a second sweeps in 6 seconds. + - **orbits**: four rings circling the center, each breathing on its own oscillator. + - **star web**: a pentagram inside two rings, its stroke pulsing on a third harmonic. + - **spirograph**: a pen on a wheel rolling inside a larger circle. The figure closes because + the rates share a 3:2 ratio. + - **lissajous**: two perpendicular oscillators at 3:2, with the phase creeping so the figure + morphs rather than repeating. + - **cube thin** / **cube thick**: a wireframe cube in perspective, tumbling on two axes. Depth + reads as brightness, and on the thick one as stroke width too. + - **walkers**: six points on a damped random walk, held near the middle by a weak spring. + - **boids**: seven of them on the classic three rules (separation, alignment, cohesion) with + soft walls. The flock's shape is emergent; nothing tells it to form one. + - **hypotrochoid**: the spirograph with the wheel and pen sizes varying, so each visit draws a + different rosette. + - **tree**: a recursive trunk forking six levels deep, swaying on a 9 second wind cycle and + growing on a 10 second one, so it never repeats a pose. +- `bpm`: how fast the orbits and curve figures run. The clock keeps its own periods. +- `fade`: how much of the previous frame survives, which is what leaves the trail. At 255 the + shapes are crisp with no trail. +- `dwell`: seconds each demo holds before `all` moves on (hidden unless `demo` is `all`). +- `drift`: how far the whole scene wanders from the panel's center, in pixels. The original orbits + its origin rather than pinning it; 0 pins it. +- `zoom`: the camera. It pushes in toward the second hand's tip on a 20 second cycle, so the scene + grows and slides off-center at the peak and settles back, which is what makes the clock sweep + across the panel rather than sit still. 0 holds the camera fixed. + +Built on `draw::disc` / `draw::ring` / `draw::strokeLine`, the sub-pixel family in the draw layer; +the effect computes no coverage itself. Concept and the original fixed-point canvas demos: +[Sutaburosu](https://github.com/sutaburosu) in FastLED, via MoonLight, which bundles twelve behind +one control; all eleven are ported here. + +Origin: MoonLight (Sutaburosu) + +Detail: [technical](moxygen/FixedPointEffect.md) + +[Tests](../../tests/unit-tests.md#fixedpointeffect) + ### MovingHead πŸ’«πŸŽΆπŸŽ― Β· 1D @@ -365,11 +412,17 @@ an LED strip paints the color pattern and moves nothing. - `panRange` / `tiltRange`: how much of the fixture's travel to use. A head at full pan spends much of its sweep pointing away from the audience, so the default is a band around center. - `panCenter` / `tiltCenter`: where the sweep is centered (128 = the fixture's middle). -- `soundReactive`: move and light with the music: the beam swings wider as the room gets louder, +- `audioReactive`: move and light with the music: the beam swings wider as the room gets louder, each head takes its brightness from its own frequency band so the rig ripples rather than pulsing as one block, and a beat widens the sweep and flares the color with a short decay so a kick is visible rather than a one-frame flicker. Silence holds the rig still, which is what makes it read as reactive rather than merely animated. +- `gobo` / `rotate`: the beam's own wheels, shown only on a rig whose fixtures carry them. Both are + raw fixture bytes rather than a slot count: a gobo channel is a range per pattern and every model + splits it differently, so the fixture's manual is what says which value selects what. +- `goboOnBeat`: roll a new gobo on a bass hit instead of holding one pattern all night, then hold + that pattern for about two seconds. Without the hold a four-to-the-floor kick changes the pattern + four times a second, which reads as a flicker rather than as patterns. A fixture chain is one-dimensional, so lay the rig out as a **1 x N** grid (width 1, height N): extrude duplicates the x=0 column, so N x 1 would copy the first head's aim over every head. @@ -390,7 +443,7 @@ In this first iteration the characters travel independently and do not notice ea - `ghosts`: how many ghosts (0-8); the arcade cast is four. - `speed`: travel rate in sprite-widths, so motion reads the same on any grid; Pacman runs slightly ahead of the ghosts, as in the original. - `spriteSize`: integer magnification (crisp nearest-neighbor); 0 = auto, scaling with the grid so the characters read on a 16x16 matrix and on a 768-wide desktop grid alike. -- `soundReactive`: move to the music: each sprite follows its own frequency band, so the scene breathes rather than surging as one block, and silence stands it still. Without an audio source the sprites keep moving normally. +- `audioReactive`: move to the music: each sprite follows its own frequency band, so the scene breathes rather than surging as one block, and silence stands it still. Without an audio source the sprites keep moving normally. Pacman is always his own yellow; the ghosts take their body colors from the active palette, so they stay four distinguishable characters whatever palette is loaded. @@ -410,7 +463,7 @@ On a panel narrower than the formation the ranks scroll through the court instea - `stepX`: how far a step moves the formation sideways, in pixels. - `dropY`: how far a wall turn drops it, in pixels. - `size`: integer magnification per art pixel; 1 on a matrix, 2 or more on a wall. -- `soundReactive`: the beat becomes the clock: the formation steps on transients and stands still in silence, so the march locks to the track. +- `audioReactive`: the beat becomes the clock: the formation steps on transients and stands still in silence, so the march locks to the track. The invaders take their body color from the active palette. Origin: projectMM original; inspired by Taito's Space Invaders (1978), the pixel art drawn fresh for this effect @@ -429,7 +482,7 @@ Physics run on elapsed time, not per frame, so the plume looks the same on a 60 - `rate`: sprites launched per beat of the emit clock. - `emitBpm`: launches per minute, so the plume's density is a choice rather than a side effect of how fast the device runs. - `size`: integer magnification per art pixel. -- `soundReactive`: one sprite per frequency band, thrown when that band is loud, so the cast maps onto the spectrum in order: the bass bands throw fish, the treble bands throw invaders. Silence throws nothing. +- `audioReactive`: one sprite per frequency band, thrown when that band is loud, so the cast maps onto the spectrum in order: the bass bands throw fish, the treble bands throw invaders. Silence throws nothing. Colors come from the active palette, one entry per sprite, held for its whole flight. Origin: projectMM original @@ -448,7 +501,7 @@ The court is fixed point rather than pixels, so the game plays identically on a - `reflex`: how sharply a paddle chases the ball. Below full speed it lags a fast ball, which is where the misses come from. - `size`: integer magnification, when the ball is a sprite. - `spriteBall`: swap the classic square for a member of the shared sprite cast, re-picked on every hit, so a paddle knocks one character away and another back. -- `soundReactive`: the ball advances only on the beat, so it crosses the court in time with the track and stands still in silence. +- `audioReactive`: the ball advances only on the beat, so it crosses the court in time with the track and stands still in silence. Uses the global palette. Origin: projectMM original; inspired by Atari's Pong (1972) @@ -1178,22 +1231,6 @@ Nothing is transported. The effect keeps a short history of band frames and ever Origin: projectMM original, the radial spectrogram on `PolarLut` and the onset detector - - -### AudioVolume πŸ’«πŸŽ΅ - -AudioVolume effect preview - -A whole-grid VU meter: every light pulses with the mic level, color indexing the palette by loudness. - -- `brightness`: overall brightness ceiling for the VU pulse (1–255). - -Origin: projectMM original (VU meter) - -Detail: [technical](moxygen/AudioVolumeEffect.md) - -[Tests](../../tests/unit-tests.md#audioservice) - ### DemoReel πŸ’« Β· 3D diff --git a/docs/moonmodules/light/layouts.md b/docs/moonmodules/light/layouts.md index 0c09f7f7..bc4b7b1a 100644 --- a/docs/moonmodules/light/layouts.md +++ b/docs/moonmodules/light/layouts.md @@ -101,6 +101,7 @@ The classic 241-LED concentric-ring disc: nested rings of 1, 8, 12, 16, 24, 32, - `scale` β€” overall radius scale (1–10). - `outside in`: light 0 on the outer ring, wired inward, instead of at the center wired outward. The direction around each ring is unchanged. +- `angleFirst`: where light 0 of each ring sits, in degrees from the bottom (0-359), the same control [Ring](#ring) has. A disc is soldered with its first LED wherever the builder started, so this turns the image to match the hardware rather than re-wiring it. 0 is the unrotated placement. Origin: MoonLight Β· via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) diff --git a/docs/tutorials/generative-effects.md b/docs/tutorials/generative-effects.md index a7f3006c..070fab12 100644 --- a/docs/tutorials/generative-effects.md +++ b/docs/tutorials/generative-effects.md @@ -388,5 +388,5 @@ before trusting it, because a test that passes on both is measuring nothing. ## Where to go next - **[Power functions](../moonmodules/light/power-functions.md)**: the catalog, with what each one costs and who calls it -- **[Writing scripts](../moonmodules/light/writing-scripts.md)**: the MoonLive language reference +- **[Writing scripts](https://github.com/MoonModules/projectMM/blob/main/moonlive/README.md)**: the MoonLive language reference - **[Effects](../moonmodules/light/effects.md)**: every effect in the tree, with its controls diff --git a/moonbase/main/CMakeLists.txt b/moonbase/main/CMakeLists.txt index 4939f6f0..4640e6e9 100644 --- a/moonbase/main/CMakeLists.txt +++ b/moonbase/main/CMakeLists.txt @@ -1,6 +1,11 @@ idf_component_register( SRCS "moonbase_main.cpp" - INCLUDE_DIRS "" + # The app's src/ is NOT on the include path: MoonBase shares no sources with the application, + # which is the trade for an image that stays small and rarely changes. core/FirmwareImage.h is + # the one exception, and earns it: a header-only parser of the ESP32 image format that depends + # on nothing but . Both images must refuse to install the other, and that rule living + # in two hand-written copies is how they drift. + INCLUDE_DIRS "../../src" # The app's own logo, embedded IDF-natively; served as /logo.png (header + favicon). EMBED_FILES "../../src/ui/moonlight-logo.png" # esp_https_ota brings esp_http_client and esp-tls with it, so neither is listed separately. diff --git a/moonbase/main/moonbase_main.cpp b/moonbase/main/moonbase_main.cpp index 2e8f5f5f..f16ec381 100644 --- a/moonbase/main/moonbase_main.cpp +++ b/moonbase/main/moonbase_main.cpp @@ -23,6 +23,8 @@ #include "esp_event.h" #include "esp_http_client.h" #include "esp_crt_bundle.h" +#include "core/FirmwareImage.h" // identify(): the one shared header, see main/CMakeLists.txt +#include "esp_app_desc.h" // esp_app_get_description: this image's own version #include "esp_https_ota.h" #include "esp_littlefs.h" #include "esp_netif.h" @@ -365,8 +367,12 @@ const char kPage[] = "

MoonBase

" // The (?) module cards carry, pointing at the published MoonBase doc. "?
" - "

Install firmware to return this device to normal operation.

" + "href='https://moonmodules.org/projectMM/gettingstarted.html#if-your-device-shows-moonbase'>?" + "

Install firmware to return this device to normal operation." + // WHICH MoonBase this is. Filled by the boot script below rather than baked into this + // literal: PROJECT_VER is defined only for IDF's own descriptor TU, and the descriptor is + // already in the image, so reading it back costs nothing and cannot drift from it. + "

" "
From a file
" "" // The last resort when no URL is at hand: name where the firmware--v*.bin files @@ -381,14 +387,34 @@ const char kPage[] = // Shown only while an install is running (S() toggles it): the one moment cancel applies. "
" "
" + // A real bar, not a sweep: an install is a minute of a user watching a number they cannot + // read as a fraction. Hidden until a byte count actually arrives. + "" "