From 9afeec13c50d96a97ce69aefb9db49c1703cabd6 Mon Sep 17 00:00:00 2001 From: ewowi Date: Sat, 8 Aug 2026 17:59:42 +0200 Subject: [PATCH 01/11] Rename the Layers container to Effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three top-level light containers are now Layouts, Effects and Drivers — L.E.D. The old name sat one character from its own child (Layers holding Layers) and read as a near-twin of Layouts, which is the pair a newcomer actually has to tell apart. The tree keeps its shape: Effects -> Layers -> effects and modifiers. desktop 140us/7,142fps | esp32 4,164us/240fps | flash unchanged Light domain - Layers -> Effects: the class, its header, the registered type name, the card image, and Drivers::setLayers -> setEffects. - Layer, the child, is untouched. The two names differ by one character, so every replacement is word-boundary anchored rather than a token sweep — the distinction this rename draws is exactly what a blind replace would collapse. - src/light/layers/ keeps its name: it also holds Layer.h, Buffer.h, BlendMap.h and MappingLUT.h, which are all still layer things. Core - ControlModule's capture-role index is kEffectsRole, and its static_assert re-pinned to spell "Effects". It checked the old name character by character ([0]=='L', [1]=='a', [5]=='s'), so it would have failed the build. Tests - unit_Layers_container -> unit_Effects_container, scenario_Layers_composition -> scenario_Effects_composition, and the scenario key that binds Drivers to the container is "effects" on both sides. - No golden moved, which is the evidence this is a rename and nothing else. Docs/CI - MIGRATING.md carries the break. The type name IS the persisted filename and the preset capture key, so a device boots with an empty light tree and presets stop applying their effects. Per ADR-0013 that is documented, not migrated. Both halves were verified on a running desktop: the tree does come up empty, and renaming /.config/Layers.json to Effects.json restores it intact — so the shortcut in that entry is tested rather than assumed. - docs/history/ is left alone: it records what was true when written. Co-Authored-By: Claude Opus 5 (1M context) --- docs/MIGRATING.md | 18 +++ docs/architecture.md | 24 ++-- docs/assets/light/{Layers.png => Effects.png} | Bin .../power-functions-analysis-top-down.md | 2 +- docs/backlog/system-modules.md | 2 +- docs/coding-standards.md | 2 +- docs/gettingstarted.md | 6 +- docs/metrics/repo-health.json | 32 ++--- docs/metrics/repo-health.md | 26 ++-- docs/moonmodules/core/control.md | 10 +- docs/moonmodules/core/services.md | 4 +- docs/moonmodules/light/supporting.md | 6 +- docs/usecases/build-your-own-moonmodules.md | 4 +- docs/usecases/home-automation.md | 2 +- src/core/ControlModule.h | 26 ++-- src/core/FilesystemModule.h | 2 +- src/core/HttpServerModule.cpp | 4 +- src/core/ModuleFactory.h | 2 +- src/core/MoonModule.h | 2 +- src/core/Scheduler.cpp | 2 +- src/core/Services.h | 2 +- src/light/drivers/Drivers.h | 44 +++--- src/light/effects/EffectBase.h | 2 +- src/light/layers/Buffer.h | 2 +- src/light/layers/{Layers.h => Effects.h} | 16 +-- src/light/layers/Layer.h | 4 +- src/light/layers/MappingLUT.h | 2 +- src/light/layouts/Layouts.h | 4 +- src/main.cpp | 26 ++-- src/ui/app.js | 20 +-- src/ui/style.css | 2 +- test/CMakeLists.txt | 2 +- test/scenario_runner.cpp | 20 +-- .../scenario_MoonModule_control_change.json | 10 +- .../light/scenario_Audio_mutation.json | 10 +- ...json => scenario_Effects_composition.json} | 24 ++-- .../light/scenario_GridBlacks_blackpixel.json | 10 +- .../light/scenario_GridLayout_resize.json | 10 +- .../scenario_MoonLiveEffect_controls.json | 8 +- .../scenario_MoonLiveEffect_livescript.json | 10 +- .../light/scenario_modifier_chain.json | 8 +- .../light/scenario_modifier_swap.json | 12 +- test/scenarios/light/scenario_perf_full.json | 8 +- test/scenarios/light/scenario_perf_light.json | 10 +- .../light/scenario_peripheral_grid_sweep.json | 12 +- .../light/scenario_peripheral_switch.json | 14 +- test/unit/core/unit_ControlModule.cpp | 50 +++---- .../core/unit_FilesystemModule_subtree.cpp | 24 ++-- test/unit/core/unit_MoonModule_lifecycle.cpp | 2 +- test/unit/core/unit_MqttModule.cpp | 12 +- .../unit/core/unit_Scheduler_unique_names.cpp | 12 +- test/unit/core/unit_Services.cpp | 4 +- test/unit/light/golden_frame.h | 2 +- test/unit/light/unit_Canvas.cpp | 2 +- test/unit/light/unit_Drivers_container.cpp | 2 +- test/unit/light/unit_Drivers_rendersplit.cpp | 12 +- ...ntainer.cpp => unit_Effects_container.cpp} | 126 +++++++++--------- test/unit/light/unit_Layer_live_modifier.cpp | 2 +- test/unit/light/unit_Layer_persistence.cpp | 2 +- .../unit/light/unit_Layer_phase_animation.cpp | 2 +- test/unit/light/unit_Layouts_container.cpp | 2 +- test/unit/light/unit_Layouts_toggle_cycle.cpp | 6 +- test/unit/light/unit_PreviewDriver.cpp | 8 +- test/unit/light/unit_RandomMapModifier.cpp | 2 +- 64 files changed, 379 insertions(+), 361 deletions(-) rename docs/assets/light/{Layers.png => Effects.png} (100%) rename src/light/layers/{Layers.h => Effects.h} (84%) rename test/scenarios/light/{scenario_Layers_composition.json => scenario_Effects_composition.json} (73%) rename test/unit/light/{unit_Layers_container.cpp => unit_Effects_container.cpp} (87%) diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 473d2194..8ac65949 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -20,6 +20,24 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul ## Unreleased (`next-iteration`) +### The `Layers` container is renamed to `Effects` (2026-08-08) + +The three top-level light containers are now **Layouts, Effects, Drivers** — L.E.D. The old name sat one character from its own child (`Layers` holding `Layer`s) and read as a near-twin of `Layouts`, which is the pair a newcomer actually has to tell apart. The tree is unchanged in shape: `Effects` → `Layer`s → effects and modifiers. + +**Action: *re-add a module* and *re-save presets*.** + +The type name is the persisted filename and the preset capture key, so two things do not survive the update: + +| What | Why | What to do | +|---|---|---| +| The saved light tree | The device looks for `/.config/Effects.json` and the old file is `Layers.json`, so the light tree boots empty | Re-add your Layer, effect and modifiers, then let it save | +| Presets that capture the look | A preset file records `"captures": ["Layers"]`, a key no module now answers to | Re-save each preset once the tree is rebuilt | + +Renaming the file on the device works if you would rather not rebuild by hand: `Layers.json` → `Effects.json`, and `"Layers"` → `"Effects"` inside each `/.config/presets/*.json`. Nothing else in either file changes. + +The child `Layer` keeps its name, as does everything under it. + + ### The `peripheral` options are renamed to name the peripheral, not the bus protocol (2026-07-30) The `peripheral` dropdown no longer says `i80` / `MoonI80`. "i80" is the Intel 8080 bus shape `esp_lcd` speaks — it is not a peripheral any ESP32 datasheet lists, and it matched nothing a user could look up: on the classic ESP32 that backend **is the I2S peripheral**, on the S3/P4/S31 it is the **LCD** peripheral. The new labels name the silicon block plus who drives it, which is the actual choice being made. diff --git a/docs/architecture.md b/docs/architecture.md index 164c8265..d5c92ce8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,7 +30,7 @@ Coding conventions live in [coding-standards.md](coding-standards.md); how to bu - [The pipeline](#the-pipeline) - [3D from the start](#3d-from-the-start) - [Layouts and Layout](#layouts-and-layout) - - [Layers and Layer](#layers-and-layer) + - [Effects and Layer](#layers-and-layer) - [Effects](#effects) - [Dimensionality](#dimensionality) - [Robustness rules](#robustness-rules) @@ -284,7 +284,7 @@ A **service** is a MoonModule (role `ModuleRole::Service`) that bridges to the o The defining line is the **data relationship, not the connector**: *does the module consume the light output buffer?* If yes it's a **driver** (ArtNet, DMX, SPI-LED all consume the buffer, differing only in transport; a DMX sender uses a UART/RS-485 transport but is a driver because it sends the rendered buffer). If no, it's a **service**. -Services are **user-add/deletable children of the `Services` container** — the core-domain twin of the light pipeline's `Layers`/`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. +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. @@ -316,11 +316,11 @@ The light domain is everything specific to driving lights. **Light** here means Modules in the light pipeline can be added, replaced, or removed dynamically at runtime. ```text - Layouts (shared by every Layer in Layers) + Layouts (shared by every Layer in Effects) ├── GridLayout ──→ coordinate iterator └── WheelLayout ──→ coordinate iterator │ - Layers + Effects ┌───────┼───────┐ ▼ ▼ ▼ Layer A Layer B Layer C @@ -339,12 +339,12 @@ Modules in the light pipeline can be added, replaced, or removed dynamically at **Data flow.** The pipeline instantiates both core data-exchange shapes (see [§ Data exchange between modules](#data-exchange-between-modules)): -- *Shared-struct (pull):* `Drivers` hands every child driver a `Buffer*` (source) plus a `Correction*` (shared brightness/reorder/white), and `Layer` exposes its pixel buffer to `Drivers` directly on the identity-mapping fast path: each consumer holds a `const`-pointer and reads it per frame. The pointers are **(re)bound on every rebuild**, not just at boot: `Drivers::prepare()` re-resolves the active `Layer` (`Layers::activeLayer()`) and calls `passBufferToDrivers()`, which re-runs `setSourceBuffer()`/`setLayer()` on each child (clearing them to `nullptr` when there is no active Layer). So a held pointer is valid only until the next rebuild — which is exactly why the consumers re-read it each frame and tolerate a null (the [robustness rule](#robustness)): a Layer add/delete/replace re-binds or clears it live, no dangling reference. +- *Shared-struct (pull):* `Drivers` hands every child driver a `Buffer*` (source) plus a `Correction*` (shared brightness/reorder/white), and `Layer` exposes its pixel buffer to `Drivers` directly on the identity-mapping fast path: each consumer holds a `const`-pointer and reads it per frame. The pointers are **(re)bound on every rebuild**, not just at boot: `Drivers::prepare()` re-resolves the active `Layer` (`Effects::activeLayer()`) and calls `passBufferToDrivers()`, which re-runs `setSourceBuffer()`/`setLayer()` on each child (clearing them to `nullptr` when there is no active Layer). So a held pointer is valid only until the next rebuild — which is exactly why the consumers re-read it each frame and tolerate a null (the [robustness rule](#robustness)): a Layer add/delete/replace re-binds or clears it live, no dangling reference. - *Push to a core sink:* `PreviewDriver` owns the preview wire format (a one-time coordinate table + per-frame RGB point list) and pushes the bytes to a `BinaryBroadcaster` (the core HTTP server). The server broadcasts them over WebSocket without knowing they're a preview: the format and the light types stay entirely in the driver. See [PreviewDriver](moonmodules/light/moxygen/PreviewDriver.md). **Graceful degradation under transport backpressure.** The preview is the transport-side sibling of the memory-side [§ Degradation cascade](#degradation-cascade): when the browser can't keep up with a full-resolution frame (128² = ~49 KB), the producer sheds quality rather than stall the loop, in video-streaming order, frame rate then resolution. The frame streams from the driver buffer with no intermediate copy, a resumable memory-adaptive chunk per tick, and the next frame starts only once the previous drained, so the effective frame rate self-limits to what the link sustains. Only when a single frame can't drain promptly does it downsample via a spatial lattice (the adaptive-bitrate idea behind HLS/DASH, on a binary WebSocket). Each delivered frame is whole (a WebSocket message is atomic), the render loop is charged a bounded slice per tick, and a client blocked past the spin budget is closed and reconnects (a blip, not a freeze). The mechanism is payload-agnostic and lives in [PreviewDriver](moonmodules/light/moxygen/PreviewDriver.md) + `HttpServerModule`, so other bulky streams can ride the same transport. -**Naming convention.** Capital `Layouts`, `Layers`, `Drivers` are class names (always capitalised when referring to the class). Lowercase "layouts", "layers", "drivers" is the English plural, used freely when context makes it clear. Singular "layout", "layer", "driver" is an individual instance. +**Naming convention.** Capital `Layouts`, `Effects`, `Drivers` are class names (always capitalised when referring to the class). Lowercase "layouts", "layers", "drivers" is the English plural, used freely when context makes it clear. Singular "layout", "layer", "driver" is an individual instance. ## 3D from the start @@ -369,13 +369,13 @@ Positions are computed algorithmically, not stored. Grid is the most commonly us Multiple layouts can live in one Layouts container. Each layout describes one light type: the model is one light type per layout (LED strips, or par lights), not mixed in a single Layouts. -## Layers and Layer +## Effects and Layer -**Layers** (a MoonModule) is the top-level container for one or more layers. Each layer renders independently into its own buffer; the Drivers container composes those buffers downstream. +**Effects** (a MoonModule) is the top-level container for one or more layers. Each layer renders independently into its own buffer; the Drivers container composes those buffers downstream. **Multi-layer composition.** The container composes more than one Layer's buffer into the shared output: each enabled Layer renders into its own buffer, and the Drivers container's blend+map step composites them in container order (bottom→top) into the physical buffer (which is why that buffer is a *blend* buffer in [§ Memory strategy](#memory-strategy)). Each Layer carries a `blendMode` (alpha-over or additive) and an `opacity` — inert parameters the Layer never acts on; Drivers reads them and the container child order, and blends bottom→top. The bottom layer clears + overwrites the output; each layer above blends onto the accumulated frame per its mode and opacity. With a single enabled Layer this is the degenerate case: a thin pass-through that hands the driver the Layer's buffer directly (no composite), byte-for-byte the single-layer pipeline. The blend math is integer-only per the hot-path rule (8-bit alpha-over `(src·α + dst·(255−α))/255`, additive sum-with-clamp); cost scales with the enabled-layer count. -A **Layer** (a MoonModule, child of Layers) owns: +A **Layer** (a MoonModule, child of Effects) owns: - A **buffer**: the light data effects write into (logical space). - A **mapping LUT**: built by the layer from the shared Layouts and the layer's static modifiers. @@ -386,7 +386,7 @@ A layer can have **multiple effects**. Each effect writes to the buffer sequenti A layer applies **all its enabled modifiers as a chain** during the mapping build (`Layer::rebuildLUT`): each modifier is a coordinate fold, and they compose in child order (M₁∘M₂∘…). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror). The fold contract (the three hooks, the physical→logical build, the live pass) is documented in [ModifierBase](moonmodules/light/moxygen/ModifierBase.md). -Each layer references the shared Layouts. The layer builds its mapping by walking the Layouts container's **physical** coordinates and folding each through the static modifier chain to its logical cell — N physical lights folding onto one logical cell is the fan-out (a Multiply kaleidoscope), so the build never produces a fan-out overflow. Different layers in Layers can have different modifiers, producing different mappings from the same Layouts. +Each layer references the shared Layouts. The layer builds its mapping by walking the Layouts container's **physical** coordinates and folding each through the static modifier chain to its logical cell — N physical lights folding onto one logical cell is the fan-out (a Multiply kaleidoscope), so the build never produces a fan-out overflow. Different layers in Effects can have different modifiers, producing different mappings from the same Layouts. ## Effects @@ -472,7 +472,7 @@ A recompile is the normal cold-path rebuild: editing the `source` control routes ## Modifiers -A modifier (MoonModule) lives inside a layer alongside its effects. Modifiers expose a virtual interface: the Layer calls modifier methods without knowing the concrete type (no `dynamic_cast`). A layer applies **all** its enabled modifiers as a chain, in child order — each a coordinate fold composed into one mapping (see [§ Layers and Layer](#layers-and-layer)). +A modifier (MoonModule) lives inside a layer alongside its effects. Modifiers expose a virtual interface: the Layer calls modifier methods without knowing the concrete type (no `dynamic_cast`). A layer applies **all** its enabled modifiers as a chain, in child order — each a coordinate fold composed into one mapping (see [§ Effects and Layer](#layers-and-layer)). A modifier is a coordinate transform, applied in one of two ways (the fold contract is in [ModifierBase](moonmodules/light/moxygen/ModifierBase.md)): @@ -600,7 +600,7 @@ The UI is **MoonModule-driven**. It contains no hard-coded knowledge of specific Adding a new MoonModule with controls needs **zero changes** to the UI files. This extends to the tree-mutation affordances: which modules accept children (and of what role) comes from each type's `acceptsChildRoles()`, and whether a module can be deleted/replaced comes from its `userEditable()`: both declared on the C++ side and reported in `/api/types` + `/api/state`. The UI hardcodes no list of "which types are containers" or "which roles are editable"; a new container type or a fixed child is a one-line C++ override. -The light domain plugs into the UI at three points: a fixed top-level tree (Layouts / Layers / Drivers pinned in `main.cpp`, root reorder disabled while child reorder works via drag-and-drop), a binary WebSocket preview channel ([PreviewDriver](moonmodules/light/moxygen/PreviewDriver.md): a `0x03` coordinate table sent once per LUT rebuild plus per-frame `0x02` RGB point lists, so sparse layouts preview at their real positions), and per-role emoji for the chip filter (the `ROLE_EMOJI` map in `app.js` is the single source of truth: `effect`, `driver`, …, `service`). Full UI spec: [docs/moonmodules/core/ui.md](moonmodules/core/ui.md). +The light domain plugs into the UI at three points: a fixed top-level tree (Layouts / Effects / Drivers pinned in `main.cpp`, root reorder disabled while child reorder works via drag-and-drop), a binary WebSocket preview channel ([PreviewDriver](moonmodules/light/moxygen/PreviewDriver.md): a `0x03` coordinate table sent once per LUT rebuild plus per-frame `0x02` RGB point lists, so sparse layouts preview at their real positions), and per-role emoji for the chip filter (the `ROLE_EMOJI` map in `app.js` is the single source of truth: `effect`, `driver`, …, `service`). Full UI spec: [docs/moonmodules/core/ui.md](moonmodules/core/ui.md). ## Tag emoji legend diff --git a/docs/assets/light/Layers.png b/docs/assets/light/Effects.png similarity index 100% rename from docs/assets/light/Layers.png rename to docs/assets/light/Effects.png diff --git a/docs/backlog/power-functions-analysis-top-down.md b/docs/backlog/power-functions-analysis-top-down.md index f0723d50..93a86680 100644 --- a/docs/backlog/power-functions-analysis-top-down.md +++ b/docs/backlog/power-functions-analysis-top-down.md @@ -245,7 +245,7 @@ What they do *not* remove is the conceptual one: an effect is 3D when its idea i ## 6d. Live performance ("DeeJaying") ❓ *(an argument that it is reachable, not a built capability)* -A stretch goal worth recording because the infrastructure is largely built: **playing effects live from the control surface — pads, faders, encoders — with no code changes.** What already exists: control changes reach a running module without a rebuild (`MoonModule::onControlChanged`), the surface routes faders and encoders through `Scheduler::setControl` (the same domain-neutral primitive IR and MQTT use), Layers composite with blend modes and opacity, and presets snapshot and restore whole subtrees. +A stretch goal worth recording because the infrastructure is largely built: **playing effects live from the control surface — pads, faders, encoders — with no code changes.** What already exists: control changes reach a running module without a rebuild (`MoonModule::onControlChanged`), the surface routes faders and encoders through `Scheduler::setControl` (the same domain-neutral primitive IR and MQTT use), Effects composite with blend modes and opacity, and presets snapshot and restore whole subtrees. Power functions sharpen this in a specific way: **the more of an effect's mechanics live in shared, control-driven primitives, the more of it is playable rather than fixed.** A hand-rolled accumulator is private state a surface cannot reach; a `BeatPhase` fed from a control is a tempo a performer can ride. The same holds for `particles` (gravity, drag, emission as live parameters) and the field family (warp amount, octaves). diff --git a/docs/backlog/system-modules.md b/docs/backlog/system-modules.md index e0fbd85b..095668f7 100644 --- a/docs/backlog/system-modules.md +++ b/docs/backlog/system-modules.md @@ -62,7 +62,7 @@ The split maps onto one rule, matching how OS system managers behave (Task Manag - **Everything under `System` is FIXED** — always present, **no add/delete**, wired-by-code. That's System's own vitals **and** the System Modules (Tasks, I2cScan; Memory, Pins later) **and** the always-there infrastructure (Network, Firmware, Improv). You don't delete a System Module any more than you delete Task Manager. - **Everything under `Services` is USER-MANAGED** — add/delete/replace, `ModuleRole::Service`. Audio, IR. (MQTT stays code-wired under Network as always-there infra; Devices is fleet-scope, see its note.) -**Services is to System what Layouts / Layers / Drivers are to the light pipeline** — this is the unifying insight, and the strongest justification (*Common patterns first*): projectMM *already* has the "top-level container holding user-added children of one role" pattern in the light domain (`Layers` holds effects you add, `Drivers` holds drivers you add). **`Services` is that exact same container shape applied to the core domain** — not a new concept, the existing one reused. So the split isn't inventing anything; it's recognising that Audio/MQTT/IR belong in a `Layers`-style container, and that container is `Services`. +**Services is to System what Layouts / Effects / Drivers are to the light pipeline** — this is the unifying insight, and the strongest justification (*Common patterns first*): projectMM *already* has the "top-level container holding user-added children of one role" pattern in the light domain (`Effects` holds effects you add, `Drivers` holds drivers you add). **`Services` is that exact same container shape applied to the core domain** — not a new concept, the existing one reused. So the split isn't inventing anything; it's recognising that Audio/MQTT/IR belong in a `Effects`-style container, and that container is `Services`. This settles the role question: **Services children = `ModuleRole::Service` (user-managed); System's fixed children carry no user-editable affordance** (wired-by-code, no delete). It **fixes TasksModule's current stopgap** — it was given `Peripheral`+delete only to render the delete button, which is now the *wrong* answer: Tasks is a fixed System child, so it should be wired-by-code with no delete, like ImprovProvisioning under Network. diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 55e0a96f..464cd7c0 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -67,7 +67,7 @@ When a `switch (type)` outside the type's home file is legitimate: the caller ha ## File shape: header-only vs `.h` + `.cpp` -- **Light-domain modules and the `MoonModule` base: header-only.** Every effect, modifier, driver, layout, the light-domain containers (`Layouts`, `Layers`, `Drivers`, `Layer`), and the `MoonModule` base class live in a single `.h` with implementation inline. The benefit is concrete: a contributor copies `RainbowEffect.h`, edits, saves as `MyEffect.h`, registers one line in `main.cpp` — no "where does the `.cpp` go, what does CMake need" friction. The chain `RainbowEffect.h → EffectBase.h → MoonModule.h` is uniform; readers don't pivot to a different file shape at the base. When a light-domain file outgrows one concern, extract a helper into its own header (`BlendMap`, `MappingLUT`) rather than splitting to `.h` + `.cpp`. Header-only is a feature of the light domain. +- **Light-domain modules and the `MoonModule` base: header-only.** Every effect, modifier, driver, layout, the light-domain containers (`Layouts`, `Effects`, `Drivers`, `Layer`), and the `MoonModule` base class live in a single `.h` with implementation inline. The benefit is concrete: a contributor copies `RainbowEffect.h`, edits, saves as `MyEffect.h`, registers one line in `main.cpp` — no "where does the `.cpp` go, what does CMake need" friction. The chain `RainbowEffect.h → EffectBase.h → MoonModule.h` is uniform; readers don't pivot to a different file shape at the base. When a light-domain file outgrows one concern, extract a helper into its own header (`BlendMap`, `MappingLUT`) rather than splitting to `.h` + `.cpp`. Header-only is a feature of the light domain. - **Core service modules: `.h` + `.cpp`.** Core modules that bridge to the platform layer or implement substantial infrastructure (`HttpServerModule`, `FilesystemModule`, `NetworkModule`, `Scheduler`, `SystemModule`, `Control`) ship as a `.h` (interface) plus a `.cpp` (implementation). Three reasons that compound: (a) implementation changes recompile only the `.cpp`, not every TU that includes the header — incremental builds are 2–5× faster on the kind of edits that happen in development; (b) readers want the interface separately from the body; (c) symbol bloat and link-time stay bounded. Small core utilities that are *almost entirely declarations or inline accessors* — `types.h`, `color.h`, `version.h`, `BinaryBroadcaster.h`, `JsonUtil.h`, `JsonSink.h`, `Sha1.h`, `Base64.h` — stay header-only. Templates (e.g. `ModuleFactory::registerType`) also must stay in the header because of C++ instantiation rules; a module that's mostly template can therefore stay header-only. - **A catalog module includes ONLY its base header.** Every effect, modifier, layout, and concrete driver leads with exactly one include — `light/effects/EffectBase.h`, `light/layouts/LayoutBase.h`, `light/modifiers/ModifierBase.h`, or `light/drivers/DriverBase.h` — the base class it subclasses, and nothing else at the top of the file. That base header is the module author's **standard library**: it declares the base class AND pulls in the render context, the common domain helpers (`draw` / `Palette` / `math8` / `noise` / `color` / `crc` for effects; `DriverBase`'s own `Layer`/`Buffer`/`Correction`/platform for drivers; the base + integer trig for modifiers/layouts), the lifecycle primitives (`ScratchBuffer`), the audio source, AND the standard-library headers the bodies use (``, ``, ``, ``; drivers add ``, ``). Bundling this whole surface is **byte-free** — unused declarations emit no code (measured: the ESP32 image did not grow when the set was maximised), so the reflex is *add the common header to the base, don't scatter it per file*. That keeps every module in a domain reading identically and the copy-edit-register workflow free of include guesswork; it's also the surface a scripted MoonLive module gets uniformly. This is a *maximal* (prelude-style) bundle on purpose — a recognisable pattern (Rust's `std::prelude`, a project-wide `framework.h`), justified at the introduction site in each base header's comment. diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 82e51d07..6fd8dc88 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -269,14 +269,14 @@ on **serpentine** if your strip zig-zags back and forth. > [Layouts](moonmodules/light/supporting.md) -**Layers** — what plays on the lights. Add an **effect** (a moving pattern), stack +**Effects** — what plays on the lights. Add an **effect** (a moving pattern), stack several to blend them, and reshape them with **modifiers** (mirror, rotate, and more). Each effect has its own controls — speed, color mode, and so on — that you tweak live. -![The Layers module](assets/gettingstarted/02-09-UI-Layers.png) +![The Effects module](assets/gettingstarted/02-09-UI-Layers.png) -> [Layers](moonmodules/light/supporting.md) · [Layer](moonmodules/light/supporting.md) +> [Effects](moonmodules/light/supporting.md) · [Layer](moonmodules/light/supporting.md) **Drivers** — where the colors go. Set overall **brightness** and color order, then add an output: real LED strips on a pin, or send the frame over the network diff --git a/docs/metrics/repo-health.json b/docs/metrics/repo-health.json index 2a6fbad6..0d983a55 100644 --- a/docs/metrics/repo-health.json +++ b/docs/metrics/repo-health.json @@ -1,13 +1,13 @@ { - "commit": "2e23f158", + "commit": "26fcf00a", "flash": { "esp32": 1744736, "esp32p4-eth": 1503280, "esp32p4-eth-wifi": 1793760, - "esp32s3-n16r8": 1739968, + "esp32s3-n16r8": 1740480, "esp32s3-n8r8": 1666992, "esp32s31": 1932624, - "desktop": 1100648 + "desktop": 1100664 }, "perf": { "desktop": { @@ -20,21 +20,21 @@ } }, "loc": { - "core": 16537, - "light": 23604, + "core": 16540, + "light": 23627, "platform": 12590, "ui": 6467, - "test": 39761, + "test": 39937, "moondeck": 20039 }, "comments": { "core": { - "lines": 6216, + "lines": 6219, "ratio": 0.409 }, "light": { - "lines": 9092, - "ratio": 0.426 + "lines": 9114, + "ratio": 0.427 }, "platform": { "lines": 4233, @@ -45,8 +45,8 @@ "ratio": 0.274 }, "test": { - "lines": 6777, - "ratio": 0.197 + "lines": 6855, + "ratio": 0.198 }, "moondeck": { "lines": 3198, @@ -54,19 +54,19 @@ } }, "tests": { - "cases": 1267, + "cases": 1271, "scenarios": 22 }, "docs": { "md_files": 176, - "md_lines": 24123, + "md_lines": 24183, "plans_files": 91, - "backlog_lines": 3611, - "lessons_lines": 418, + "backlog_lines": 3617, + "lessons_lines": 454, "claude_md_lines": 135 }, "complexity": { - "functions": 2417, + "functions": 2419, "over_threshold": 149, "worst_ccn": 93 } diff --git a/docs/metrics/repo-health.md b/docs/metrics/repo-health.md index c91ce5c6..ff0112e7 100644 --- a/docs/metrics/repo-health.md +++ b/docs/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `2e23f158`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `26fcf00a`. 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,11 +8,11 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Flash | |---|---:| -| desktop | 1,075 KB (+1 KB) ⚠ | +| desktop | 1,075 KB (+0 KB) ⚠ | | esp32 | 1,704 KB | | esp32p4-eth | 1,468 KB | | esp32p4-eth-wifi | 1,752 KB | -| esp32s3-n16r8 | 1,699 KB (+3 KB) ⚠ | +| esp32s3-n16r8 | 1,700 KB (+0 KB) ⚠ | | esp32s3-n8r8 | 1,628 KB | | esp32s31 | 1,887 KB | @@ -20,33 +20,33 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Tick | FPS | |---|---:|---:| -| desktop | 140 µs (+8 µs) ⚠ | 7,142 (−433) ⚠ | +| desktop | 140 µs | 7,142 | | esp32 | 4,164 µs | 240 | ## Code | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 16,537 (+104) ⚠ | 6,216 | 40.9 % (−0.1 %) ✓ | -| light | 23,604 (+282) ⚠ | 9,092 | 42.6 % (+0.1 %) ⚠ | +| core | 16,540 (+3) ⚠ | 6,219 | 40.9 % | +| light | 23,627 (+23) ⚠ | 9,114 | 42.7 % (+0.1 %) ⚠ | | platform | 12,590 | 4,233 | 37.2 % | | ui | 6,467 | 1,670 | 27.4 % | -| test | 39,761 (+215) ⚠ | 6,777 | 19.7 % | +| test | 39,937 (+176) ⚠ | 6,855 | 19.8 % (+0.1 %) ⚠ | | moondeck | 20,039 | 3,198 | 18.3 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 1,267 (+13) ✓ | +| unit cases | 1,271 (+4) ✓ | | scenarios | 22 | ## Complexity | Metric | Value | |---|---:| -| functions | 2,417 (+12) ✓ | -| over threshold | 149 (−2) ✓ | +| functions | 2,419 (+2) ✓ | +| over threshold | 149 | | worst CCN | 93 | ## Documentation @@ -54,9 +54,9 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Metric | Value | |---|---:| | markdown files | 176 | -| markdown lines | 24,123 (+94) ⚠ | +| markdown lines | 24,183 (+60) ⚠ | | plan files | 91 | -| backlog lines | 3,611 (+62) ⚠ | -| lessons lines | 418 | +| backlog lines | 3,617 (+6) ⚠ | +| lessons lines | 454 (+36) ⚠ | | CLAUDE.md lines | 135 | diff --git a/docs/moonmodules/core/control.md b/docs/moonmodules/core/control.md index 0582428e..af3569e3 100644 --- a/docs/moonmodules/core/control.md +++ b/docs/moonmodules/core/control.md @@ -2,7 +2,7 @@ The device's control surface — the place that says "put the device into this state", whatever asked for it. A preset applied from the grid, and later a fader moved on a MIDI desk, arrive at the same code. Its first capability is presets; the surface layout exists so external controllers map onto something that already looks like them. -`ControlModule` is a top-level module, a peer of Layouts / Layers / Drivers rather than a child of Services: it reaches *across* the top-level modules, so it cannot sit inside one. +`ControlModule` is a top-level module, a peer of Layouts / Effects / Drivers rather than a child of Services: it reaches *across* the top-level modules, so it cannot sit inside one. ## Control modules @@ -35,14 +35,14 @@ A preset captures **exactly one** top-level subtree, recorded in the file: ```json { "slot": 12, - "captures": "Layers", - "Layers.enabled": true, "Layers.0.type": "Layer", "Layers.0.0.type": "NoiseEffect" + "captures": "Effects", + "Effects.enabled": true, "Effects.0.type": "Layer", "Effects.0.0.type": "NoiseEffect" } ``` Each captured subtree is exactly the bytes the persistence engine already writes for that module, namespaced under a `.` key prefix. Save and restore therefore reuse the engine that reconciles a tree against JSON ([`saveSubtreeTo` / `applySubtree`](moxygen/FilesystemModule.md)) rather than a second serializer that could drift from it. -One subtree per preset is the whole model: a preset is *a look*, or *a geometry*, or *a hardware setup*, or *a service configuration. Never a combination. A `Layers` preset is a look, and applies to a board with completely different hardware; a `Drivers` preset carries pin maps and is device-specific. Choosing the role is a single radio button when saving, and the pad's color says which role it holds. +One subtree per preset is the whole model: a preset is *a look*, or *a geometry*, or *a hardware setup*, or *a service configuration. Never a combination. A `Effects` preset is a look, and applies to a board with completely different hardware; a `Drivers` preset carries pin maps and is device-specific. Choosing the role is a single radio button when saving, and the pad's color says which role it holds. A preset naming a subtree this build does not have is refused with a reason rather than partially applied, and a file written by an older build that names several subtrees is listed but not applied, so it can be seen and deleted rather than silently vanishing. A malformed file leaves the live tree untouched. @@ -60,7 +60,7 @@ Structural mutation quiesces the render worker, and mutations run inline on the ## Home Assistant -Looks reach Home Assistant two ways, and only `Layers` presets travel either of them. +Looks reach Home Assistant two ways, and only `Effects` presets travel either of them. **The WLED integration** (`/presets.json`) is the native path: HA renders looks in its own preset dropdown, shows which one is applied, and applies one when it is chosen. This is what HA calls a preset. diff --git a/docs/moonmodules/core/services.md b/docs/moonmodules/core/services.md index 3d7d8315..a4cae64f 100644 --- a/docs/moonmodules/core/services.md +++ b/docs/moonmodules/core/services.md @@ -1,12 +1,12 @@ # Core services -The user-added **Service** modules — capability bridges the device provides or consumes, added and removed at runtime in the `Services` container (the core-domain twin of the light domain's `Layers`/`Drivers`). Fixed device infrastructure (identity, network, inspection tools) lives under **System** — see [core/system.md](system.md). Every row links to its generated technical page (the full API, from the `.h`) and its tests. +The user-added **Service** modules — capability bridges the device provides or consumes, added and removed at runtime in the `Services` container (the core-domain twin of the light domain's `Effects`/`Drivers`). Fixed device infrastructure (identity, network, inspection tools) lives under **System** — see [core/system.md](system.md). Every row links to its generated technical page (the full API, from the `.h`) and its tests. ### Services -The top-level container the Service modules hang under — a grouping node with no controls of its own, the same shape as `Layers`/`Drivers` in the light domain. Adds/removes its children (Audio, IR) at runtime via the generic module machinery. +The top-level container the Service modules hang under — a grouping node with no controls of its own, the same shape as `Effects`/`Drivers` in the light domain. Adds/removes its children (Audio, IR) at runtime via the generic module machinery. Detail: [technical](moxygen/Services.md) diff --git a/docs/moonmodules/light/supporting.md b/docs/moonmodules/light/supporting.md index a60bbb45..fe3992d2 100644 --- a/docs/moonmodules/light/supporting.md +++ b/docs/moonmodules/light/supporting.md @@ -18,13 +18,13 @@ Detail: [technical](moxygen/Layer.md) -### Layers +### Effects The container of layers — composites them (blend mode + opacity per layer) into the final light buffer. -Layers container +Effects container -Detail: [technical](moxygen/Layers.md) +Detail: [technical](moxygen/Effects.md) [Tests](../../tests/unit-tests.md#layers) diff --git a/docs/usecases/build-your-own-moonmodules.md b/docs/usecases/build-your-own-moonmodules.md index 733343ca..2a84a262 100644 --- a/docs/usecases/build-your-own-moonmodules.md +++ b/docs/usecases/build-your-own-moonmodules.md @@ -23,7 +23,7 @@ A projectMM light show is a small tree of MoonModules: ``` Layouts → where the LEDs are in space (a Grid, a sphere, a strip) -Layers → a stack of images being drawn +Effects → a stack of images being drawn Layer → one image, built by… Effect → draws color into the image (the fun part) Modifier → bends/masks/repeats the image @@ -319,7 +319,7 @@ You get all of that "release the pin on disable" behaviour by implementing the s ## What to read next - **The effects catalog:** [docs/moonmodules/light/effects.md](../moonmodules/light/effects.md) — every shipped effect, with screenshots and controls. The best source of copy-and-tweak starting points. -- **The architecture doc:** [docs/architecture.md](../architecture.md) — the render pipeline (Layouts → Layers → Effects/Modifiers → Drivers) and the hot-path rules (why we avoid heap and floats inside `tick()`). +- **The architecture doc:** [docs/architecture.md](../architecture.md) — the render pipeline (Layouts → Effects → Effects/Modifiers → Drivers) and the hot-path rules (why we avoid heap and floats inside `tick()`). - **Coding standards:** [docs/coding-standards.md](../coding-standards.md) — the house style (header-only light modules, `constexpr`, naming) so your module reads like the rest. - **The real modules:** the smallest ones make the best teachers — `RainbowEffect` (a clean loop), `GameOfLifeEffect` (the memory lifecycle), `GridLayout` (`forEachCoord`). diff --git a/docs/usecases/home-automation.md b/docs/usecases/home-automation.md index 26301d02..a378d348 100644 --- a/docs/usecases/home-automation.md +++ b/docs/usecases/home-automation.md @@ -108,7 +108,7 @@ Because Hue is a rate-limited HTTP hub (~10 commands/s), this is **smooth ambien To set it up: -1. **Add a Hue driver.** In the device's web UI pipeline (**Layers → a Layer → its Drivers**), add a **Hue** driver. Enter your bridge's IP in `bridgeIp` (find it in the Hue app, or at [discovery.meethue.com](https://discovery.meethue.com)). +1. **Add a Hue driver.** In the device's web UI pipeline (**Effects → a Layer → its Drivers**), add a **Hue** driver. Enter your bridge's IP in `bridgeIp` (find it in the Hue app, or at [discovery.meethue.com](https://discovery.meethue.com)). 2. **Pair with the bridge.** Press the physical **link button** on the Hue bridge, then click the driver's **`pair`** button within ~30 seconds. The device claims an app key (stored on the driver as `appKey`) — a one-time step; the status line reports `paired, N lights`. 3. **Pick what it drives.** The driver lists the bridge's color-capable, reachable bulbs and its rooms; use the `room` / `light` controls to aim the effect at all bulbs, one room, or a single light. Each selected bulb becomes one pixel of the driver's window. 4. **Run an effect.** Any effect on the layer now drives the bulbs — the global brightness slider and color-order correction apply to them just like a physical strip (brightness 0 turns a bulb off). diff --git a/src/core/ControlModule.h b/src/core/ControlModule.h index 07cda3eb..59e86c8d 100644 --- a/src/core/ControlModule.h +++ b/src/core/ControlModule.h @@ -21,7 +21,7 @@ namespace mm { /// generic — MoonLight's presets carry effects and modifiers only, ours carry whichever top-level /// subtrees the user chose to capture. /// -/// Top-level by necessity rather than convention: a preset reaches ACROSS Layouts, Layers, Drivers +/// Top-level by necessity rather than convention: a preset reaches ACROSS Layouts, Effects, Drivers /// and Services, so this module cannot be a child of any of them. /// /// **Not to be confused with `LightPresetsModule`**, which despite the name is a different thing: a @@ -39,7 +39,7 @@ namespace mm { /// The `capture` controls choose which top-level subtrees a save includes, and the file records the /// choice, so applying one is never a surprise about what it will touch. /// -/// That choice is what decides **portability**. A preset capturing `Layers` alone is a look: effects, +/// That choice is what decides **portability**. A preset capturing `Effects` alone is a look: effects, /// modifiers, their settings, and nothing about the hardware — it applies on any board and drives /// whatever that board has. Adding `Drivers` makes it a device snapshot that carries pin maps and /// lane counts, which is what you want for cloning a board and NOT what you want for sharing a look. @@ -70,7 +70,7 @@ class ControlModule : public MoonModule, public ListSource { static constexpr uint8_t kMaxNameLen = 32; /// The top-level subtrees a preset can carry. Names are `typeName()`s, which is what the file /// records and what `Scheduler` resolves them back to. - static constexpr const char* kCapturable[] = {"Layouts", "Layers", "Drivers", "Services"}; + static constexpr const char* kCapturable[] = {"Layouts", "Effects", "Drivers", "Services"}; /// The role each capturable subtree holds, so a pad can show what a preset covers with the same /// emoji the module cards use (ROLE_EMOJI in the UI): one vocabulary rather than a second set /// invented here. Index-aligned with kCapturable. @@ -79,10 +79,10 @@ class ControlModule : public MoonModule, public ListSource { static_assert(sizeof(kCapturable) / sizeof(kCapturable[0]) == sizeof(kCaptureRole) / sizeof(kCaptureRole[0]), "kCapturable and kCaptureRole are index-aligned"); - /// Index of "Layers" within kCapturable — the role a pure look occupies. - static constexpr uint8_t kLayersRole = 1; - static_assert(kCapturable[kLayersRole][0] == 'L' && kCapturable[kLayersRole][1] == 'a' && - kCapturable[kLayersRole][5] == 's', "kLayersRole must index Layers"); + /// Index of "Effects" within kCapturable — the role a pure look occupies. + static constexpr uint8_t kEffectsRole = 1; + static_assert(kCapturable[kEffectsRole][0] == 'E' && kCapturable[kEffectsRole][1] == 'f' && + kCapturable[kEffectsRole][6] == 's', "kEffectsRole must index Effects"); /// How many faders the bank shows. Fixed for now; the surfaces we will map onto this have 8 /// (X-Touch) or 9 (nanoKONTROL), so the count becomes a control once a second surface needs it. @@ -207,7 +207,7 @@ class ControlModule : public MoonModule, public ListSource { // ---- Presets as an external surface (Home Assistant, and any future consumer) -------------- // - // A preset carrying ONLY Layers is a look: it changes what the lights show and nothing else. One + // A preset carrying ONLY Effects is a look: it changes what the lights show and nothing else. One // that also carries Drivers or Layouts rewires pins or geometry, which must not be reachable from // a voice assistant or an automation that thinks it is picking a colour scheme. These two calls // are the whole seam a publisher needs, so no consumer has to learn the file format or the @@ -224,8 +224,8 @@ class ControlModule : public MoonModule, public ListSource { return n == 1 ? found : kCaptureCount; } - /// Is this preset a pure look? With one role per preset this is simply "its role is Layers". - bool isLookOnly(uint8_t row) const { return roleOf(row) == kLayersRole; } + /// Is this preset a pure look? With one role per preset this is simply "its role is Effects". + bool isLookOnly(uint8_t row) const { return roleOf(row) == kEffectsRole; } /// The preset's name, or null for an out-of-range row. const char* presetName(uint8_t row) const { @@ -254,7 +254,7 @@ class ControlModule : public MoonModule, public ListSource { /// The look applied most recently, or "" when none is. Reports the LAYER role's holder, since a /// look is by definition what occupies that role. - const char* currentLook() const { return current_[kLayersRole]; } + const char* currentLook() const { return current_[kEffectsRole]; } bool isEditableList() const override { return true; } @@ -687,7 +687,7 @@ class ControlModule : public MoonModule, public ListSource { } /// Is `type` in the comma-separated `captures` header? Whole-token match, so "Layer" never - /// matches "Layers". + /// matches "Effects". static bool listHas(const char* list, const char* type) { const size_t tlen = std::strlen(type); for (const char* p = list; *p;) { @@ -740,7 +740,7 @@ class ControlModule : public MoonModule, public ListSource { /// Which ONE subtree the next save captures, as an index into kCapturable. A preset carries /// exactly one role: "this preset is a look" is a thing a user can hold in their head, where /// "a look and a geometry, lit for one and superseded for the other" is not. - uint8_t captureRole_ = kLayersRole; // a look, by default + uint8_t captureRole_ = kEffectsRole; // a look, by default /// Which preset currently holds each capturable role, index-aligned with kCapturable. A /// preset that carries layout+layer claims both, so applying a layer-only preset afterwards /// replaces the layer holder and leaves the layout one lit. That is what a mixed preset means diff --git a/src/core/FilesystemModule.h b/src/core/FilesystemModule.h index 6d8e85ad..2675e47c 100644 --- a/src/core/FilesystemModule.h +++ b/src/core/FilesystemModule.h @@ -137,7 +137,7 @@ class FilesystemModule : public MoonModule { /// caller that stores a subtree elsewhere (ControlModule's presets, one file per named preset) /// gets the persistence format for free rather than growing a second serializer that could /// drift from this one. Emits the enclosing `{}`; returns false only on allocation failure. - /// `prefix` namespaces every key ("Layers.0.type"), so several subtrees can share one flat + /// `prefix` namespaces every key ("Effects.0.type"), so several subtrees can share one flat /// object and `applySubtree` reads each back with the same prefix. Empty for a bare subtree. bool saveSubtreeTo(MoonModule* m, JsonSink& sink, const char* prefix = ""); diff --git a/src/core/HttpServerModule.cpp b/src/core/HttpServerModule.cpp index a9f0d096..b037b35a 100644 --- a/src/core/HttpServerModule.cpp +++ b/src/core/HttpServerModule.cpp @@ -1643,7 +1643,7 @@ HttpServerModule::OpResult HttpServerModule::applyAddModule( char* outName, size_t outNameLen) { if (!typeName || typeName[0] == 0) return OpResult::BadRequest; - // Top-level modules (Layouts/Layers/Drivers/Filesystem/System/Network/HttpServer) + // Top-level modules (Layouts/Effects/Drivers/Filesystem/System/Network/HttpServer) // are policy-fixed and wired in main.cpp at boot. Only *child* adds are allowed — // anything else would orphan the module (never ticked, leaked). if (!parentId || parentId[0] == 0) return OpResult::BadRequest; @@ -1805,7 +1805,7 @@ void HttpServerModule::handleDeleteModule(platform::TcpConnection& conn, const c return; } - // Top-level modules (Layouts/Layers/Drivers/Filesystem/System/Network/HttpServer) + // Top-level modules (Layouts/Effects/Drivers/Filesystem/System/Network/HttpServer) // have no parent — they're registered via Scheduler::addModule in main.cpp and the // top-level shape is policy-fixed. Reject the delete here instead of release+delete'ing // a module that the scheduler still holds a pointer to (which would dangle on next tick). diff --git a/src/core/ModuleFactory.h b/src/core/ModuleFactory.h index f5367300..ab7fbb60 100644 --- a/src/core/ModuleFactory.h +++ b/src/core/ModuleFactory.h @@ -109,7 +109,7 @@ class ModuleFactory { // Driver → strip "Driver" (PreviewDriver → Preview) // Generic → strip "Module" (FilesystemModule → Filesystem) // Layer → no suffix (the class is literally named "Layer") - // Names without the suffix are returned unchanged (Layouts, Layers, Drivers). + // Names without the suffix are returned unchanged (Layouts, Effects, Drivers). static const char* displayNameFor(const char* typeName, ModuleRole role) { const char* suffix = ""; switch (role) { diff --git a/src/core/MoonModule.h b/src/core/MoonModule.h index c06dc30c..3deaa4a2 100644 --- a/src/core/MoonModule.h +++ b/src/core/MoonModule.h @@ -17,7 +17,7 @@ namespace mm { /// Read-vs-write is NOT a role distinction — direction is a per-module decision, not a role split — /// so one role spans the category, justified by that named roster, not one member (core grows /// slower than the domain, see CLAUDE.md). Services is the core-domain twin of the light domain's -/// `Layers`/`Drivers`: a top-level container of user-added children of one role. +/// `Effects`/`Drivers`: a top-level container of user-added children of one role. enum class ModuleRole : uint8_t { Generic, Effect, Modifier, Driver, Layout, Layer, Service }; /// Lowercase role name for JSON/API output. Single source of truth so the role diff --git a/src/core/Scheduler.cpp b/src/core/Scheduler.cpp index 4e88fcb8..32d48ba9 100644 --- a/src/core/Scheduler.cpp +++ b/src/core/Scheduler.cpp @@ -154,7 +154,7 @@ void Scheduler::deleteTree(MoonModule* mod) { // Why this exists: ModuleFactory::create gives every freshly-created module // a display name derived from its type ("NoiseEffect" → "Noise", "Layer" -// stays "Layer"). When the user adds two Layers, both factory-default to +// stays "Layer"). When the user adds two Effects, both factory-default to // "Layer"; the HTTP API uses names as parent_id, and findModuleByName does // a first-match DFS, so the second Layer becomes unreachable. Same problem // happens when persistence rebuilds the tree positionally on boot. diff --git a/src/core/Services.h b/src/core/Services.h index a94805fd..15018a4d 100644 --- a/src/core/Services.h +++ b/src/core/Services.h @@ -6,7 +6,7 @@ namespace mm { /// Top-level container for the user-added **Service** modules — capability bridges the device /// provides or consumes (Audio, IR): optional, per-board, added and removed at runtime. It is the -/// core-domain twin of the light domain's `Layers`/`Drivers`: a top-level container holding +/// core-domain twin of the light domain's `Effects`/`Drivers`: a top-level container holding /// user-added children of a single role, so the generic add/replace/delete/persistence machinery /// applies unchanged. Fixed device infrastructure (Tasks, I2cScan, Network, …) lives under /// `System`, not here — Services is exactly the mutable half of that split. diff --git a/src/light/drivers/Drivers.h b/src/light/drivers/Drivers.h index 33c111eb..283d21db 100644 --- a/src/light/drivers/Drivers.h +++ b/src/light/drivers/Drivers.h @@ -5,7 +5,7 @@ #include "core/ActiveInstance.h" // the summary-seat election (the seat + its RAII vacate) #include "light/layers/Buffer.h" #include "light/layers/Layer.h" -#include "light/layers/Layers.h" +#include "light/layers/Effects.h" #include "light/layers/BlendMap.h" #include "light/drivers/Correction.h" #include "light/Palette.h" // the global active palette + its select control @@ -24,7 +24,7 @@ namespace mm { /// **Naming convention.** Capital `Drivers` is the container class; lowercase /// "driver"/"drivers" is the English singular/plural for individual `DriverBase` /// children. Capitalisation disambiguates "the Drivers container" from "two drivers -/// running" (same rule for `Layouts`/layout and `Layers`/layer). +/// running" (same rule for `Layouts`/layout and `Effects`/layer). /// /// **Shared output buffer.** Necessary because blend+map writes to arbitrary physical /// positions via LUT — the output is not filled sequentially, so a driver cannot read @@ -35,7 +35,7 @@ namespace mm { /// (the zero-copy fast path, at the cost of parallelism). /// /// **Multi-layer composition.** When two or more layers are enabled, Drivers composites -/// them into the shared output buffer each frame in Layers container order (bottom→top, +/// them into the shared output buffer each frame in Effects container order (bottom→top, /// via `forEachEnabledLayer`). The bottom layer clears and overwrites the buffer; each /// layer above blends onto the accumulated frame per its own `blendMode` and `opacity` /// (the inert per-Layer controls). Drivers owns the orchestration because only it sees @@ -68,7 +68,7 @@ namespace mm { /// **Prior art:** MoonLight's PhysicalLayer — owns `channelsD` (display buffer), /// `compositeLayers()` maps virtualChannels → channelsD, parallelism via a semaphore /// (driver signals completion, compositor writes) -/// (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Layers/PhysicalLayer.h). +/// (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Effects/PhysicalLayer.h). /// @card Drivers.png class Drivers : public MoonModule { public: @@ -176,18 +176,18 @@ class Drivers : public MoonModule { uint8_t palette = 0; // Two ways to wire the source Layer: - // - setLayers(Layers*): bind the container; layer_ is re-resolved from + // - setEffects(Effects*): bind the container; layer_ is re-resolved from // activeLayer() at every prepareTree. This makes the link self-healing — // a Layer cleared and rebuilt via the API (clear_children + add_module) // is picked up on the next prepareTree without re-running main.cpp wiring. // - setLayer(Layer*): pin a specific Layer directly (test rigs that build a - // Layer outside a Layers container). Skips re-resolution. - void setLayers(Layers* layers) { - layers_ = layers; - if (layers_) layer_ = layers_->activeLayer(); + // Layer outside a Effects container). Skips re-resolution. + void setEffects(Effects* layers) { + effects_ = layers; + if (effects_) layer_ = effects_->activeLayer(); } void setLayer(Layer* layer) { - layers_ = nullptr; // explicit pin overrides container resolution + effects_ = nullptr; // explicit pin overrides container resolution layer_ = layer; } @@ -283,8 +283,8 @@ class Drivers : public MoonModule { void prepare() override { // Re-resolve the active Layer from the bound container so a Layer that // was cleared and rebuilt via the API is picked up here (self-healing). - // setLayer() pins a Layer directly and leaves layers_ null — skip then. - if (layers_) layer_ = layers_->activeLayer(); + // setLayer() pins a Layer directly and leaves effects_ null — skip then. + if (effects_) layer_ = effects_->activeLayer(); // The output (composition) buffer is needed when we must blend into a // physical-space buffer rather than hand a driver a Layer's logical buffer // directly: whenever ≥2 layers composite, OR a single layer has a LUT @@ -300,10 +300,10 @@ class Drivers : public MoonModule { // fallback activeLayer() may return (which exists only so geometry stays // queryable while every layer is toggled off). With no enabled layer there // is nothing to emit, so no output buffer — drivers go idle (see - // passBufferToDrivers). A pinned setLayer() (layers_ null) is always treated + // passBufferToDrivers). A pinned setLayer() (effects_ null) is always treated // as the live source. - Layer* const out = layers_ ? layers_->firstEnabledLayer() : layer_; - const uint8_t enabled = layers_ ? layers_->enabledLayerCount() : (layer_ ? 1 : 0); + Layer* const out = effects_ ? effects_->firstEnabledLayer() : layer_; + const uint8_t enabled = effects_ ? effects_->enabledLayerCount() : (layer_ ? 1 : 0); const bool needOutput = out && (enabled > 1 || out->lut().hasLUT()); // The render↔encode split wants an outputBuffer_ EVEN in the identity case (a lone no-LUT @@ -377,7 +377,7 @@ class Drivers : public MoonModule { bool firstOutputRgb(uint8_t out[3]) const override { const Buffer* src = nullptr; if (outputBuffer_.data()) src = &outputBuffer_; - else if (Layer* l = layers_ ? layers_->firstEnabledLayer() : layer_; l && l->buffer().data()) + else if (Layer* l = effects_ ? effects_->firstEnabledLayer() : layer_; l && l->buffer().data()) src = &l->buffer(); if (!src || src->count() == 0 || src->channelsPerLight() < 3) return false; const uint8_t* p = src->data(); @@ -408,9 +408,9 @@ class Drivers : public MoonModule { // The single-layer source, resolved ONCE: both single-layer branches below need the same // value, and declaring it per-branch in an if-init shadowed the outer one (MSVC C4456 — // legitimately: two `Layer* out` in one chain reads as a bug even when it isn't). - Layer* srcLayer = layers_ ? layers_->firstEnabledLayer() : layer_; + Layer* srcLayer = effects_ ? effects_->firstEnabledLayer() : layer_; - if (outputBuffer_.data() && layers_ && layers_->enabledLayerCount() > 1) { + if (outputBuffer_.data() && effects_ && effects_->enabledLayerCount() > 1) { // Multi-layer composite: blend each enabled layer in container order. // The first (bottom) layer clears + overwrites; each subsequent layer // blends onto the accumulated frame per its own blendMode + opacity. @@ -418,7 +418,7 @@ class Drivers : public MoonModule { // specialized loop each — no-LUT layers blend 1:1, LUT layers map), // and a full-opacity additive/overwrite layer pays no alpha math, so // cost scales with enabled-layer count only. - layers_->forEachEnabledLayer([&](Layer* L, bool first) { + effects_->forEachEnabledLayer([&](Layer* L, bool first) { BlendOp op = first ? BlendOp::Overwrite : L->blendOp(); uint8_t op_opacity = first ? 255 : L->opacity; blendMap(L->buffer(), outputBuffer_, L->lut(), L->channelsPerLight(), @@ -478,7 +478,7 @@ class Drivers : public MoonModule { void quiesce() override { if (!quiesceEncode()) stopEncodeTask(); } private: - Layers* layers_ = nullptr; // bound container; layer_ re-resolved from it at prepareTree + Effects* effects_ = nullptr; // bound container; layer_ re-resolved from it at prepareTree Layer* layer_ = nullptr; Buffer outputBuffer_; @@ -626,8 +626,8 @@ class Drivers : public MoonModule { // while the split encodes from outputBuffer_ would output a stale frame. // The source is the first *enabled* layer, never the disabled fallback activeLayer() returns // when all layers are off — with no enabled layer buf stays null and every driver idles (its - // last frame is not re-sent). A pinned setLayer() (layers_ null) is always the live source. - Layer* const out = layers_ ? layers_->firstEnabledLayer() : layer_; + // last frame is not re-sent). A pinned setLayer() (effects_ null) is always the live source. + Layer* const out = effects_ ? effects_->firstEnabledLayer() : layer_; Buffer* buf = out ? (outputBuffer_.data() ? &outputBuffer_ : &out->buffer()) : nullptr; for (uint8_t i = 0; i < childCount(); i++) { diff --git a/src/light/effects/EffectBase.h b/src/light/effects/EffectBase.h index cce9715e..6eb90ea8 100644 --- a/src/light/effects/EffectBase.h +++ b/src/light/effects/EffectBase.h @@ -66,7 +66,7 @@ class Layer; // forward declaration (defined in light/layers/Layer.h, included a /// `layer->width()/height()/depth()` directly via the VirtualLayer pointer (no separate /// EffectBase), buffer access via `layer->virtualChannels`, time via `timeMicros()` /// (https://github.com/ewowi/MoonLight/blob/main/src/MoonBase/Nodes.h, -/// https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Layers/VirtualLayer.h). +/// https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Effects/VirtualLayer.h). class EffectBase : public MoonModule { public: ModuleRole role() const MM_NONBLOCKING override { return ModuleRole::Effect; } diff --git a/src/light/layers/Buffer.h b/src/light/layers/Buffer.h index 7f923e11..6396b3a3 100644 --- a/src/light/layers/Buffer.h +++ b/src/light/layers/Buffer.h @@ -25,7 +25,7 @@ namespace mm { /// /// **Prior art:** MoonLight's `VirtualLayer.virtualChannels` — a raw `uint8_t*` sized /// by `channelsPerLight * nrOfLights`, RGB/RGBW/DMX via LightsHeader offsets -/// (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Layers/VirtualLayer.h). +/// (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Effects/VirtualLayer.h). class Buffer { public: Buffer() = default; diff --git a/src/light/layers/Layers.h b/src/light/layers/Effects.h similarity index 84% rename from src/light/layers/Layers.h rename to src/light/layers/Effects.h index 4b2032a2..d9035b1c 100644 --- a/src/light/layers/Layers.h +++ b/src/light/layers/Effects.h @@ -11,11 +11,11 @@ namespace mm { /// /// **Why a container:** multi-layer composition (alpha-blend, additive, layered overlays) needs a place to walk every layer in order so drivers can merge their buffers before consuming the result. With one child Layer this is a thin pass-through: tick() runs the child Layer's tick() in order; behaviour matches the single-Layer pipeline byte-for-byte (Drivers takes its single-layer fast path). /// -/// **No buffer of its own:** each Layer owns its buffer and the `Drivers` container owns the composited output. Layers wires the shared `Layouts` into every child so each can size its buffer. Two queries serve the Drivers compositor: `activeLayer` (the first enabled child, or a disabled one as fallback) answers physical dimensions and feeds the single-layer fast path, and `forEachEnabledLayer` walks the enabled children in container order (bottom→top) marking the bottom layer that clears the buffer. `enabledLayerCount` lets Drivers pick the fast path (one enabled layer → hand its buffer straight to the driver) versus the composite path (≥2 → blend into the output buffer). +/// **No buffer of its own:** each Layer owns its buffer and the `Drivers` container owns the composited output. Effects wires the shared `Layouts` into every child so each can size its buffer. Two queries serve the Drivers compositor: `activeLayer` (the first enabled child, or a disabled one as fallback) answers physical dimensions and feeds the single-layer fast path, and `forEachEnabledLayer` walks the enabled children in container order (bottom→top) marking the bottom layer that clears the buffer. `enabledLayerCount` lets Drivers pick the fast path (one enabled layer → hand its buffer straight to the driver) versus the composite path (≥2 → blend into the output buffer). /// -/// **Prior art:** MoonLight's `PhysicalLayer` runs N `VirtualLayer`s and composites their buffers into the display channel — same idea, different shape: Drivers (not Layers) does the compositing here (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight). -/// @card Layers.png -class Layers : public MoonModule { +/// **Prior art:** MoonLight's `PhysicalLayer` runs N `VirtualLayer`s and composites their buffers into the display channel — same idea, different shape: Drivers (not Effects) does the compositing here (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight). +/// @card Effects.png +class Effects : public MoonModule { public: const char* acceptsChildRoles() const override { return "layer"; } @@ -42,10 +42,10 @@ class Layers : public MoonModule { setLayouts(layouts_); } - /// Role-filtered loop propagation: only tick children that are Layers. - /// The factory / UI shouldn't allow non-Layer children of a Layers + /// Role-filtered loop propagation: only tick children that are Effects. + /// The factory / UI shouldn't allow non-Layer children of a Effects /// container, but if one slips in (test fixture, hand-crafted config), - /// ticking it through Layers would run its loop at the wrong tree + /// ticking it through Effects would run its loop at the wrong tree /// depth (an Effect that should be ticked inside a Layer). Matches /// the role-filter precedent in setLayouts / activeLayer above. void tick() MM_NONBLOCKING override { @@ -100,7 +100,7 @@ class Layers : public MoonModule { return n; } - /// Walk enabled Layers in container (composition) order — the order Drivers + /// Walk enabled Effects in container (composition) order — the order Drivers /// blends them, bottom (first) to top (last). `` `cb(layer, isFirst)` ``: isFirst /// marks the bottom layer (clears the buffer; the rest blend onto it). template diff --git a/src/light/layers/Layer.h b/src/light/layers/Layer.h index d4e42aa1..dc3d0b08 100644 --- a/src/light/layers/Layer.h +++ b/src/light/layers/Layer.h @@ -14,7 +14,7 @@ namespace mm { -/// A `Layer` MoonModule (role `ModuleRole::Layer`, child of the `Layers` container) owns a buffer, a mapping LUT, an ordered effect list, and an ordered modifier list, and references the shared `Layouts` that describes the physical topology. +/// A `Layer` MoonModule (role `ModuleRole::Layer`, child of the `Effects` container) owns a buffer, a mapping LUT, an ordered effect list, and an ordered modifier list, and references the shared `Layouts` that describes the physical topology. /// /// **Ownership:** a `Buffer` (logical light data, sized to the logical box); a `MappingLUT` (logical lights to physical positions); effects (write lights into the buffer, dynamic heap-grown list, no fixed max); modifiers (transform the LUT or light values, same dynamic list). /// @@ -30,7 +30,7 @@ namespace mm { /// /// **Status:** the status slot shows the LOGICAL box the effects render into (`` `××` ``), which can differ from the physical box shown on `Layouts` (a Mirror-XY modifier folds a 128×128 physical layout into a 64×64 logical box). The same slot carries memory-degradation warnings when a build can't fit (`modifier mapping skipped`, `buffer reduced`, `buffer allocation failed`, all `— not enough memory`), and a warning wins over the neutral box line. /// -/// **Prior art:** MoonLight's `VirtualLayer` — `oneToOneMapping` fast-path flag, `virtualChannels` per-layer buffer, `effectDimension`, a `nodes` vector for effects/modifiers, and `forEachLight` per-logical-light iteration that asks the modifier for physical destinations (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Layers/VirtualLayer.h). +/// **Prior art:** MoonLight's `VirtualLayer` — `oneToOneMapping` fast-path flag, `virtualChannels` per-layer buffer, `effectDimension`, a `nodes` vector for effects/modifiers, and `forEachLight` per-logical-light iteration that asks the modifier for physical destinations (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Effects/VirtualLayer.h). /// @card Layer.png class Layer : public MoonModule { public: diff --git a/src/light/layers/MappingLUT.h b/src/light/layers/MappingLUT.h index fa5fcc62..dd8d38f1 100644 --- a/src/light/layers/MappingLUT.h +++ b/src/light/layers/MappingLUT.h @@ -15,7 +15,7 @@ namespace mm { /// /// **Paged destinations (no-PSRAM fragmentation fallback):** the destinations array can be large for a many-to-one modifier on a big grid (a 128×128 XY mirror → 32768 entries × 2 B ≈ 64 KB). On a no-PSRAM ESP32 the largest *contiguous* free block can be smaller than that even when total free heap is fine — a fragmentation cliff, not exhaustion. So when a single block won't allocate but total heap allows it, destinations are split into fixed-size power-of-two PAGES that each fit a fragmented heap. Paging is the exception, not the rule: PSRAM boards (alloc is PSRAM-first → one huge block) and every no-PSRAM case where the single block fits keep the flat single array and the flat hot-path walk, byte-identical to a non-paged build. Only the one failing config (no-PSRAM + large grid + fragmented heap) pages, where the alternative is the modifier silently degrading to 1:1. `offsets_` is always a single small allocation; output is identical either way, so paging is purely an allocation detail. /// -/// **Prior art:** MoonLight's `PhysMap` — a memory-optimal union (2 B no-PSRAM / 4 B PSRAM) with the map type stored in each entry, `oneToOneMapping` / `allOneLight` fast-path flags, and `forEachLightIndex` for 1:N iteration (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Layers/PhysMap.h). projectMM renames `oneToOneMapping` → `setIdentity` / `!hasLUT` for the reason above. +/// **Prior art:** MoonLight's `PhysMap` — a memory-optimal union (2 B no-PSRAM / 4 B PSRAM) with the map type stored in each entry, `oneToOneMapping` / `allOneLight` fast-path flags, and `forEachLightIndex` for 1:N iteration (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight/Effects/PhysMap.h). projectMM renames `oneToOneMapping` → `setIdentity` / `!hasLUT` for the reason above. class MappingLUT { public: MappingLUT() = default; diff --git a/src/light/layouts/Layouts.h b/src/light/layouts/Layouts.h index 2bc5473d..103790c5 100644 --- a/src/light/layouts/Layouts.h +++ b/src/light/layouts/Layouts.h @@ -9,7 +9,7 @@ namespace mm { -/// Top-level container for one or more `LayoutBase` children — it defines the physical light topology of the installation and is shared by every Layer in the `Layers` container (one Layouts describing the physical setup, multiple Layers render into it). +/// Top-level container for one or more `LayoutBase` children — it defines the physical light topology of the installation and is shared by every Layer in the `Effects` container (one Layouts describing the physical setup, multiple Effects render into it). /// /// **Coordinate iteration is owned by the container, not the layer:** `forEachCoord` walks every enabled child layout's coordinates in registration order, offsetting physical indices so multiple layouts (for example 16 strips making one panel) stitch into a single flat physical address space without overlap. A Layer *uses* those coordinates to build its LUT. `totalLightCount` (the sum across enabled children) sizes both the layer buffer and the driver output buffer. /// @@ -25,7 +25,7 @@ class Layouts : public MoonModule { /// Sum of `lightCount` across enabled children — sizes the layer buffer and the /// driver output buffer. Disabled children are skipped, the same gate - /// Layer/Layers/Drivers apply to their children. Indices of subsequent enabled + /// Layer/Effects/Drivers apply to their children. Indices of subsequent enabled /// layouts shift down to close the gap — disable Layout A and Layout B's lights /// move to indices 0..N. Users who need a stable index-to-fixture mapping disable /// the driver, not the layout. diff --git a/src/main.cpp b/src/main.cpp index 0cd05132..da647361 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ #include "core/Scheduler.h" -#include "light/layers/Layers.h" +#include "light/layers/Effects.h" #include "light/layouts/GridLayout.h" #include "light/layouts/GridBlacksLayout.h" #include "light/layouts/SphereLayout.h" @@ -155,7 +155,7 @@ static void registerModuleTypes() { // core modules keep a per-module page named for the type. // Containers mm::ModuleFactory::registerType("Layouts", "light/supporting.md#layouts"); - mm::ModuleFactory::registerType("Layers", "light/supporting.md#layers"); + mm::ModuleFactory::registerType("Effects", "light/supporting.md#layers"); mm::ModuleFactory::registerType("Layer", "light/supporting.md#layer"); mm::ModuleFactory::registerType("Drivers", "light/supporting.md#drivers"); mm::ModuleFactory::registerType("LightPresetsModule", "light/supporting.md#lightpresets"); @@ -363,12 +363,12 @@ void mm_main(volatile bool& keepRunning, uint16_t httpPort) { systemModule->addChild(pinsModule); // Services — top-level container for user-added capability modules (Audio, IR). - // The core-domain twin of the light domain's Layers/Drivers: a grouping node + // The core-domain twin of the light domain's Effects/Drivers: a grouping node // whose children the user adds/removes at runtime. Added as a root below. auto* servicesModule = static_cast(mm::ModuleFactory::create("Services")); // ControlModule — puts the device into a named state. Top-level rather than a Services child - // because a preset reaches ACROSS Layouts/Layers/Drivers/Services, so it cannot live inside one + // because a preset reaches ACROSS Layouts/Effects/Drivers/Services, so it cannot live inside one // of them. Boot-wired: presets are a device capability, not something a user adds. auto* controlModule = static_cast(mm::ModuleFactory::create("ControlModule")); @@ -449,14 +449,14 @@ void mm_main(volatile bool& keepRunning, uint16_t httpPort) { auto* grid = static_cast(mm::ModuleFactory::create("GridLayout")); layouts->addChild(grid); - // Layers: top-level container; one or more layers, each rendering + // Effects: top-level container; one or more layers, each rendering // into its own buffer. Today one Layer with one effect + one modifier. - auto* layersContainer = static_cast(mm::ModuleFactory::create("Layers")); + auto* effectsContainer = static_cast(mm::ModuleFactory::create("Effects")); auto* layer = static_cast(mm::ModuleFactory::create("Layer")); layer->setChannelsPerLight(3); - layersContainer->addChild(layer); + effectsContainer->addChild(layer); // setLayouts wires the shared Layouts to the container AND propagates to every child Layer. - layersContainer->setLayouts(layouts); + effectsContainer->setLayouts(layouts); // One default effect so a bare device (no catalog inject) still shows lights out // of the box — but NO default modifier: the boot Layer is just an effect on a @@ -466,13 +466,13 @@ void mm_main(volatile bool& keepRunning, uint16_t httpPort) { layer->addChild(noise); // Drivers: top-level container; one or more Driver children. Bound to the - // Layers container — Drivers re-resolves the active Layer from it at every + // Effects container — Drivers re-resolves the active Layer from it at every // prepareTree, so a Layer cleared+rebuilt via the API self-heals without // re-running this wiring. Binding the container (not a single Layer) is what // lets a driver read across N Layer buffers from one place — the hook // multi-layer blending uses. auto* drivers = static_cast(mm::ModuleFactory::create("Drivers")); - drivers->setLayers(layersContainer); + drivers->setEffects(effectsContainer); // Output drivers (NetworkSend + the LED drivers: RMT / LCD_CAM / Parlio) are // NOT boot-wired. They are added explicitly per board through the catalog @@ -488,7 +488,7 @@ void mm_main(volatile bool& keepRunning, uint16_t httpPort) { // PreviewDriver is the one driver that stays boot-wired: it needs the HTTP // server's WS broadcaster (set below, once httpServer exists), a reference only // main.cpp has and the catalog can't supply. It reads the active Layer (resolved - // by the Drivers container's setLayers above) for the light positions and the + // by the Drivers container's setEffects above) for the light positions and the // sparse buffer it streams; it owns its own scratch buffers. // The light-preset library: a boot-wired singleton under Drivers (child role `preset`). It owns // the named channel-role wirings every driver references by id; exactly one exists, so drivers @@ -529,7 +529,7 @@ void mm_main(volatile bool& keepRunning, uint16_t httpPort) { // network is up), services (the user-added-capability container: Audio, IR — placed after // network because a service may use it, e.g. WLED audio sync, and before the light pipeline // so a capability like audio is available to the effects that consume it), light pipeline - // (Layouts → Layers → Drivers), then HTTP. The Scheduler walks roots in this order each + // (Layouts → Effects → Drivers), then HTTP. The Scheduler walks roots in this order each // tick; child propagation happens inside each root. scheduler.addModule(filesystemModule); scheduler.addModule(systemModule); @@ -552,7 +552,7 @@ void mm_main(volatile bool& keepRunning, uint16_t httpPort) { scheduler.addModule(servicesModule); scheduler.addModule(controlModule); scheduler.addModule(layouts); - scheduler.addModule(layersContainer); + scheduler.addModule(effectsContainer); scheduler.addModule(drivers); scheduler.addModule(httpServer); diff --git a/src/ui/app.js b/src/ui/app.js index fd03fd9c..c8d76567 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -884,7 +884,7 @@ function renderChildTabs(mod, childrenEl, depth) { addTab.title = "add " + rolesAcceptedBy(mod).join(" / "); addTab.addEventListener("click", () => { // THIS card's own footer — a plain querySelector would match the first .card-footer in the - // subtree, which belongs to a nested child's card (Layers would then offer the Layer's + // subtree, which belongs to a nested child's card (Effects would then offer the Layer's // effects instead of another layer). Scope to direct children of this card. const card = childrenEl.parentElement; const footer = [...card.children].find(el => el.classList.contains("card-footer")); @@ -1026,7 +1026,7 @@ function createCard(mod, depth) { card.appendChild(title); // -- Controls -- - // Child-hosting modules deeper in the tree (Layers, Layer, Drivers, Layouts) + // Child-hosting modules deeper in the tree (Effects, Layer, Drivers, Layouts) // collapse their own controls so the children are the focus by default. // Modules that merely host a code-wired child (Network → Improv) keep their // controls expanded — the parent's settings are the main point, the code-wired @@ -1388,7 +1388,7 @@ function allAcceptedChildRoles() { // // We test mod.role against the UNION of all containers' acceptsChildRoles, not // against this module's specific parent. That's exact while the role→container -// mapping is 1:1 (effect→Layer, driver→Drivers, layout→Layouts, layer→Layers) — +// mapping is 1:1 (effect→Layer, driver→Drivers, layout→Layouts, layer→Effects) — // a child of an add-accepted role is always under the one container that // accepts it. If a role ever becomes accepted by more than one container, this // would need the parent threaded in to scope the check to the actual parent. @@ -2142,7 +2142,7 @@ function buildCaptureToggles(body, moduleName) { const ctrl = mod && (mod.controls || []).find(c => c.name === "captures"); if (!ctrl) return; const names = Array.isArray(ctrl.options) && ctrl.options.length - ? ctrl.options : ["Layouts", "Layers", "Drivers", "Services"]; + ? ctrl.options : ["Layouts", "Effects", "Drivers", "Services"]; const wrap = document.createElement("div"); wrap.className = "surface-popup-captures"; names.forEach((n, i) => { @@ -3135,10 +3135,10 @@ function syncVisibleControls(mod) { const card = document.querySelector(`.card[data-module="${cssEscape(mod.name)}"]`); if (!card) return false; // The controls host is THIS card's own collapse wrapper — must be a DIRECT - // child (`:scope >`), not any descendant: a container card (e.g. Layers) nests + // child (`:scope >`), not any descendant: a container card (e.g. Effects) nests // its child cards (Layer) inside .card-children, and a plain // `card.querySelector(".card-controls-collapse")` would reach down and match - // the CHILD's wrapper. That made Layers adopt Layer's control rows as its own, + // the CHILD's wrapper. That made Effects adopt Layer's control rows as its own, // so both cards saw a control-set mismatch every WS frame and rebuilt each // other's rows in a loop — tearing down (and closing) any open