From e6f0abeb4de54ade8dd0b2c977f486054a229102 Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 24 Aug 2026 11:11:55 +0200 Subject: [PATCH] A module declares every control with addControl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit addUint8, addUint16, addInt16, addInt32 and addBool become one overloaded addControl: the widget follows the variable's own type, which the compiler already knows, so the name no longer repeats a width the declaration states. It is the same call a MoonLive script makes, so someone who has written a script can read a compiled module and the other way round. Performance: desktop 132us idle, esp32 2,151us. Nothing on a tick path moved; a renamed call produces a byte-identical descriptor. **Core** - five addControl overloads on uint8_t, uint16_t, int16_t, int32_t and bool, bodies unchanged from the adders they replace. The per-type defaults DIFFER deliberately — each is its own type's full range, so a call omitting min/max still means "no UI constraint" — and unifying them would silently move the bounds of the 24 sites that rely on them - addControl on an int8_t is DELETED, with a diagnostic naming addPin and addReadOnlyInt. An int8_t is a GPIO or telemetry, never a small number, and deducing a Pin from the type would make any future small signed control register as a claimed GPIO in PinsModule's scan - the widget-specific adders keep their names: addPin, addSelect, addPalette, addText, addTextArea, addFilePath, addPassword, addIPv4, addReadOnly, addReadOnlyInt, addProgress, addList, addButton. They name a widget rather than a width, and that intent is not recoverable from the C++ type — uint8_t backs a slider, a dropdown AND a palette picker **Light domain / everywhere** - 384 call sites across 102 files renamed. A non-const lvalue reference binds only to its exact type, so a site that compiles produces the widget it always did; with the old names removed, a missed site is a compile error rather than a silent change **Scripts** - the NEW-SCRIPT templates still used uint8_t and addUint8, so every script created from the card failed to compile. The MoonLive type migration missed them **MoonDeck** - the spec check's range-drift regex matched add(Uint8|Int16|Uint16) and would have matched NOTHING after the rename: green while checking zero controls. Migrated and control-checked — it finds 216 **Tests** - addControl binds the widget its member's type calls for; a call without a range gets that type's full range; an explicit range reaches the descriptor. The deleted int8_t overload is its own test, verified by compiling a probe that must fail **Docs** - the contributor tutorial, testing.md and MoonLiveLayout.md; MIGRATING says *nothing* for a device and *recompile* for a third-party module - comments and backlog snippets now describe the API as it IS: a forward-looking doc telling an implementer to call addUint8 would be telling them to write something that does not compile Co-Authored-By: Claude Fable 5 --- docs/MIGRATING.md | 30 +++++++ docs/backlog/backlog-core.md | 4 +- .../backlog/livescripts-analysis-bottom-up.md | 4 +- docs/backlog/livescripts-analysis-top-down.md | 12 +-- docs/metrics/repo-health.json | 40 +++++----- docs/metrics/repo-health.md | 32 ++++---- docs/moonmodules/light/MoonLiveLayout.md | 2 +- docs/testing.md | 2 +- docs/usecases/build-your-own-moonmodules.md | 12 +-- moondeck/check/check_specs.py | 7 +- moondeck/moonlive/emit_isa.cpp | 2 +- src/core/AudioService.h | 8 +- src/core/Control.h | 58 ++++++++++---- src/core/ControlModule.h | 6 +- src/core/FileManagerModule.cpp | 2 +- src/core/HttpServerModule.cpp | 2 +- src/core/MqttModule.cpp | 4 +- src/core/NetworkModule.h | 12 +-- src/core/SystemModule.h | 2 +- src/core/moonlive/MoonLiveBuiltins.h | 4 +- src/core/moonlive/MoonLiveCompiler.cpp | 8 +- src/core/moonlive/MoonLiveIr.h | 4 +- src/core/moonlive/moonlive_emit.h | 13 ++- src/light/drivers/DriverBase.h | 6 +- src/light/drivers/Drivers.h | 6 +- src/light/drivers/MoonLedDriver.h | 14 ++-- src/light/drivers/NetworkSendDriver.h | 4 +- src/light/drivers/PanelCardDriver.h | 2 +- src/light/drivers/ParallelLedDriver.h | 12 +-- src/light/drivers/PreviewDriver.h | 4 +- src/light/drivers/RmtLedDriver.h | 4 +- src/light/effects/AudioVolumeEffect.h | 2 +- src/light/effects/BallpitEffect.h | 12 +-- src/light/effects/BlurzEffect.h | 8 +- src/light/effects/BouncingBallsEffect.h | 4 +- src/light/effects/DemoReelEffect.h | 8 +- src/light/effects/DissolveEffect.h | 8 +- src/light/effects/DistortionWavesEffect.h | 6 +- src/light/effects/EchoEffect.h | 10 +-- src/light/effects/FireEffect.h | 4 +- src/light/effects/FireworksEffect.h | 14 ++-- src/light/effects/FixedRectangleEffect.h | 22 ++--- src/light/effects/FreqMatrixEffect.h | 12 +-- src/light/effects/FreqSawsEffect.h | 12 +-- src/light/effects/GEQ3DEffect.h | 12 +-- src/light/effects/GEQEffect.h | 8 +- src/light/effects/GameOfLifeEffect.h | 22 ++--- src/light/effects/LavaLampEffect.h | 6 +- src/light/effects/LinesEffect.h | 6 +- src/light/effects/LissajousEffect.h | 6 +- src/light/effects/MetaballsEffect.h | 8 +- src/light/effects/NetworkReceiveEffect.h | 4 +- src/light/effects/Noise2DEffect.h | 4 +- src/light/effects/NoiseEffect.h | 4 +- src/light/effects/NoiseMeterEffect.h | 4 +- src/light/effects/PaintBrushEffect.h | 12 +-- src/light/effects/ParticlesEffect.h | 8 +- src/light/effects/PlasmaEffect.h | 8 +- src/light/effects/PolarNoiseEffect.h | 12 +-- src/light/effects/PraxisEffect.h | 14 ++-- src/light/effects/RainbowEffect.h | 2 +- src/light/effects/RandomEffect.h | 2 +- src/light/effects/RaymarchEffect.h | 10 +-- src/light/effects/RingsEffect.h | 8 +- src/light/effects/RipplesEffect.h | 4 +- src/light/effects/RubiksCubeEffect.h | 8 +- src/light/effects/SdfShapesEffect.h | 12 +-- src/light/effects/SineEffect.h | 6 +- src/light/effects/SolidEffect.h | 14 ++-- src/light/effects/SpectrumEffect.h | 10 +-- src/light/effects/SphereMoveEffect.h | 2 +- src/light/effects/SpiralEffect.h | 6 +- src/light/effects/StarFieldEffect.h | 8 +- src/light/effects/StarSkyEffect.h | 6 +- src/light/effects/TetrixEffect.h | 6 +- src/light/effects/TextEffect.h | 6 +- src/light/effects/TruchetEffect.h | 12 +-- src/light/effects/TunnelEffect.h | 12 +-- src/light/effects/VectorBallsEffect.h | 10 +-- src/light/effects/WaterRippleEffect.h | 14 ++-- src/light/effects/WaveEffect.h | 4 +- src/light/layers/Layer.h | 2 +- src/light/layouts/CarLightsLayout.h | 2 +- src/light/layouts/CubeLayout.h | 18 ++--- src/light/layouts/GridBlacksLayout.h | 12 +-- src/light/layouts/GridLayout.h | 8 +- src/light/layouts/HumanSizedCubeLayout.h | 6 +- src/light/layouts/PanelLayout.h | 10 +-- src/light/layouts/PanelsLayout.h | 20 ++--- src/light/layouts/RingLayout.h | 10 +-- src/light/layouts/Rings241Layout.h | 2 +- src/light/layouts/SingleColumnLayout.h | 8 +- src/light/layouts/SingleRowLayout.h | 8 +- src/light/layouts/SphereLayout.h | 2 +- src/light/layouts/SpiralLayout.h | 6 +- src/light/layouts/TorontoBarGourdsLayout.h | 2 +- src/light/layouts/TubesLayout.h | 8 +- src/light/layouts/WheelLayout.h | 4 +- src/light/modifiers/CheckerboardModifier.h | 4 +- src/light/modifiers/MirrorModifier.h | 6 +- src/light/modifiers/MultiplyModifier.h | 12 +-- src/light/modifiers/PinwheelModifier.h | 12 +-- src/light/modifiers/RandomMapModifier.h | 2 +- src/light/modifiers/RegionModifier.h | 12 +-- src/light/modifiers/RippleXZModifier.h | 6 +- src/light/modifiers/RotateModifier.h | 2 +- src/light/modifiers/TransposeModifier.h | 12 +-- src/light/moonlive/MoonLiveBuiltins_light.h | 12 ++- src/light/moonlive/MoonLiveEffect.h | 2 +- src/light/moonlive/MoonLiveLayout.h | 2 +- src/light/moonlive/MoonLiveScript.h | 6 +- src/light/moonlive/MoonLiveScriptFile.h | 12 +-- src/platform/desktop/moonlive_asm_x86_64.cpp | 2 +- test/python/test_check_specs_drift.py | 12 +-- test/unit/core/moonlive_device_codegen.inc | 2 +- .../core/unit_Control_apply_absent_key.cpp | 8 +- test/unit/core/unit_Control_int32.cpp | 80 ++++++++++++++++++- .../unit_FilesystemModule_persistence.cpp | 6 +- .../unit/core/unit_HttpServerModule_apply.cpp | 8 +- test/unit/core/unit_IrService.cpp | 4 +- test/unit/core/unit_MoonModule.cpp | 10 +-- test/unit/core/unit_MqttModule.cpp | 6 +- .../unit/core/unit_NetworkModule_ethernet.cpp | 6 +- test/unit/core/unit_moonlive_compiler.cpp | 6 +- test/unit/core/unit_moonlive_fill.cpp | 6 +- test/unit/light/unit_MoonLiveLayout.cpp | 6 +- test/unit/light/unit_MoonLiveModifier.cpp | 6 +- test/unit/light/unit_MoonLiveScripts.cpp | 19 ++--- .../light/unit_ParallelLedDriver_ring.cpp | 2 +- 129 files changed, 649 insertions(+), 514 deletions(-) diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 7713d1e1..2c7af47c 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -22,6 +22,36 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul ## Unreleased (`next-iteration`) +### A module declares every control with `addControl` (2026-08-24) + +`addUint8`, `addUint16`, `addInt16`, `addInt32` and `addBool` are replaced by one overloaded +`addControl(name, variable, min, max)`. The widget follows the variable's own type, which the +compiler already knows, so the name no longer repeats a width the declaration states: + +```cpp +controls_.addUint8("speed", speed_, 1, 255); // before +controls_.addControl("speed", speed_, 1, 255); // after +``` + +This is the same call a MoonLive script makes, which is the point: someone who has written a +script can read a compiled module, and someone who has read a module can write a script. + +The **widget-specific** adders keep their names — `addPin`, `addSelect`, `addPalette`, `addText`, +`addTextArea`, `addFilePath`, `addPassword`, `addIPv4`, `addReadOnly`, `addReadOnlyInt`, +`addProgress`, `addList`, `addButton`. Those name a widget rather than a width, and the intent is +not recoverable from the C++ type: `uint8_t` backs a slider, a dropdown *and* a palette picker, and +an `int8_t` silently becoming a Pin would register as a claimed GPIO in the pin map. `addControl` +on an `int8_t` is deliberately deleted, with a diagnostic naming the two real options. + +**Action: *nothing* for a device.** No control name, type, range, wire format or persisted value +changes — a renamed call produces a byte-identical descriptor, which is why nothing on the device +can notice. + +**Action for a third-party module: *recompile*.** Rename the five calls to `addControl`; the +arguments are unchanged. A missed one is a compile error, never a silent behaviour change: the +overloads bind by exact reference type, so a call that compiles produces the widget it always did. + + ### Desktop settings move to a per-user directory (2026-08-23) The desktop build wrote its configuration to `build/.config`, resolved against whatever directory the process happened to start in. That is a source-checkout layout, and it shipped: a downloaded binary either could not write there at all, failing every save and logging one line per save, or it wrote settings that belonged to that *folder* rather than to the user, so moving the executable lost them. diff --git a/docs/backlog/backlog-core.md b/docs/backlog/backlog-core.md index 2fcca1c2..e1d070cb 100644 --- a/docs/backlog/backlog-core.md +++ b/docs/backlog/backlog-core.md @@ -65,10 +65,10 @@ Forward-looking to-build items for the **core / infrastructure** domain (`src/co second, uint8 and uint16). Only the re-read after a slot write is still wrong. **Reproduce** (in `unit_moonlive_fill.cpp`, which has `kCtrlTable`/`kSys` to hand): compile - `class T { uint16_t big = 5; defineControls() { addUint16("big", big, 0, 1000); } tick() { + `class T { int big = 5; defineControls() { addControl("big", big, 0, 1000); } tick() { setRGB(0, big, 0, 0); } }`, `run(..., kEntryTick)` → 5, write the slot to 7, run again → still 5. - **Impact: desktop only, nothing ships broken.** `addUint16` is hardware-verified on both ISAs + **Impact: desktop only, nothing ships broken.** A wide control is hardware-verified on both ISAs (S3 Xtensa and S31 RISC-V drive ember's `cycle` to 2000 and back). What is missing is DESKTOP coverage of the live-edit loop — the path users touch most — so no host test can pin it and the next regression there would surface only on a board. Add the runtime assertion together with the diff --git a/docs/backlog/livescripts-analysis-bottom-up.md b/docs/backlog/livescripts-analysis-bottom-up.md index 8f6ef63f..86607f2b 100644 --- a/docs/backlog/livescripts-analysis-bottom-up.md +++ b/docs/backlog/livescripts-analysis-bottom-up.md @@ -223,8 +223,8 @@ Decisions from the design discussion that produced this survey. These are *direc 1. **Execution = native, the standout.** Build our own **native-codegen** engine (ESPLiveScript-class speed, near-100%) — the differentiator; projectMM should stand out with something not done before (a native live-compiler bound to a real module system). *Not* a slow interpreter, *not* WASM-as-flagship. 2. **No dead-ends, start small + beautiful (the LED-driver method).** Ship **one ISA backend first — Xtensa (classic ESP32 + S3)** — as a complete, blazing first increment, then grow. The **IR seam** (front-end → typed IR → per-ISA backend) is the no-dead-end guarantee: RISC-V (P4), ARM (Teensy), x86/ARM64 (desktop) each become a *new backend behind the unchanged IR*, never a rewrite. WASM/WAMR is the **per-target fallback** so no target is ever blocked. 3. **The IR must NOT cost speed (hard constraint).** It is a *compile-time* representation that lowers to the *same* native instructions ESPLiveScript hand-emits — **zero per-pixel runtime overhead**, no interpreted layer. Equally fast as hpwit on Xtensa is non-negotiable; prove it by diffing generated instructions for a hot loop against hand-written Xtensa, and keep an `__asm__` escape hatch for the very hottest paths (as ESPLiveScript has). -4. **Source language = a C-subset, "as close as possible" to the precompiled effect, with pragmatic simplifications.** The effect *body* (types like `uint8_t`/`uint32_t`/`lengthType`, nested `for`, integer + 64-bit math, `static_cast`, `RGB`, `hsvToRgb`, buffer writes) ports **near-verbatim** from a file like `RipplesEffect.h` (our reference effect — it exercises the hard cases: `float` trig `std::sqrt`/`std::sin`/`std::floor`, `std::memset`, 3D with `depth()`, two controls). The C++ *file/class ceremony* that buys nothing in a script (`#pragma`/`#include`/`namespace`, and — accepted as a pragmatic simplification — `class : public EffectBase`/`override`/the `controls_.addUint8` host-object dance) is **dropped or lightened**: the engine supplies the module scaffolding around the script. Target: porting an existing effect is the loop body verbatim + a handful of lines changed, *not* a rewrite, and *not* implementing a full C++ object model (inheritance/vtables/host-method binding) in the engine. **Not** a JS-subset (the ARTI-FX surface): JS's double-everything number model is the slow path *and* further from our C++ codebase, so it's worse on both speed and portability of existing effects. -5. **Controls = minimal ceremony.** A scripted control is a near-plain top-level variable (e.g. `uint8_t speed = 60;` with a range annotation); the engine derives the MoonModule control + UI + persistence. Lighter than today's explicit `controls_.addUint8(...)`, copy-paste-friendly. (Exact annotation syntax is the top-down's call.) +4. **Source language = a C-subset, "as close as possible" to the precompiled effect, with pragmatic simplifications.** The effect *body* (types like `uint8_t`/`uint32_t`/`lengthType`, nested `for`, integer + 64-bit math, `static_cast`, `RGB`, `hsvToRgb`, buffer writes) ports **near-verbatim** from a file like `RipplesEffect.h` (our reference effect — it exercises the hard cases: `float` trig `std::sqrt`/`std::sin`/`std::floor`, `std::memset`, 3D with `depth()`, two controls). The C++ *file/class ceremony* that buys nothing in a script (`#pragma`/`#include`/`namespace`, and — accepted as a pragmatic simplification — `class : public EffectBase`/`override`/the `controls_.addControl` host-object dance) is **dropped or lightened**: the engine supplies the module scaffolding around the script. Target: porting an existing effect is the loop body verbatim + a handful of lines changed, *not* a rewrite, and *not* implementing a full C++ object model (inheritance/vtables/host-method binding) in the engine. **Not** a JS-subset (the ARTI-FX surface): JS's double-everything number model is the slow path *and* further from our C++ codebase, so it's worse on both speed and portability of existing effects. +5. **Controls = minimal ceremony.** A scripted control is a near-plain top-level variable (e.g. `uint8_t speed = 60;` with a range annotation); the engine derives the MoonModule control + UI + persistence. Lighter than today's explicit `controls_.addControl(...)`, copy-paste-friendly. (Exact annotation syntax is the top-down's call.) 6. **Safety = staged, climb the tiers, don't pay upfront.** Ship the **cheap** tier first — array **bounds-checking** (a compare-branch per indexed access, low single-digit %, removable in a trusted/fast mode) + **watchdog / instruction budget** (kill a runaway loop, near-free). The **expensive** true-memory-sandbox tier (a script physically can't touch memory outside its arena — what WASM gives free, native can't cheaply) is **deferred**, reachable via the IR→WASM fallback only if a public script editor in the field shows the cheap tier isn't enough. Decided this way because the price of full sandboxing upfront isn't worth paying before evidence demands it. 7. **MoonModule-first.** A scripted module **is** a MoonModule (role, controls, `loop()`, generic UI, lifecycle, robustness, live-reconfig). The script ⇄ MoonModule binding (reach the `Buffer`/`AudioFrame`/LUT via the producer/consumer pull pattern, no copy) is the projectMM value-add to design — no prior art copies cleanly. 8. **General in core + specific in light.** One engine serves a domain-neutral core script (e.g. transform sensor data) *and* a scripted layout / effect / modifier / driver. **Effect is the first role.** `RipplesEffect.h` is the *reference* effect for the language design (it stresses float trig + 3D + memset), but it is **too complex for the hello-world spike** — the first running script must be trivial (e.g. fill the buffer one color, or a single moving dot), proving the engine end-to-end before any real effect. Ripples is the *graduation* target, not the spike. For how an effect is structured for a newcomer, the [MoonLight effects tutorial](https://moonmodules.org/MoonLight/moonlight/effects-tutorial/) is a good read (a sibling project's step-by-step). The simple→Ripples progression is itself the start-small-grow staging applied to the demo. diff --git a/docs/backlog/livescripts-analysis-top-down.md b/docs/backlog/livescripts-analysis-top-down.md index 5898678b..97d29d5b 100644 --- a/docs/backlog/livescripts-analysis-top-down.md +++ b/docs/backlog/livescripts-analysis-top-down.md @@ -142,7 +142,7 @@ The same shape gives `MoonLiveLayout` (role `Layout`, emits coordinates), `MoonL So the binding overrides the same hooks any compiled module does; the only difference is that each one delegates to the compiled `MoonLive` instead of hand-written C++. -**Crucially, all of these lifecycle methods live in the *binding* (`MoonLiveEffect`, `src/light/moonlive/`), not in the engine.** `onBuildControls`/`onBuildState`/`onUpdate`/`teardown`, `EffectBase`, `ModuleRole`, `controls_` — every projectMM type — sit on the binding side of the §3.9 seam. The engine (`MoonLive`, `src/core/moonlive/`) sees none of them; the binding reaches it only through a **neutral public API**: `compile(source)`, `run()`, `free()`, `declaredControls()` → a plain list of `{name, type, min, max, default}` structs the engine owns, and `allocForSize(w, h, d)` → plain ints. The binding *translates* — it reads the engine's neutral `declaredControls()` and calls projectMM's `controls_.addUint8(...)`; it maps a grid resize to `allocForSize`. **The engine never takes a `ControlList`, a `Buffer`, or any projectMM type** — so the rich MoonModule lifecycle is entirely a property of the binding, and the engine stays the domain-neutral core §3.9 describes. (This is the seam working as intended: a different host writes its own binding with its own lifecycle against the same neutral engine API.) +**Crucially, all of these lifecycle methods live in the *binding* (`MoonLiveEffect`, `src/light/moonlive/`), not in the engine.** `onBuildControls`/`onBuildState`/`onUpdate`/`teardown`, `EffectBase`, `ModuleRole`, `controls_` — every projectMM type — sit on the binding side of the §3.9 seam. The engine (`MoonLive`, `src/core/moonlive/`) sees none of them; the binding reaches it only through a **neutral public API**: `compile(source)`, `run()`, `free()`, `declaredControls()` → a plain list of `{name, type, min, max, default}` structs the engine owns, and `allocForSize(w, h, d)` → plain ints. The binding *translates* — it reads the engine's neutral `declaredControls()` and calls projectMM's `controls_.addControl(...)`; it maps a grid resize to `allocForSize`. **The engine never takes a `ControlList`, a `Buffer`, or any projectMM type** — so the rich MoonModule lifecycle is entirely a property of the binding, and the engine stays the domain-neutral core §3.9 describes. (This is the seam working as intended: a different host writes its own binding with its own lifecycle against the same neutral engine API.) ### 3.4 The host binding — script ⇄ MoonModule (decision 7, the value-add) @@ -160,11 +160,11 @@ The binding is generated *around* the script body — the script never writes `# A control is a near-plain top-level variable with a range annotation; the engine derives the `MoonModule` control + UI + persistence: ```c -uint8_t speed = 50; // @control 0..99 → controls_.addUint8("speed", …, 0, 99) +byte speed = 50; // @control 0..99 → controls_.addControl("speed", …, 0, 99) uint8_t interval = 128; // @control 1..254 ``` -The front-end collects annotated top-level vars during parsing and the engine exposes them as a neutral `declaredControls()` list (`{name, type, min, max, default}` — no projectMM type); the *binding* reads that list and calls the normal `controls_.add(...)` the rest of projectMM uses (§3.3) — so a scripted control is indistinguishable from a compiled one in the UI, persistence, and the live-reconfig sweep, while the engine stays projectMM-agnostic. Lighter than today's explicit `onBuildControls` + `addUint8` (the engine writes that for you), and copy-paste-friendly: the `uint8_t speed = 50;` line is *already* how RipplesEffect.h declares it. (Exact annotation syntax — `@control`, a trailing comment convention, or a `slider(0,99)` initializer — is settled in the spike; the principle is "declare the var, get the control".) +The front-end collects annotated top-level vars during parsing and the engine exposes them as a neutral `declaredControls()` list (`{name, type, min, max, default}` — no projectMM type); the *binding* reads that list and calls the normal `controls_.add(...)` the rest of projectMM uses (§3.3) — so a scripted control is indistinguishable from a compiled one in the UI, persistence, and the live-reconfig sweep, while the engine stays projectMM-agnostic. Lighter than today's explicit `onBuildControls` + `addControl` (the engine writes that for you), and copy-paste-friendly: the `uint8_t speed = 50;` line is *already* how RipplesEffect.h declares it. (Exact annotation syntax — `@control`, a trailing comment convention, or a `slider(0,99)` initializer — is settled in the spike; the principle is "declare the var, get the control".) ### 3.6 Live reconfig + tick-atomic hot-swap (decision: sync) @@ -239,7 +239,7 @@ A C-subset, not full C++, not JS. The type model is exactly what real effects us ### 5.2 What's dropped vs lightened (the pragmatic simplifications) - **Dropped** (file ceremony, zero value in a script): `#pragma once`, `#include`, `namespace`. The engine supplies the surrounding module. -- **Lightened** (the C++ object model): no `class : public EffectBase`, no `override`, no `controls_.addUint8(...)` host-object dance. The engine synthesizes the `MoonLiveEffect` wrapper (§3.3) around the script body; the role/`dimensions`/controls come from light annotations (§3.5) and the script's `loop()`. +- **Lightened** (the C++ object model): no `class : public EffectBase`, no `override`, no `controls_.addControl(...)` host-object dance. The engine synthesizes the `MoonLiveEffect` wrapper (§3.3) around the script body; the role/`dimensions`/controls come from light annotations (§3.5) and the script's `loop()`. - **Kept verbatim** (the part you iterate on): types, the `loop()` body, all the math, `static_cast`, `RGB c = hsvToRgb(...)`, the loops. **Why not full C++:** supporting `class`/inheritance/`override`/host-method-binding means implementing a C++ object model (vtables, member-reference binding) in the engine — build cost up front, and the object machinery is the very "object graph in the hot path" the architecture forbids. The wrapper has no runtime value; let the engine write it. @@ -259,8 +259,8 @@ class RipplesEffect : public EffectBase { // ← dropped (engine su uint8_t speed = 50; // ← kept (becomes a control) uint8_t interval = 128; void onBuildControls() override { // ← dropped (derived from the vars) - controls_.addUint8("speed", speed, 0, 99); - controls_.addUint8("interval", interval, 1, 254); + controls_.addControl("speed", speed, 0, 99); + controls_.addControl("interval", interval, 1, 254); } void loop() override { // ← KEPT VERBATIM (the body) uint8_t* buf = buffer(); … std::memset(buf, 0, nrOfLights()*cpl); diff --git a/docs/metrics/repo-health.json b/docs/metrics/repo-health.json index 32b9dea4..02ce0c68 100644 --- a/docs/metrics/repo-health.json +++ b/docs/metrics/repo-health.json @@ -1,8 +1,8 @@ { - "commit": "f985a366", + "commit": "6b9d520d", "flash": { - "esp32s3-n16r8": 1820752, - "desktop": 1231192, + "esp32s3-n16r8": 1821104, + "desktop": 1247704, "esp32": 1764416, "esp32p4rev1-eth": 1653696, "esp32p4rev1-eth-wifi": 1933472, @@ -16,8 +16,8 @@ }, "perf": { "desktop": { - "tick_us": 259, - "fps": 3861 + "tick_us": 131, + "fps": 7633 }, "esp32": { "tick_us": 2151, @@ -25,24 +25,24 @@ } }, "loc": { - "core": 20117, - "light": 25873, - "platform": 15128, + "core": 20194, + "light": 25878, + "platform": 15161, "ui": 7047, - "test": 46792, - "moondeck": 21847 + "test": 46933, + "moondeck": 21848 }, "comments": { "core": { - "lines": 7929, - "ratio": 0.426 + "lines": 7988, + "ratio": 0.428 }, "light": { - "lines": 10271, + "lines": 10275, "ratio": 0.438 }, "platform": { - "lines": 5379, + "lines": 5389, "ratio": 0.39 }, "ui": { @@ -50,24 +50,24 @@ "ratio": 0.282 }, "test": { - "lines": 8658, - "ratio": 0.212 + "lines": 8694, + "ratio": 0.213 }, "moondeck": { - "lines": 3529, + "lines": 3530, "ratio": 0.185 } }, "tests": { - "cases": 1565, + "cases": 1571, "scenarios": 23 }, "docs": { "md_files": 192, - "md_lines": 28089, + "md_lines": 28136, "plans_files": 98, "backlog_lines": 4451, - "lessons_lines": 592, + "lessons_lines": 606, "claude_md_lines": 136 }, "complexity": { diff --git a/docs/metrics/repo-health.md b/docs/metrics/repo-health.md index 66f32fe5..c9e2e05a 100644 --- a/docs/metrics/repo-health.md +++ b/docs/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `f985a366`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `6b9d520d`. 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,7 +8,7 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Flash | |---|---:| -| desktop | 1,202 KB | +| desktop | 1,218 KB (+16 KB) ⚠ | | esp32 | 1,723 KB | | esp32-16mb | 1,674 KB | | esp32-eth | 1,294 KB | @@ -16,7 +16,7 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | esp32p4rev1-eth | 1,615 KB | | esp32p4rev1-eth-wifi | 1,888 KB | | esp32p4rev3-eth | 1,605 KB | -| esp32s3-n16r8 | 1,778 KB (+1 KB) ⚠ | +| esp32s3-n16r8 | 1,778 KB (+0 KB) ⚠ | | esp32s3-n8r8 | 1,712 KB | | esp32s31 | 2,031 KB | | qemu | 1,287 KB | @@ -25,32 +25,32 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Tick | FPS | |---|---:|---:| -| desktop | 259 µs (+73 µs) ⚠ | 3,861 (−1,515) ⚠ | +| desktop | 131 µs (−128 µs) ✓ | 7,633 (+3,772) ✓ | | esp32 | 2,151 µs | 464 | ## Code | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 20,117 (+11) ⚠ | 7,929 | 42.6 % | -| light | 25,873 | 10,271 | 43.8 % | -| platform | 15,128 (+19) ⚠ | 5,379 | 39.0 % | -| ui | 7,047 (+14) ⚠ | 1,874 | 28.2 % (+0.1 %) ⚠ | -| test | 46,792 (+96) ⚠ | 8,658 | 21.2 % | -| moondeck | 21,847 | 3,529 | 18.5 % | +| core | 20,194 (+77) ⚠ | 7,988 | 42.8 % (+0.2 %) ⚠ | +| light | 25,878 (+5) ⚠ | 10,275 | 43.8 % | +| platform | 15,161 (+33) ⚠ | 5,389 | 39.0 % | +| ui | 7,047 | 1,874 | 28.2 % | +| test | 46,933 (+141) ⚠ | 8,694 | 21.3 % (+0.1 %) ⚠ | +| moondeck | 21,848 (+1) ⚠ | 3,530 | 18.5 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 1,565 (+4) ✓ | +| unit cases | 1,571 (+6) ✓ | | scenarios | 23 | ## Complexity | Metric | Value | |---|---:| -| functions | 2,745 (−2) ⚠ | +| functions | 2,745 | | over threshold | 168 | | worst CCN | 108 | @@ -58,10 +58,10 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Metric | Value | |---|---:| -| markdown files | 192 (+1) ⚠ | -| markdown lines | 28,089 (+108) ⚠ | -| plan files | 98 (+1) ⚠ | +| markdown files | 192 | +| markdown lines | 28,136 (+47) ⚠ | +| plan files | 98 | | backlog lines | 4,451 | -| lessons lines | 592 | +| lessons lines | 606 (+14) ⚠ | | CLAUDE.md lines | 136 | diff --git a/docs/moonmodules/light/MoonLiveLayout.md b/docs/moonmodules/light/MoonLiveLayout.md index ecb3c50f..89d68851 100644 --- a/docs/moonmodules/light/MoonLiveLayout.md +++ b/docs/moonmodules/light/MoonLiveLayout.md @@ -107,7 +107,7 @@ Past half full, the status also names the tightest limit the script is approachi |---|---| | `script` | the file name under `/moonlive/`; naming it (or re-naming it after an edit) recompiles and re-places the lights live | -Plus one control per `addUint8` / `addUint16` in the script's `defineControls()`. +Plus one control per `addControl` in the script's `defineControls()`. Editing any of them rebuilds the pipeline, because every one can change where the lights are. A script that fails to compile leaves a fixture with no lights, shows the parse error on the module, and the device keeps running. diff --git a/docs/testing.md b/docs/testing.md index 10fd270e..993e428d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -154,7 +154,7 @@ The JS suite proves the installer *chunks* an op correctly; the **device side th **`test/python/test_installer_manifests.py`** (pytest) — pins the web installer's per-release file contract. For every `ships: true` firmware in `web-installer/firmwares.json` it runs `moondeck/build/generate_manifest.py` (with a synthetic `flasher_args.json`, so no firmware build is needed) and asserts the manifest is valid (a `chipFamily` + non-empty `parts[]`) AND that **every part filename matches one of the globs the release workflow stages onto Pages** (`firmware-*.bin` / `shared-ota-data.bin` / `partition-table-*.bin`). A manifest that names a file outside those globs points at something the deploy never stages → the installer 404s at fetch-firmware (the failure that shipped a broken v2.0.0 installer). The test guards the manifest-generation ↔ staged-files contract; the *deploy mechanics* that stage them (per-tag, in `release.yml`) are workflow shell logic a unit test can't reach, so the two are complementary. -**`test/python/test_check_specs_drift.py`** (pytest) — pins the two spec-drift guards in `moondeck/check/check_specs.py` (the spec-check commit gate). Some facts live in both the `.h` and the module doc in different forms: a control's **numeric range** (`addUint8("floor", floor, 0, 255)` vs the prose "noise floor (0–255)") and the **author/source URL** (`// Author: … — ` vs the `Origin:` markdown link). Neither can be single-sourced — they're the same fact for two audiences — so instead the gate *validates* them: if the doc restates a control's range and it conflicts with the `.h`, or an `.h` author URL is missing from the doc, the spec check flags it. The checks are block-scoped on the consolidated catalog pages (a control name shared across modules, `fps`/`fadeRate`, matches only its own module's block), and tolerant of the human range spellings (`1–8` / `1-8` / `1 to 8`) — this suite pins both the catch and the no-false-alarm behaviour. +**`test/python/test_check_specs_drift.py`** (pytest) — pins the two spec-drift guards in `moondeck/check/check_specs.py` (the spec-check commit gate). Some facts live in both the `.h` and the module doc in different forms: a control's **numeric range** (`addControl("floor", floor, 0, 255)` vs the prose "noise floor (0–255)") and the **author/source URL** (`// Author: … — ` vs the `Origin:` markdown link). Neither can be single-sourced — they're the same fact for two audiences — so instead the gate *validates* them: if the doc restates a control's range and it conflicts with the `.h`, or an `.h` author URL is missing from the doc, the spec check flags it. The checks are block-scoped on the consolidated catalog pages (a control name shared across modules, `fps`/`fadeRate`, matches only its own module's block), and tolerant of the human range spellings (`1–8` / `1-8` / `1 to 8`) — this suite pins both the catch and the no-false-alarm behaviour. MoonDeck's pure logic (catalog reverse-lookup, state migration) and the installer's op-walk / storage are the next host-side candidates as they accrete regression risk. diff --git a/docs/usecases/build-your-own-moonmodules.md b/docs/usecases/build-your-own-moonmodules.md index 4337caea..3394afb6 100644 --- a/docs/usecases/build-your-own-moonmodules.md +++ b/docs/usecases/build-your-own-moonmodules.md @@ -77,7 +77,7 @@ public: // Declare the control so it shows up in the UI. Runs at startup. void defineControls() override { - controls_.addUint8("speed", speed, 1, 255); // name, variable, min, max + controls_.addControl("speed", speed, 1, 255); // name, variable, min, max } // Called every render tick. Draw the picture for "now". @@ -130,8 +130,8 @@ We build the moving `phase` from `elapsed()` (milliseconds since the show starte `speed` is an ordinary `uint8_t`. In `defineControls()` you tell the UI about it with one line, and from then on the framework keeps `speed` in sync with the on-screen slider. You read it in `tick()` like any variable. There are helpers for every common control: ```cpp -controls_.addUint8("brightness", brightness, 0, 255); // a 0–255 slider -controls_.addBool("mirror", mirror); // a checkbox +controls_.addControl("brightness", brightness, 0, 255); // a 0–255 slider +controls_.addControl("mirror", mirror); // a checkbox controls_.addSelect("mode", mode, kModeOptions, kModeCount); // a dropdown ``` @@ -274,8 +274,8 @@ public: lengthType height = 16; void defineControls() override { - controls_.addInt16("width", width, 1, 512); - controls_.addInt16("height", height, 1, 512); + controls_.addControl("width", width, 1, 512); + controls_.addControl("height", height, 1, 512); } nrOfLightsType lightCount() const override { return width * height; } @@ -309,7 +309,7 @@ You get all of that "release the pin on disable" behaviour by implementing the s ## A suggested classroom path 1. **Copy the rainbow.** Change the maths in `tick()` — make it pulse, or swap the palette lookup for a fixed color. Rebuild and add it in the UI. See it move. -2. **Add a control.** Give it a `brightness` slider (`addUint8`) and multiply your colors by it. Watch the UI wire itself up. +2. **Add a control.** Give it a `brightness` slider (`addControl`) and multiply your colors by it. Watch the UI wire itself up. 3. **Add memory.** Write an effect that keeps a per-pixel value between frames (a fading trail, a bouncing dot). Now you need `prepare()` + `release()` — practice matching them. 4. **Make it robust.** Resize the grid live to 1×1, then 0×0. Your effect must not crash. (Reading `width()`/`height()` each frame is what saves you.) 5. **Write a test.** Add `test/unit/light/unit_MyEffect.cpp` with one case: build a small grid, run a frame, `CHECK` the buffer got painted (and doesn't crash on 0×0). Run `ctest`. That's the habit — a module and its test travel together. diff --git a/moondeck/check/check_specs.py b/moondeck/check/check_specs.py index 7bf01d96..0961fe7d 100644 --- a/moondeck/check/check_specs.py +++ b/moondeck/check/check_specs.py @@ -164,10 +164,11 @@ def _match(ln): return "\n".join(lines[start:end]) -# Range-bearing control forms: addUint8/addInt16/addUint16("name", var, MIN, MAX). -# (addBool/addPin/addSelect/addText/addButton carry no numeric range — skipped.) +# Range-bearing control forms: addControl("name", var, MIN, MAX). +# (addPin/addSelect/addText/addButton and a bool addControl carry no numeric range — a call with +# no MIN/MAX simply does not match, which is what skips them.) _RANGE_CTRL_RE = re.compile( - r'controls_\.add(?:Uint8|Int16|Uint16)\("(?P\w+)"\s*,\s*\w+\s*,\s*' + r'controls_\.addControl\("(?P\w+)"\s*,\s*\w+\s*,\s*' r'(?P-?\d+)\s*,\s*(?P-?\d+)' ) # A numeric range as spelled in .md prose: 1–8 / 1-8 / 1 to 8 (en-dash, hyphen, or "to"). diff --git a/moondeck/moonlive/emit_isa.cpp b/moondeck/moonlive/emit_isa.cpp index ec325a12..273a70d2 100644 --- a/moondeck/moonlive/emit_isa.cpp +++ b/moondeck/moonlive/emit_isa.cpp @@ -84,7 +84,7 @@ int main(int argc, char** argv) { const auto sysvars = std::strcmp(binding, "modifier") == 0 ? moonlive::modifierSysVars() : std::strcmp(binding, "effect") == 0 ? moonlive::effectSysVars() : moonlive::layoutSysVars(); - // A string pool, as the engine supplies one: `addUint8("name", ...)` interns its label there + // A string pool, as the engine supplies one: `addControl("name", ...)` interns its label there // and the emitted code carries a pointer to it. Static so the pointers stay valid while the // bytes below are dumped. static char strings[moonlive::CompileResult::kStringPool]; diff --git a/src/core/AudioService.h b/src/core/AudioService.h index 35eb05cb..7d81a5c9 100644 --- a/src/core/AudioService.h +++ b/src/core/AudioService.h @@ -200,11 +200,11 @@ class AudioService : public MoonModule { controls_.addSelect("sampleRate", sampleRateSel, kRateOptions, kSampleRateCount); controls_.setHidden(controls_.count() - 1, !localMode); // floor/gain condition the local FFT/level mapping. - controls_.addUint8("floor", floor, 0, 255); controls_.setHidden(controls_.count() - 1, !localMode); - controls_.addUint8("gain", gain, 1, 255); controls_.setHidden(controls_.count() - 1, !localMode); + controls_.addControl("floor", floor, 0, 255); controls_.setHidden(controls_.count() - 1, !localMode); + controls_.addControl("gain", gain, 1, 255); controls_.setHidden(controls_.count() - 1, !localMode); // "send audio": broadcast the locally-analyzed frame. Only meaningful in Local mode. if constexpr (platform::hasNetwork) { - controls_.addBool("send audio", send); + controls_.addControl("send audio", send); controls_.setHidden(controls_.count() - 1, !localMode); } // --- Simulate group: the synthesized-pattern picker, shown only in Simulate mode. --- @@ -219,7 +219,7 @@ class AudioService : public MoonModule { // The UDP port — the Send destination and the Receive listen port. Defaults to WLED's // 11988 (interop with WLED/MoonLight); change it on BOTH ends to run a private // projectMM-only sync group on a non-WLED port. - controls_.addUint16("syncPort", syncPort, 1, 65535); + controls_.addControl("syncPort", syncPort, 1, 65535); controls_.setHidden(controls_.count() - 1, !hasSocket); controls_.addReadOnly("sync status", syncStr_, sizeof(syncStr_)); controls_.setHidden(controls_.count() - 1, !hasSocket); diff --git a/src/core/Control.h b/src/core/Control.h index e026f67c..855ceb21 100644 --- a/src/core/Control.h +++ b/src/core/Control.h @@ -377,42 +377,68 @@ class ControlList { ControlList(ControlList&&) = delete; ControlList& operator=(ControlList&&) = delete; + /// Bind a member as a control. ONE NAME for every numeric and boolean type: the widget + /// follows the variable's own type, which the compiler already knows, so a call cannot + /// disagree with the declaration and a contributor has one spelling to learn. + /// + /// This is deliberately the same vocabulary a MoonLive script uses — `addControl("speed", + /// speed, 0, 99)` reads identically in a script and in a compiled module, which is the point: + /// someone who has written one can read the other. + /// + /// A non-const lvalue reference binds only to its EXACT type, so overload selection is decided + /// by the variable alone. No conversion is considered, and no two overloads share a type. + /// + /// The WIDGET-specific adders keep their own names — addPin, addSelect, addPalette, addText, + /// addButton and the rest. They name a widget rather than a width, and that intent is not + /// recoverable from the C++ type: `uint8_t` backs a slider, a dropdown AND a palette picker, + /// and an `int8_t` that silently became a Pin would register as a claimed GPIO in PinsModule. + /// /// Bind a `uint8_t` as a 0–255 slider (the preferred default control). `min`/`max` /// bound the UI drag range and clamp writes server-side. - void addUint8(const char* name, uint8_t& var, uint8_t min = 0, uint8_t max = 255) { + void addControl(const char* name, uint8_t& var, uint8_t min = 0, uint8_t max = 255) { grow(); controls_[count_++] = {&var, name, 0, ControlType::Uint8, min, max}; } /// Bind a `uint16_t` as a number input. min/max default to the full type range - /// (no UI constraint); pass explicit bounds (e.g. `addUint16("sampleRate", r, - /// 8000, 48000)`) for a bounded slider + server-side write clamp — same contract - /// as addUint8/addInt16. - void addUint16(const char* name, uint16_t& var, - uint16_t min = 0, uint16_t max = UINT16_MAX) { + /// (no UI constraint); pass explicit bounds (e.g. `addControl("sampleRate", r, + /// 8000, 48000)`) for a bounded slider + server-side write clamp. + /// + /// The defaults DIFFER per overload, deliberately: each is its own type's full range, so a + /// call that omits them means "no UI constraint" whatever the type. Unifying them would + /// silently move a control's bounds. + void addControl(const char* name, uint16_t& var, + uint16_t min = 0, uint16_t max = UINT16_MAX) { grow(); controls_[count_++] = {&var, name, 0, ControlType::Uint16, min, max}; } // lengthType (int16_t) — signed wire format so negative values round-trip // correctly. min/max default to INT16_MIN/INT16_MAX (no UI constraint) when - // omitted; pass explicit bounds (e.g. addInt16("width", w, 1, 512)) to get a + // omitted; pass explicit bounds (e.g. addControl("width", w, 1, 512)) to get a // bounded slider in the UI and server-side clamping on write. - void addInt16(const char* name, int16_t& var, - int16_t min = INT16_MIN, int16_t max = INT16_MAX) { + void addControl(const char* name, int16_t& var, + int16_t min = INT16_MIN, int16_t max = INT16_MAX) { grow(); controls_[count_++] = {&var, name, 0, ControlType::Int16, min, max}; } /// Bind an `int32_t` where the value does not fit 16 bits. min/max default to the /// full type range (no UI constraint); pass explicit bounds for a bounded slider + - /// server-side write clamp — same contract as addInt16. - void addInt32(const char* name, int32_t& var, - int32_t min = INT32_MIN, int32_t max = INT32_MAX) { + /// server-side write clamp. + void addControl(const char* name, int32_t& var, + int32_t min = INT32_MIN, int32_t max = INT32_MAX) { grow(); controls_[count_++] = {&var, name, 0, ControlType::Int32, min, max}; } + /// An `int8_t` is NOT a control type here: it is either a GPIO (addPin, which PinsModule + /// scans for to collect claimed pins) or telemetry (addReadOnlyInt, which needs a unit). + /// Deducing one from the type would make any future small signed control register as a + /// claimed GPIO, so the caller says which. Deleted rather than absent, so the diagnostic + /// names the two real options instead of listing every unrelated overload. + void addControl(const char* name, int8_t& var, int16_t min = 0, int16_t max = 0) = delete; + // A GPIO pin number (int8_t storage — one byte; -1 = unused/default). A GPIO // never exceeds ~54 on any ESP32-family chip, so int8 (−128..127) is ample and // smaller than int16. Renders as a plain number input, not a slider (see @@ -429,7 +455,9 @@ class ControlList { controls_[count_++] = {&var, name, 0, ControlType::Pin, min, max}; } - void addBool(const char* name, bool& var) { + /// A toggle. No min/max: a bool's range is itself, which is why this overload takes three + /// arguments where the numeric ones take four. + void addControl(const char* name, bool& var) { grow(); controls_[count_++] = {&var, name, 0, ControlType::Bool, 0, 1}; } @@ -547,7 +575,7 @@ class ControlList { // A momentary action button (ControlType::Button). No backing storage — a click // POSTs through to the module's onControlChanged(name), which performs the action. Use - // for "do this now" (rescan, reset, self-test); use addBool for on/off state. + // for "do this now" (rescan, reset, self-test); use addControl on a bool for on/off state. void addButton(const char* name) { grow(); controls_[count_++] = {nullptr, name, 0, ControlType::Button, 0, 0}; @@ -588,7 +616,7 @@ class ControlList { // Ask the UI to render a numeric control as a plain number input, never a slider — for a value where // each integer is a discrete identity (a PHY/I2C address, a channel), not a magnitude to sweep. - // Typical use: addInt16()/addUint8() then setNumberField(count() - 1). See the descriptor's field. + // Typical use: addControl() then setNumberField(count() - 1). See the descriptor's field. void setNumberField(uint8_t i, bool numberField = true) { if (i < count_) controls_[i].numberField = numberField; } diff --git a/src/core/ControlModule.h b/src/core/ControlModule.h index a1523335..11d44eb7 100644 --- a/src/core/ControlModule.h +++ b/src/core/ControlModule.h @@ -96,7 +96,7 @@ class ControlModule : public MoonModule, public ListSource { // Encoders first: they sit ABOVE the pads on the surfaces this mirrors, and control order is // render order. for (uint8_t i = 0; i < kEncoderCount; i++) { - controls_.addUint8(kEncoderNames[i], encoders_[i]); + controls_.addControl(kEncoderNames[i], encoders_[i]); controls_.setEncoder(controls_.count() - 1, true, nullptr); } controls_.addList("presets", *this); @@ -104,7 +104,7 @@ class ControlModule : public MoonModule, public ListSource { // and is settable from anywhere the control system reaches — which is what a MIDI surface // will bind to later. Rendered as a bank of vertical sliders by the UI. for (uint8_t i = 0; i < kFaderCount; i++) { - controls_.addUint8(kFaderNames[i], faders_[i]); + controls_.addControl(kFaderNames[i], faders_[i]); controls_.setFader(controls_.count() - 1, true, faderTarget(i)); } // The save form. All HIDDEN: these are what the pad popup drives, not controls a user reads @@ -117,7 +117,7 @@ class ControlModule : public MoonModule, public ListSource { // The pad a save is aimed at: transient UI intent, reset at setup (see setup()), never a // restored value. The declared max is kMaxPresets-1, so a persisted or API-supplied kNoSlot // would clamp to the last pad; savePreset re-checks the range for the same reason. - controls_.addUint8("slot", saveSlot_, 0, kMaxPresets - 1); + controls_.addControl("slot", saveSlot_, 0, kMaxPresets - 1); controls_.setHidden(controls_.count() - 1, true); // One flag per capturable subtree rather than a single multi-select: the set is small and // fixed, and a checkbox each is what makes "what will this preset contain" readable at a diff --git a/src/core/FileManagerModule.cpp b/src/core/FileManagerModule.cpp index 10212537..e7db162f 100644 --- a/src/core/FileManagerModule.cpp +++ b/src/core/FileManagerModule.cpp @@ -11,7 +11,7 @@ void FileManagerModule::defineControls() { // mkdir/delete OPS are their own HTTP endpoints (POST/DELETE /api/dir?path=) — not persisted // controls — so a create/delete carries its path in the request, not in device storage. The // `show hidden` flag keeps it bound for the API while the generic control list skips it. - controls_.addBool("show hidden", showHidden_); // reveal dot-prefixed entries (e.g. .config) + controls_.addControl("show hidden", showHidden_); // reveal dot-prefixed entries (e.g. .config) controls_.setHidden(controls_.count() - 1, true); // Filesystem-usage gauge (used / total bytes), read from the platform. Shown below the tree in // the panel — the File Manager is where filesystem space is relevant, so it owns the control. diff --git a/src/core/HttpServerModule.cpp b/src/core/HttpServerModule.cpp index 3cd8d781..9be7bac1 100644 --- a/src/core/HttpServerModule.cpp +++ b/src/core/HttpServerModule.cpp @@ -35,7 +35,7 @@ namespace mm { void HttpServerModule::defineControls() { - controls_.addUint16("port", port); + controls_.addControl("port", port); } void HttpServerModule::setup() { diff --git a/src/core/MqttModule.cpp b/src/core/MqttModule.cpp index 334661c6..fc3c8d39 100644 --- a/src/core/MqttModule.cpp +++ b/src/core/MqttModule.cpp @@ -394,10 +394,10 @@ void MqttModule::release() { void MqttModule::defineControls() { controls_.addText("broker", broker_, sizeof(broker_)); - controls_.addUint16("port", port_, 1, 65535); + controls_.addControl("port", port_, 1, 65535); controls_.addText("username", username_, sizeof(username_)); controls_.addPassword("password", password_, sizeof(password_)); - controls_.addBool("haDiscovery", haDiscovery_); // announce a HA MQTT-discovery light (default off; WLED /json covers HA) + controls_.addControl("haDiscovery", haDiscovery_); // announce a HA MQTT-discovery light (default off; WLED /json covers HA) controls_.addReadOnly("mqtt_status", statusStr_, sizeof(statusStr_)); MoonModule::defineControls(); } diff --git a/src/core/NetworkModule.h b/src/core/NetworkModule.h index 48688666..1c2cf090 100644 --- a/src/core/NetworkModule.h +++ b/src/core/NetworkModule.h @@ -311,12 +311,12 @@ class NetworkModule : public MoonModule { // deviceModels.json catalog injects 8 dBm for brown-out-prone boards. // Hidden with the same radioOn gate as the txPower readout above — a WiFi // TX-power cap is meaningless on Ethernet / Idle where the radio is off. - controls_.addInt16("txPowerSetting", txPowerSetting_, 0, 21); + controls_.addControl("txPowerSetting", txPowerSetting_, 0, 21); controls_.setHidden(controls_.count() - 1, !radioOn); controls_.setAdvanced(controls_.count() - 1); } // Expert-only: discovery works without it, and the projectMM UI finds devices over UDP. - controls_.addBool("mDNS", mdnsEnabled_); + controls_.addControl("mDNS", mdnsEnabled_); controls_.setAdvanced(controls_.count() - 1); // addressing goes immediately before the static-IP fields it conditions, so @@ -360,12 +360,12 @@ class NetworkModule : public MoonModule { // GPIO controls use addPin → a plain number input (ControlType::Pin), // not a slider: a GPIO has no meaningful range to drag. -1 = unused. // phyAddr is a PHY MDIO address (-1 = auto-detect, else 0..31), NOT a GPIO — - // so it uses a signed int control (addInt16), not addPin. (A Pin here would + // so it uses a signed int16 control, not addPin. (A Pin here would // make the pin ownership map report it as a false GPIO claim, since that map // reads every ControlType::Pin as a claimed GPIO.) It must be signed: -1 is // IDF's ESP_ETH_PHY_ADDR_AUTO sentinel (scan the MDIO bus), the S31's RGMII // default — a uint8 mangled -1 to 31 and the PHY never answered. - controls_.addInt16("ethPhyAddr", ethPhyAddr_, -1, 31); + controls_.addControl("ethPhyAddr", ethPhyAddr_, -1, 31); controls_.setNumberField(controls_.count() - 1); // an MDIO address is an identity, not a magnitude — a number field, not a slider controls_.setHidden(controls_.count() - 1, !isEth); controls_.addPin("ethRstGpio", ethRstGpio_); @@ -383,7 +383,7 @@ class NetworkModule : public MoonModule { controls_.setHidden(controls_.count() - 1, !isRmii); // Clock direction is a boolean (true = clock IN / board feeds it, // false = chip drives it OUT) — a toggle, not a 0..1 slider. - controls_.addBool("ethClockExtIn", ethClockExtIn_); + controls_.addControl("ethClockExtIn", ethClockExtIn_); controls_.setHidden(controls_.count() - 1, !isRmii); controls_.addPin("ethSpiMiso", ethSpiMiso_); controls_.setHidden(controls_.count() - 1, !isSpi); @@ -778,7 +778,7 @@ class NetworkModule : public MoonModule { // ~54 on any ESP32-family chip, so int8 is ample — bound via addPin (Pin control // → number input). ethConfigDefault's fields are plain int; the values are all // small (≤52 / -1) so the copy into int8_t is lossless. - int16_t ethPhyAddr_ = static_cast(platform::ethConfigDefault.phyAddr); // PHY MDIO addr 0..31, or -1 = auto-detect (scan the bus). Signed (int16, via addInt16) so -1 round-trips — a uint8 cast the platform's -1 to 255 and the 0..31 control showed 31, a fixed address no PHY answered, so the S31's RGMII never linked. NOT a GPIO (deliberately not addPin, or the pin-map would false-claim it). + int16_t ethPhyAddr_ = static_cast(platform::ethConfigDefault.phyAddr); // PHY MDIO addr 0..31, or -1 = auto-detect (scan the bus). Signed (int16) so -1 round-trips — a uint8 cast the platform's -1 to 255 and the 0..31 control showed 31, a fixed address no PHY answered, so the S31's RGMII never linked. NOT a GPIO (deliberately not addPin, or the pin-map would false-claim it). int8_t ethMdcGpio_ = static_cast(platform::ethConfigDefault.mdcGpio); int8_t ethMdioGpio_ = static_cast(platform::ethConfigDefault.mdioGpio); int8_t ethRstGpio_ = static_cast(platform::ethConfigDefault.rstGpio); diff --git a/src/core/SystemModule.h b/src/core/SystemModule.h index 94f0b95f..3d4c1877 100644 --- a/src/core/SystemModule.h +++ b/src/core/SystemModule.h @@ -205,7 +205,7 @@ class SystemModule : public MoonModule { // Expert mode: reveals controls tagged `advanced` (ring diagnostics, tuning knobs) across the UI. // Persisted so it survives a reboot; the UI honors it client-side (see the `advanced` flag on // Control) — nothing in the firmware reads it, so it needs no rebuild trigger. - controls_.addBool("expertMode", expertMode_); + controls_.addControl("expertMode", expertMode_); // Serial log level: how chatty the device is on the UART. Default Warn keeps the once-a-second // KPI tick line off (a status LED that blinks on serial TX rests quiet) while real warnings and // errors still print. Applied to the platform logger on change (see applyLogLevel); the KPI line diff --git a/src/core/moonlive/MoonLiveBuiltins.h b/src/core/moonlive/MoonLiveBuiltins.h index b3880d83..23f45be5 100644 --- a/src/core/moonlive/MoonLiveBuiltins.h +++ b/src/core/moonlive/MoonLiveBuiltins.h @@ -94,7 +94,7 @@ struct Builtin { HostCallFn fn = nullptr; // Call: the host C function pointer InlineOp inlineOp{}; // Inline: the neutral opcode tag // Which arguments are passed BY REFERENCE, as a bit per position (bit 0 = first argument). - // A script names a member and the compiler passes its arena offset, so `addUint8("bpm", bpm, + // A script names a member and the compiler passes its arena offset, so `addControl("bpm", bpm, // 1, 120)` reads as the reference a compiled module passes rather than as bpm's value. Zero // for every builtin that takes plain values, which is all of them but this one. // @@ -102,7 +102,7 @@ struct Builtin { // by-reference, and `draw::line` already proves a builtin may take seven arguments. uint8_t byRef = 0; // Which arguments must be a STRING LITERAL, a bit per position. Without it a bare identifier - // in a name slot compiles: `addUint8(s, s, 0, 9)` read `s`'s VALUE as the label and handed the + // in a name slot compiles: `addControl(s, s, 0, 9)` read `s`'s VALUE as the label and handed the // host a pointer built from a color byte. Stated per builtin for the same reason byRef is, // rather than special-cased by name in the parser. uint8_t byStr = 0; diff --git a/src/core/moonlive/MoonLiveCompiler.cpp b/src/core/moonlive/MoonLiveCompiler.cpp index 43b01add..ba26a82a 100644 --- a/src/core/moonlive/MoonLiveCompiler.cpp +++ b/src/core/moonlive/MoonLiveCompiler.cpp @@ -90,7 +90,7 @@ struct Lexer { // Every line comment is whitespace. A comment that CHANGED BEHAVIOR lived here: // `// @control 1..120` declared a control's range, which is not C and does not // resemble the compiled module a script stands in for. `defineControls()` calling - // `addUint8("bpm", bpm, 1, 120)` replaced it, so the token, its capture and the + // `addControl("bpm", bpm, 1, 120)` replaced it, so the token, its capture and the // `lineStart` this needed are all gone. while (*p && *p != '\n') p++; continue; @@ -192,7 +192,7 @@ struct Parser { // Every class-scope `uint8_t x = 0;` is a MEMBER: the class model, where a declaration inside // the class is a member of it. Whether the UI shows one is a separate question the script - // answers by calling addUint8 in defineControls, so `controls` below is a VIEW of these rather + // answers by calling addControl in defineControls, so `controls` below is a VIEW of these rather // than a second storage: a control's offset IS its member's arena byte. // // `DeclaredControl` carries both because they are the same record. A member that no control @@ -681,7 +681,7 @@ struct Parser { // a STRING, for the UI label. A frame slot is a machine word, so the pointer // into the source fits; the host reads it as a `const char*`. // - // a MEMBER BY NAME, meaning its ADDRESS rather than its value. `addUint8("bpm", + // a MEMBER BY NAME, meaning its ADDRESS rather than its value. `addControl("bpm", // bpm, 1, 120)` reads as the reference a compiled module passes, and the compiler // supplies the arena offset the host binds to. Only where the builtin asks for it // (byRef), so `setRGB(bpm, …)` still reads bpm's value as it always did. @@ -1191,7 +1191,7 @@ struct Parser { /// appear to work and then vanish, which is worse than being refused. /// /// A CONTROL is deliberately NOT refused, though the UI does own its value. Whether a member - /// becomes a control is decided at RUN time, by `defineControls()` calling `addUint8` on it, + /// becomes a control is decided at RUN time, by `defineControls()` calling `addControl` on it, /// so the parser cannot know: that is the direct consequence of a control being an ordinary /// call rather than an annotation. Writing one is also legitimate (an effect that ramps its /// own speed and lets the slider re-take it), and the outcome is visible rather than silent: diff --git a/src/core/moonlive/MoonLiveIr.h b/src/core/moonlive/MoonLiveIr.h index 5292ab43..0940f447 100644 --- a/src/core/moonlive/MoonLiveIr.h +++ b/src/core/moonlive/MoonLiveIr.h @@ -170,7 +170,7 @@ struct IrInst { InlineOp inlineOp{}; // Inline: the neutral opcode tag }; -// A control a script declared (`addUint8("speed", speed, 0, 99)`). Neutral: the core +// A control a script declared (`addControl("speed", speed, 0, 99)`). Neutral: the core // knows {name, a neutral type, range, default, and the byte offset into the run-time controls // arena it lives at}. The light-domain binding turns this into a real MoonModule control bound to // the arena slot. `type` is a neutral kind — Uint8 only in Stage 1 — NOT a projectMM ControlType. @@ -181,7 +181,7 @@ struct IrInst { struct DeclaredControl { const char* name = nullptr; // script-declared name (points into the source buffer) - // The UI range, as wide as the widest member a control can bind: addUint8 declares 0..255 and + // The UI range, as wide as the widest member a control can bind: a byte declares 0..255 and // an int member the full 32-bit span, so the field has to hold the wider one. int32_t min = 0, max = 255; // The initializer, wide enough for the widest member type. Separate from the range because a diff --git a/src/core/moonlive/moonlive_emit.h b/src/core/moonlive/moonlive_emit.h index a362a2d7..f7ea5b3e 100644 --- a/src/core/moonlive/moonlive_emit.h +++ b/src/core/moonlive/moonlive_emit.h @@ -3,10 +3,15 @@ #include #include -// MM_MOONLIVE_HAS_HOST_JIT is defined in platform_config.h (per-platform), so the -// arch check stays behind the platform boundary and this header stays neutral. The macro -// (not a constexpr) is required because #include-side test files gate whole TEST_CASEs -// on `#if MM_MOONLIVE_HAS_HOST_JIT`. +// MM_MOONLIVE_HAS_HOST_JIT is defined in platform_config.h (per-platform), so the arch check stays +// behind the platform boundary and this header stays neutral. +// +// It exists for the TEST HARNESS and nothing else — no shipping code branches on it. A build with +// no backend degrades at RUN time instead: lowerToBytes returns 0 (moonlive_asm_noarch.cpp), +// compile() reports the failure, and a scripted module renders dark. But a test that calls +// render() cannot even be COMPILED there, so ~40 TEST_CASEs are gated on `#if +// MM_MOONLIVE_HAS_HOST_JIT`. That is why it is a macro rather than a constexpr, and why the +// noarch path does not make it redundant: one answers the program, the other answers the build. #include "platform/platform.h" // MoonLive — per-ISA code emitter (the backend seam, §3.2 of livescripts-analysis-top-down.md). diff --git a/src/light/drivers/DriverBase.h b/src/light/drivers/DriverBase.h index 973fd495..d7f470cb 100644 --- a/src/light/drivers/DriverBase.h +++ b/src/light/drivers/DriverBase.h @@ -264,7 +264,7 @@ class DriverBase : public MoonModule { /// via the DriverBase::defineControls template method. The Select is rebuilt from the library on /// every defineControls (which re-runs on a control change), so adding/renaming a preset shows up. void defineCorrectionControls() { - controls_.addUint8("localBrightness", localBrightness_, 0, 255); + controls_.addControl("localBrightness", localBrightness_, 0, 255); buildPresetOptions(); // fill presetOptions_ from the library, sync id/sel/ref controls_.addSelect("lightPreset", presetSel_, presetOptions_, presetOptionCount_); controls_.addSelect("whiteMode", whiteMode_, kWhiteModeOptions, kWhiteModeCount); @@ -355,8 +355,8 @@ class DriverBase : public MoonModule { /// helper (not auto-added) so a driver opts in by calling it where its other /// controls go, keeping control *order* in the driver's hands. void addWindowControls() { - controls_.addUint16("start", start_); - controls_.addUint16("count", count_); + controls_.addControl("start", start_); + controls_.addControl("count", count_); } /// True if `name` is one of the window controls — a driver folds this into its diff --git a/src/light/drivers/Drivers.h b/src/light/drivers/Drivers.h index 770f19c0..a978e8aa 100644 --- a/src/light/drivers/Drivers.h +++ b/src/light/drivers/Drivers.h @@ -196,10 +196,10 @@ class Drivers : public MoonModule { uint8_t effectiveBrightness() const { return on ? brightness : 0; } void defineControls() override { - controls_.addBool("on", on); // master power — first so it renders at the top of the card - controls_.addUint8("brightness", brightness, 0, 255); + controls_.addControl("on", on); // master power — first so it renders at the top of the card + controls_.addControl("brightness", brightness, 0, 255); controls_.addPalette("palette", palette, mm::paletteOptions, mm::palettes::kCount); - controls_.addBool("multicore", multicore); // render↔encode split on/off (see the member's doc) + controls_.addControl("multicore", multicore); // render↔encode split on/off (see the member's doc) controls_.setAdvanced(controls_.count() - 1); // a tuning knob, not a user setting // Read-only KPI, the multicore sibling of the driver's frameTime: how long core 0 waited at the // frame boundary for core 1's encode. ~0 = render and encode overlap perfectly (the split pays diff --git a/src/light/drivers/MoonLedDriver.h b/src/light/drivers/MoonLedDriver.h index 405a43c9..262e405b 100644 --- a/src/light/drivers/MoonLedDriver.h +++ b/src/light/drivers/MoonLedDriver.h @@ -300,37 +300,37 @@ class MoonI80Peripheral : public LedPeripheral { // The shift-clock speed switch, below the clockPin/latchPin wiring pair (the orchestrator places // this hook right after latchPin): OFF = 20 MHz (safe default), ON = 26.67 MHz (overclock). The // fix for per-strand '595 corruption is OFF; see the member doc. Shift-mode only. - controls.addBool("shiftOverclock", shiftOverclock); + controls.addControl("shiftOverclock", shiftOverclock); controls.setHidden(controls.count() - 1, !owner_->pinExpanderMode()); controls.setAdvanced(controls.count() - 1); // a '595 clock tuning knob — expert only // Path selector (pin-expander mode only): the ring, or the whole frame. A distinct axis from // ringSnapshot — this picks the PATH, ringSnapshot tunes how the RING reads its source; they // compose. Whole-frame is the A/B reference: it is how the whole-frame-PSRAM-at-the-expander-clock // question stays testable on the same board and content. - controls.addBool("useRing", useRing); + controls.addControl("useRing", useRing); controls.setHidden(controls.count() - 1, !owner_->pinExpanderMode()); // The source-snapshot A/B knob, directly under useRing (the path it belongs to): the ring reads // its source through an immutable snapshot (ON, the safe default) or the live buffer (OFF, a bench // lever). Meaningful only when the ring is the chosen path, so hidden on wantsRing() like the // geometry below. `ringSnapshot` lives on the orchestrator (ParallelLedDriver); the control binds // it here through the mutable reference accessor. - controls.addBool("ringSnapshot", owner_->ringSnapshotRef()); + controls.addControl("ringSnapshot", owner_->ringSnapshotRef()); controls.setHidden(controls.count() - 1, !wantsRing()); // The geometry + the instrument, shown only when the RING is the chosen path — all meaningless on // the whole-frame one. (Gating on wantsRing() is safe: it reads plain members, pinExpanderMode + // useRing, not frameBytes_, so it resolves even before the source buffer is wired at boot.) - controls.addBool("ringAuto", ringAuto); + controls.addControl("ringAuto", ringAuto); controls.setHidden(controls.count() - 1, !wantsRing()); // ringRows/ringBufs/ringPadUs are DEV TUNING — ringAuto derives them for the end user (kept // visible above); the manual knobs are expert-only. (Once ringAuto is verified to always pick the // right geometry, ringAuto itself could go advanced too — for now it stays visible as the recourse.) - controls.addUint8("ringRows", ringRows, 1, 64); + controls.addControl("ringRows", ringRows, 1, 64); controls.setHidden(controls.count() - 1, !wantsRing()); controls.setAdvanced(controls.count() - 1); - controls.addUint8("ringBufs", ringBufs, platform::kRingBufsMin, platform::kRingBufsMax); + controls.addControl("ringBufs", ringBufs, platform::kRingBufsMin, platform::kRingBufsMax); controls.setHidden(controls.count() - 1, !wantsRing()); controls.setAdvanced(controls.count() - 1); - controls.addUint8("ringPadUs", ringPadUs, 0, platform::kRingPadMaxUs); + controls.addControl("ringPadUs", ringPadUs, 0, platform::kRingPadMaxUs); controls.setHidden(controls.count() - 1, !wantsRing()); controls.setAdvanced(controls.count() - 1); // Ring internals, so the streaming can be diagnosed by polling /api/state (reliable) rather than diff --git a/src/light/drivers/NetworkSendDriver.h b/src/light/drivers/NetworkSendDriver.h index a9e65f93..57253d12 100644 --- a/src/light/drivers/NetworkSendDriver.h +++ b/src/light/drivers/NetworkSendDriver.h @@ -125,9 +125,9 @@ class NetworkSendDriver : public DriverBase { controls_.addSelect("protocol", protocol, kProtocolOptions, kProtocolCount); controls_.addText("ips", ips, sizeof(ips)); controls_.addText("lightsPerIp", lightsPerIp, sizeof(lightsPerIp)); - controls_.addUint16("universe_start", universeStart); + controls_.addControl("universe_start", universeStart); addWindowControls(); // start / count — the slice of the shared buffer this sink sends - controls_.addUint8("fps", fps, 1, 120); + controls_.addControl("fps", fps, 1, 120); } /// A start/count change resizes the window this sink sends, and a Custom channel-count diff --git a/src/light/drivers/PanelCardDriver.h b/src/light/drivers/PanelCardDriver.h index c9b8520f..9918f1a8 100644 --- a/src/light/drivers/PanelCardDriver.h +++ b/src/light/drivers/PanelCardDriver.h @@ -159,7 +159,7 @@ class PanelCardDriver : public DriverBase { if constexpr (!platform::hasNamedNetInterfaces) controls_.setHidden(controls_.count() - 1, true); addWindowControls(); // start / count — which slice of the shared buffer this sink sends - controls_.addUint8("fps", fps, 1, 120); + controls_.addControl("fps", fps, 1, 120); } /// Geometry and the window change how much of the buffer is corrected, so both re-run the diff --git a/src/light/drivers/ParallelLedDriver.h b/src/light/drivers/ParallelLedDriver.h index 891229f6..ec6797e9 100644 --- a/src/light/drivers/ParallelLedDriver.h +++ b/src/light/drivers/ParallelLedDriver.h @@ -355,7 +355,7 @@ class ParallelLedDriver : public DriverBase { // ensurePeripheralMatchesSelection so a peripheral CHANGE evaluates the visibility against the NEW // backend. The saved value stays bound, so it survives a round-trip through a single-buffer-only // peripheral and re-engages when a peripheral that supports it is selected again. - controls_.addBool("doubleBuffer", doubleBuffer); + controls_.addControl("doubleBuffer", doubleBuffer); controls_.setHidden(controls_.count() - 1, !(peripheral_ && peripheral_->supportsDoubleBuffer())); // The bus pins sit UNDER the peripheral selector because they ARE peripheral-specific: for a driver // that owns its own GPIO routing they are '595 pins: MoonI80 routes WR only when a shift register @@ -370,7 +370,7 @@ class ParallelLedDriver : public DriverBase { // a hardware limit, see supportsPinExpander). The '595's width (8) is the chip's, not a setting, // so the toggle is a plain checkbox; latchPin is bound always (a saved value survives a // round-trip through direct mode) but shown only when the expander is on. - controls_.addBool("pinExpander", pinExpander); + controls_.addControl("pinExpander", pinExpander); controls_.setHidden(controls_.count() - 1, !(peripheral_ && peripheral_->supportsPinExpander())); controls_.addPin("latchPin", latchPin); controls_.setHidden(controls_.count() - 1, !pinExpanderMode()); @@ -379,7 +379,7 @@ class ParallelLedDriver : public DriverBase { if (peripheral_) peripheral_->addRingControls(controls_); // The on-device loopback self-test + its wiring — a dev/bring-up instrument (jumper a lane to the // rx pin), not a casual-user setting, so the whole cluster is expert-only. - controls_.addBool("loopbackTest", loopbackTest); + controls_.addControl("loopbackTest", loopbackTest); controls_.setAdvanced(controls_.count() - 1); // Always bound, shown only in test mode — the conditional-control shape. controls_.addPin("loopbackTxPin", loopbackTxPin); @@ -393,11 +393,11 @@ class ParallelLedDriver : public DriverBase { controls_.setAdvanced(controls_.count() - 1); // Which strand carries the pattern — shift mode only (in direct mode it is always lane 0). // Lets the jumper come off ANY '595 output, including a spare one that drives no panel. - controls_.addUint8("loopbackStrand", loopbackStrand, 0, kMaxStrands - 1); + controls_.addControl("loopbackStrand", loopbackStrand, 0, kMaxStrands - 1); controls_.setHidden(controls_.count() - 1, !loopbackTest || !pinExpanderMode()); controls_.setAdvanced(controls_.count() - 1); // Intrusive: ride the live pipeline instead of a private replica (see the member doc). - controls_.addBool("loopbackIntrusive", loopbackIntrusive); + controls_.addControl("loopbackIntrusive", loopbackIntrusive); controls_.setHidden(controls_.count() - 1, !loopbackTest); controls_.setAdvanced(controls_.count() - 1); } @@ -1136,7 +1136,7 @@ class ParallelLedDriver : public DriverBase { /// read `outChannels` through this instead of inheriting `correction_` directly. const Correction& correction() const { return correction_; } /// Mutable reference to the ring-snapshot A/B knob, for a backend's addRingControls to bind - /// (`controls.addBool("ringSnapshot", owner_->ringSnapshotRef())`) — addBool binds by reference, + /// (`controls.addControl("ringSnapshot", owner_->ringSnapshotRef())`) — a bool addControl binds by reference, /// so the control needs the member's address, not a copy. bool& ringSnapshotRef() { return ringSnapshot; } /// The ring snapshot buffer (null when unallocated / off the ring path) — a backend's KPI refresh diff --git a/src/light/drivers/PreviewDriver.h b/src/light/drivers/PreviewDriver.h index a816bea6..400575d0 100644 --- a/src/light/drivers/PreviewDriver.h +++ b/src/light/drivers/PreviewDriver.h @@ -71,8 +71,8 @@ class PreviewDriver : public DriverBase { /// ON leg tears the preview until the slot-sharing fix lands), so it shows only when System.expertMode /// is on. It still persists and still accepts API writes; only the default UI hides it. void defineDriverControls() override { - controls_.addUint8("fps", fps, 1, 60); - controls_.addBool("resumableFrames", resumableFrames); + controls_.addControl("fps", fps, 1, 60); + controls_.addControl("resumableFrames", resumableFrames); controls_.setAdvanced(controls_.count() - 1); } diff --git a/src/light/drivers/RmtLedDriver.h b/src/light/drivers/RmtLedDriver.h index b12e6d89..03c37c02 100644 --- a/src/light/drivers/RmtLedDriver.h +++ b/src/light/drivers/RmtLedDriver.h @@ -110,7 +110,7 @@ class RmtLedDriver : public DriverBase { addWindowControls(); // start / count — the slice of the shared buffer this driver outputs controls_.addText("pins", pins, sizeof(pins)); controls_.addText("ledsPerPin", ledsPerPin, sizeof(ledsPerPin)); - controls_.addBool("loopbackTest", loopbackTest); + controls_.addControl("loopbackTest", loopbackTest); controls_.setAdvanced(controls_.count() - 1); // expert-mode: a bench self-test, not a normal-use control // loopbackTxPin / loopbackRxPin are always bound (so persistence can load // them any time) but only shown while the test mode is on — same always- @@ -122,7 +122,7 @@ class RmtLedDriver : public DriverBase { controls_.setHidden(controls_.count() - 1, !loopbackTest); controls_.addPin("loopbackRxPin", loopbackRxPin); controls_.setHidden(controls_.count() - 1, !loopbackTest); - controls_.addBool("loopbackFrame", loopbackFrame); + controls_.addControl("loopbackFrame", loopbackFrame); controls_.setHidden(controls_.count() - 1, !loopbackTest); } diff --git a/src/light/effects/AudioVolumeEffect.h b/src/light/effects/AudioVolumeEffect.h index 76109ae1..480a6705 100644 --- a/src/light/effects/AudioVolumeEffect.h +++ b/src/light/effects/AudioVolumeEffect.h @@ -18,7 +18,7 @@ class AudioVolumeEffect : public EffectBase { uint8_t brightness = 255; // overall ceiling void defineControls() override { - controls_.addUint8("brightness", brightness, 1, 255); + controls_.addControl("brightness", brightness, 1, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/BallpitEffect.h b/src/light/effects/BallpitEffect.h index 61b01c0e..c0b7d05b 100644 --- a/src/light/effects/BallpitEffect.h +++ b/src/light/effects/BallpitEffect.h @@ -40,12 +40,12 @@ class BallpitEffect : public EffectBase { uint8_t drag = 6; // damping, so the heap settles instead of sloshing forever void defineControls() override { - controls_.addUint8("balls", balls, 1, kPool); - controls_.addUint8("gravity", gravity, 0, 64); - controls_.addUint8("size", size, 1, 8); - controls_.addUint8("bounce", bounce, 0, 255); - controls_.addUint8("tilt", tilt, 0, 255); - controls_.addUint8("drag", drag, 0, 64); + controls_.addControl("balls", balls, 1, kPool); + controls_.addControl("gravity", gravity, 0, 64); + controls_.addControl("size", size, 1, 8); + controls_.addControl("bounce", bounce, 0, 255); + controls_.addControl("tilt", tilt, 0, 255); + controls_.addControl("drag", drag, 0, 64); } void prepare() override { diff --git a/src/light/effects/BlurzEffect.h b/src/light/effects/BlurzEffect.h index 9d2a14f9..bf724e74 100644 --- a/src/light/effects/BlurzEffect.h +++ b/src/light/effects/BlurzEffect.h @@ -38,10 +38,10 @@ class BlurzEffect : public EffectBase { bool geqScanner = false; // steady sweep across the strip (vs. random jump) when freqMap is off void defineControls() override { - controls_.addUint8("fadeRate", fadeRate, 1, 255); - controls_.addUint8("blur", blur, 1, 255); - controls_.addBool("freqMap", freqMap); - controls_.addBool("geqScanner", geqScanner); + controls_.addControl("fadeRate", fadeRate, 1, 255); + controls_.addControl("blur", blur, 1, 255); + controls_.addControl("freqMap", freqMap); + controls_.addControl("geqScanner", geqScanner); } // WLED clears the segment once on the first call (SEGENV.call == 0 → fadeToBlackBy(255), a full diff --git a/src/light/effects/BouncingBallsEffect.h b/src/light/effects/BouncingBallsEffect.h index 30fd3ae8..f8f73784 100644 --- a/src/light/effects/BouncingBallsEffect.h +++ b/src/light/effects/BouncingBallsEffect.h @@ -34,8 +34,8 @@ class BouncingBallsEffect : public EffectBase { uint8_t numBalls = 8; // balls per column (1..maxNumBalls) void defineControls() override { - controls_.addUint8("grav", grav, 0, 255); - controls_.addUint8("numBalls", numBalls, 1, maxNumBalls); + controls_.addControl("grav", grav, 0, 255); + controls_.addControl("numBalls", numBalls, 1, maxNumBalls); } void prepare() override { diff --git a/src/light/effects/DemoReelEffect.h b/src/light/effects/DemoReelEffect.h index 9dc1e910..81c765b0 100644 --- a/src/light/effects/DemoReelEffect.h +++ b/src/light/effects/DemoReelEffect.h @@ -39,10 +39,10 @@ class DemoReelEffect : public EffectBase { bool showName = true; // overlay the playing effect's name (the reel as a showcase tool) void defineControls() override { - controls_.addUint8("interval", interval, 1, 120); - controls_.addBool("shuffle", shuffle); - controls_.addBool("randomPalette", randomPalette); - controls_.addBool("showName", showName); + controls_.addControl("interval", interval, 1, 120); + controls_.addControl("shuffle", shuffle); + controls_.addControl("randomPalette", randomPalette); + controls_.addControl("showName", showName); } // Build the eligible-effect list (all Effect-role types except this one) and start the first. diff --git a/src/light/effects/DissolveEffect.h b/src/light/effects/DissolveEffect.h index 5b52b53f..e9c39da7 100644 --- a/src/light/effects/DissolveEffect.h +++ b/src/light/effects/DissolveEffect.h @@ -39,10 +39,10 @@ class DissolveEffect : public EffectBase { bool scatter = true; // random order; off gives a positional wipe instead void defineControls() override { - controls_.addUint8("bpm", bpm, 1, 120); - controls_.addUint8("spread", spread, 0, 255); - controls_.addBool("eased", eased); - controls_.addBool("scatter", scatter); + controls_.addControl("bpm", bpm, 1, 120); + controls_.addControl("spread", spread, 0, 255); + controls_.addControl("eased", eased); + controls_.addControl("scatter", scatter); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/DistortionWavesEffect.h b/src/light/effects/DistortionWavesEffect.h index 69d0ced9..27cb8fb0 100644 --- a/src/light/effects/DistortionWavesEffect.h +++ b/src/light/effects/DistortionWavesEffect.h @@ -29,9 +29,9 @@ class DistortionWavesEffect : public EffectBase { uint8_t speed = 50; // animation speed, 0..100 (0 = frozen) void defineControls() override { - controls_.addUint8("freq_x", freq_x, 1, 8); - controls_.addUint8("freq_y", freq_y, 1, 8); - controls_.addUint8("speed", speed, 0, 100); + controls_.addControl("freq_x", freq_x, 1, 8); + controls_.addControl("freq_y", freq_y, 1, 8); + controls_.addControl("speed", speed, 0, 100); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/EchoEffect.h b/src/light/effects/EchoEffect.h index e6930b62..fa49ab9f 100644 --- a/src/light/effects/EchoEffect.h +++ b/src/light/effects/EchoEffect.h @@ -39,11 +39,11 @@ class EchoEffect : public EffectBase { uint8_t size = 2; // radius of the bright source void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 120); - controls_.addUint8("zoom", zoom, 0, 64); - controls_.addUint8("rotate", rotate, 0, 64); - controls_.addUint8("decay", decay, 1, 128); - controls_.addUint8("size", size, 0, 16); + controls_.addControl("bpm", bpm, 0, 120); + controls_.addControl("zoom", zoom, 0, 64); + controls_.addControl("rotate", rotate, 0, 64); + controls_.addControl("decay", decay, 1, 128); + controls_.addControl("size", size, 0, 16); } void prepare() override { diff --git a/src/light/effects/FireEffect.h b/src/light/effects/FireEffect.h index 131ba58e..38a597ac 100644 --- a/src/light/effects/FireEffect.h +++ b/src/light/effects/FireEffect.h @@ -22,8 +22,8 @@ class FireEffect : public EffectBase { uint8_t sparking = 120; void defineControls() override { - controls_.addUint8("cooling", cooling, 1, 255); - controls_.addUint8("sparking", sparking, 1, 255); + controls_.addControl("cooling", cooling, 1, 255); + controls_.addControl("sparking", sparking, 1, 255); } void prepare() override { diff --git a/src/light/effects/FireworksEffect.h b/src/light/effects/FireworksEffect.h index efc5c2ce..42cb71cd 100644 --- a/src/light/effects/FireworksEffect.h +++ b/src/light/effects/FireworksEffect.h @@ -45,13 +45,13 @@ class FireworksEffect : public EffectBase { uint8_t fade = 25; // trail length (the Layer's decay, not the pool's) void defineControls() override { - controls_.addUint8("launchRate", launchRate, 1, 255); - controls_.addUint8("launchSpeed", launchSpeed, 10, 255); - controls_.addUint8("gravity", gravity, 1, 128); - controls_.addUint8("sparks", sparks, 1, 64); - controls_.addUint8("sparkLife", sparkLife, 10, 255); - controls_.addUint8("drag", drag, 0, 64); - controls_.addUint8("fade", fade, 1, 255); + controls_.addControl("launchRate", launchRate, 1, 255); + controls_.addControl("launchSpeed", launchSpeed, 10, 255); + controls_.addControl("gravity", gravity, 1, 128); + controls_.addControl("sparks", sparks, 1, 64); + controls_.addControl("sparkLife", sparkLife, 10, 255); + controls_.addControl("drag", drag, 0, 64); + controls_.addControl("fade", fade, 1, 255); } void prepare() override { diff --git a/src/light/effects/FixedRectangleEffect.h b/src/light/effects/FixedRectangleEffect.h index c403531c..79b70a4d 100644 --- a/src/light/effects/FixedRectangleEffect.h +++ b/src/light/effects/FixedRectangleEffect.h @@ -43,17 +43,17 @@ class FixedRectangleEffect : public EffectBase { bool alternateWhite = false; void defineControls() override { - controls_.addUint8("red", red); - controls_.addUint8("green", green); - controls_.addUint8("blue", blue); - controls_.addUint8("white", white); - controls_.addInt16("X position", rectX, 0, INT16_MAX); - controls_.addInt16("Y position", rectY, 0, INT16_MAX); - controls_.addInt16("Z position", rectZ, 0, INT16_MAX); - controls_.addInt16("Rectangle width", rectW, 1, INT16_MAX); - controls_.addInt16("Rectangle height", rectH, 1, INT16_MAX); - controls_.addInt16("Rectangle depth", rectD, 1, INT16_MAX); - controls_.addBool("alternateWhite", alternateWhite); + controls_.addControl("red", red); + controls_.addControl("green", green); + controls_.addControl("blue", blue); + controls_.addControl("white", white); + controls_.addControl("X position", rectX, 0, INT16_MAX); + controls_.addControl("Y position", rectY, 0, INT16_MAX); + controls_.addControl("Z position", rectZ, 0, INT16_MAX); + controls_.addControl("Rectangle width", rectW, 1, INT16_MAX); + controls_.addControl("Rectangle height", rectH, 1, INT16_MAX); + controls_.addControl("Rectangle depth", rectD, 1, INT16_MAX); + controls_.addControl("alternateWhite", alternateWhite); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/FreqMatrixEffect.h b/src/light/effects/FreqMatrixEffect.h index d4c2eff9..c2a9abed 100644 --- a/src/light/effects/FreqMatrixEffect.h +++ b/src/light/effects/FreqMatrixEffect.h @@ -52,12 +52,12 @@ class FreqMatrixEffect : public EffectBase { bool audioSpeed = false; // when set, the audio level modulates the scroll rate void defineControls() override { - controls_.addUint8("speed", speed, 1, 255); - controls_.addUint8("fx", fx, 0, 255); - controls_.addUint8("lowBin", lowBin, 0, 255); - controls_.addUint8("highBin", highBin, 0, 255); - controls_.addUint8("sensitivity", sensitivity, 10, 100); - controls_.addBool("audioSpeed", audioSpeed); + controls_.addControl("speed", speed, 1, 255); + controls_.addControl("fx", fx, 0, 255); + controls_.addControl("lowBin", lowBin, 0, 255); + controls_.addControl("highBin", highBin, 0, 255); + controls_.addControl("sensitivity", sensitivity, 10, 100); + controls_.addControl("audioSpeed", audioSpeed); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/FreqSawsEffect.h b/src/light/effects/FreqSawsEffect.h index b34b114c..7c61cba9 100644 --- a/src/light/effects/FreqSawsEffect.h +++ b/src/light/effects/FreqSawsEffect.h @@ -47,12 +47,12 @@ class FreqSawsEffect : public EffectBase { uint8_t method = 2; // 0 Chaos, 1 Chaos fix, 2 BandPhases void defineControls() override { - controls_.addUint8("fade", fade, 0, 255); - controls_.addUint8("increaser", increaser, 0, 255); - controls_.addUint8("decreaser", decreaser, 0, 255); - controls_.addUint8("bpmMax", bpmMax, 0, 255); - controls_.addBool("invert", invert); - controls_.addBool("keepOn", keepOn); + controls_.addControl("fade", fade, 0, 255); + controls_.addControl("increaser", increaser, 0, 255); + controls_.addControl("decreaser", decreaser, 0, 255); + controls_.addControl("bpmMax", bpmMax, 0, 255); + controls_.addControl("invert", invert); + controls_.addControl("keepOn", keepOn); static constexpr const char* kMethodOptions[] = {"Chaos", "Chaos fix", "BandPhases"}; controls_.addSelect("method", method, kMethodOptions, 3); } diff --git a/src/light/effects/GEQ3DEffect.h b/src/light/effects/GEQ3DEffect.h index 4eb1ac60..f9e223c5 100644 --- a/src/light/effects/GEQ3DEffect.h +++ b/src/light/effects/GEQ3DEffect.h @@ -36,15 +36,15 @@ class GEQ3DEffect : public EffectBase { bool borders = true; // outline each bar void defineControls() override { - controls_.addUint8("speed", speed, 1, 10); - controls_.addUint8("frontFill", frontFill, 0, 255); + controls_.addControl("speed", speed, 1, 10); + controls_.addControl("frontFill", frontFill, 0, 255); // MoonLight's horizon range is 0..size.x-1 (set at runtime). The control descriptor here is a // fixed 0..255 slider — the source's row index — and the value is clamped to the live row // count in tick(). A width/height-relative descriptor range isn't expressible at build time. - controls_.addUint8("horizon", horizon, 0, 255); - controls_.addUint8("depth", depth, 0, 255); - controls_.addUint8("numBands", numBands, 2, 16); - controls_.addBool("borders", borders); + controls_.addControl("horizon", horizon, 0, 255); + controls_.addControl("depth", depth, 0, 255); + controls_.addControl("numBands", numBands, 2, 16); + controls_.addControl("borders", borders); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/GEQEffect.h b/src/light/effects/GEQEffect.h index ead2d932..71e7574e 100644 --- a/src/light/effects/GEQEffect.h +++ b/src/light/effects/GEQEffect.h @@ -41,10 +41,10 @@ class GEQEffect : public EffectBase { bool smoothBars = false; // blend each band with its neighbours for a smoother profile void defineControls() override { - controls_.addUint8("fadeOut", fadeOut, 0, 255); - controls_.addUint8("ripple", ripple, 0, 255); - controls_.addBool("colorBars", colorBars); - controls_.addBool("smoothBars", smoothBars); + controls_.addControl("fadeOut", fadeOut, 0, 255); + controls_.addControl("ripple", ripple, 0, 255); + controls_.addControl("colorBars", colorBars); + controls_.addControl("smoothBars", smoothBars); } // One peak tracker per column: previousBarHeight[width]. WLED stores this in the segment's data diff --git a/src/light/effects/GameOfLifeEffect.h b/src/light/effects/GameOfLifeEffect.h index eb5f13af..7fe3f78e 100644 --- a/src/light/effects/GameOfLifeEffect.h +++ b/src/light/effects/GameOfLifeEffect.h @@ -65,19 +65,19 @@ class GameOfLifeEffect : public EffectBase { void defineControls() override { // MoonLight's bgC is a Coord3D 0..255 read as RGB. projectMM has no color control, so the // three components are three uint8s — the native, recognisable shape for an RGB triple here. - controls_.addUint8("backgroundColorR", backgroundColorR, 0, 255); - controls_.addUint8("backgroundColorG", backgroundColorG, 0, 255); - controls_.addUint8("backgroundColorB", backgroundColorB, 0, 255); + controls_.addControl("backgroundColorR", backgroundColorR, 0, 255); + controls_.addControl("backgroundColorG", backgroundColorG, 0, 255); + controls_.addControl("backgroundColorB", backgroundColorB, 0, 255); controls_.addSelect("ruleset", ruleset, kRulesetOptions, kRulesetCount); controls_.addText("customRuleString", customRuleString, sizeof(customRuleString)); - controls_.addUint8("GameSpeed (FPS)", speed, 0, 100); - controls_.addUint8("startingLifeDensity", lifeChance, 10, 90); - controls_.addUint8("mutationChance", mutation, 0, 100); - controls_.addBool("wrap", wrap); - controls_.addBool("disablePause", disablePause); - controls_.addBool("colorByAge", colorByAge); - controls_.addBool("infinite", infinite); - controls_.addUint8("blur", blur, 0, 255); + controls_.addControl("GameSpeed (FPS)", speed, 0, 100); + controls_.addControl("startingLifeDensity", lifeChance, 10, 90); + controls_.addControl("mutationChance", mutation, 0, 100); + controls_.addControl("wrap", wrap); + controls_.addControl("disablePause", disablePause); + controls_.addControl("colorByAge", colorByAge); + controls_.addControl("infinite", infinite); + controls_.addControl("blur", blur, 0, 255); } // Grid state lives on the heap (cells + next-gen + per-cell color), sized to the light count. diff --git a/src/light/effects/LavaLampEffect.h b/src/light/effects/LavaLampEffect.h index 892308c0..7b484563 100644 --- a/src/light/effects/LavaLampEffect.h +++ b/src/light/effects/LavaLampEffect.h @@ -24,9 +24,9 @@ class LavaLampEffect : public EffectBase { uint8_t intensity = 200; void defineControls() override { - controls_.addUint8("bpm", bpm, 1, 255); - controls_.addUint8("radius", radius, 8, 255); - controls_.addUint8("intensity", intensity, 1, 255); + controls_.addControl("bpm", bpm, 1, 255); + controls_.addControl("radius", radius, 8, 255); + controls_.addControl("intensity", intensity, 1, 255); } // Class scope, not function-local: -Wfunction-effects flags ANY static local in a diff --git a/src/light/effects/LinesEffect.h b/src/light/effects/LinesEffect.h index f79d392f..7d6c54f1 100644 --- a/src/light/effects/LinesEffect.h +++ b/src/light/effects/LinesEffect.h @@ -32,13 +32,13 @@ class LinesEffect : public EffectBase { // on every control change (MoonModule), so toggling `mode` re-hides these automatically — the // conditional-control shape the driver uses for latchPin/loopback pins. const bool dots = (mode == 1); - controls_.addUint8("speed", speed, 1, 240); + controls_.addControl("speed", speed, 1, 240); controls_.setHidden(controls_.count() - 1, dots); // lines only controls_.addSelect("axis", axis, kAxisOptions, 4); controls_.setHidden(controls_.count() - 1, dots); // lines only - controls_.addUint8("panelW", panelW, 1, 64); + controls_.addControl("panelW", panelW, 1, 64); controls_.setHidden(controls_.count() - 1, !dots); // panel dots only - controls_.addUint8("panelH", panelH, 1, 64); + controls_.addControl("panelH", panelH, 1, 64); controls_.setHidden(controls_.count() - 1, !dots); // panel dots only } diff --git a/src/light/effects/LissajousEffect.h b/src/light/effects/LissajousEffect.h index 6748188b..190de3b0 100644 --- a/src/light/effects/LissajousEffect.h +++ b/src/light/effects/LissajousEffect.h @@ -33,9 +33,9 @@ class LissajousEffect : public EffectBase { uint8_t speed = 128; // phase advance rate void defineControls() override { - controls_.addUint8("xFrequency", xFrequency, 0, 255); - controls_.addUint8("fadeRate", fadeRate, 0, 255); - controls_.addUint8("speed", speed, 0, 255); + controls_.addControl("xFrequency", xFrequency, 0, 255); + controls_.addControl("fadeRate", fadeRate, 0, 255); + controls_.addControl("speed", speed, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/MetaballsEffect.h b/src/light/effects/MetaballsEffect.h index 4b1766fc..8be5218e 100644 --- a/src/light/effects/MetaballsEffect.h +++ b/src/light/effects/MetaballsEffect.h @@ -22,10 +22,10 @@ class MetaballsEffect : public EffectBase { static constexpr uint8_t MAX_BALLS = 8; void defineControls() override { - controls_.addUint8("bpm", bpm, 1, 255); - controls_.addUint8("radius", radius, 4, 255); - controls_.addUint8("count", count, 1, MAX_BALLS); - controls_.addUint8("hue_shift", hue_shift, 0, 255); + controls_.addControl("bpm", bpm, 1, 255); + controls_.addControl("radius", radius, 4, 255); + controls_.addControl("count", count, 1, MAX_BALLS); + controls_.addControl("hue_shift", hue_shift, 0, 255); } // Class scope, not function-local: -Wfunction-effects flags ANY static local in a diff --git a/src/light/effects/NetworkReceiveEffect.h b/src/light/effects/NetworkReceiveEffect.h index a1aac100..38f14aed 100644 --- a/src/light/effects/NetworkReceiveEffect.h +++ b/src/light/effects/NetworkReceiveEffect.h @@ -52,8 +52,8 @@ class NetworkReceiveEffect : public EffectBase { uint16_t channelsPerUniverse = static_cast(MAX_CHANNELS_PER_UNIVERSE); void defineControls() override { - controls_.addUint16("universe_start", universeStart); - controls_.addUint16("channels_per_universe", channelsPerUniverse); + controls_.addControl("universe_start", universeStart); + controls_.addControl("channels_per_universe", channelsPerUniverse); } void release() override { diff --git a/src/light/effects/Noise2DEffect.h b/src/light/effects/Noise2DEffect.h index c0dffef5..9bac21e5 100644 --- a/src/light/effects/Noise2DEffect.h +++ b/src/light/effects/Noise2DEffect.h @@ -32,8 +32,8 @@ class Noise2DEffect : public EffectBase { uint8_t scale = 64; // noise zoom (2..255); higher = finer/more-detailed field void defineControls() override { - controls_.addUint8("speed", speed, 0, 15); - controls_.addUint8("scale", scale, 2, 255); + controls_.addControl("speed", speed, 0, 15); + controls_.addControl("scale", scale, 2, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/NoiseEffect.h b/src/light/effects/NoiseEffect.h index f285bd83..90d3d0ee 100644 --- a/src/light/effects/NoiseEffect.h +++ b/src/light/effects/NoiseEffect.h @@ -17,8 +17,8 @@ class NoiseEffect : public EffectBase { uint8_t bpm = 60; // beats per minute — scrolls 8 noise cells per beat void defineControls() override { - controls_.addUint8("scale", scale, 1, 32); - controls_.addUint8("bpm", bpm, 1, 255); + controls_.addControl("scale", scale, 1, 32); + controls_.addControl("bpm", bpm, 1, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/NoiseMeterEffect.h b/src/light/effects/NoiseMeterEffect.h index ed3b7800..bf935f63 100644 --- a/src/light/effects/NoiseMeterEffect.h +++ b/src/light/effects/NoiseMeterEffect.h @@ -31,8 +31,8 @@ class NoiseMeterEffect : public EffectBase { uint8_t width = 128; // level→length gain: how much of the column a given level fills (0..255) void defineControls() override { - controls_.addUint8("fadeRate", fadeRate, 200, 254); - controls_.addUint8("width", width, 0, 255); + controls_.addControl("fadeRate", fadeRate, 200, 254); + controls_.addControl("width", width, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/PaintBrushEffect.h b/src/light/effects/PaintBrushEffect.h index 86c56b99..b21f8d18 100644 --- a/src/light/effects/PaintBrushEffect.h +++ b/src/light/effects/PaintBrushEffect.h @@ -31,12 +31,12 @@ class PaintBrushEffect : public EffectBase { bool phase_chaos = false; // random per-frame phase jitter void defineControls() override { - controls_.addUint8("oscillatorOffset", oscillatorOffset, 0, 16); - controls_.addUint8("numLines", numLines, 2, 255); - controls_.addUint8("fadeRate", fadeRate, 0, 128); - controls_.addUint8("minLength", minLength); // slider over the full 0..255 range - controls_.addBool("color_chaos", color_chaos); - controls_.addBool("phase_chaos", phase_chaos); + controls_.addControl("oscillatorOffset", oscillatorOffset, 0, 16); + controls_.addControl("numLines", numLines, 2, 255); + controls_.addControl("fadeRate", fadeRate, 0, 128); + controls_.addControl("minLength", minLength); // slider over the full 0..255 range + controls_.addControl("color_chaos", color_chaos); + controls_.addControl("phase_chaos", phase_chaos); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/ParticlesEffect.h b/src/light/effects/ParticlesEffect.h index da1aa5c5..b037f641 100644 --- a/src/light/effects/ParticlesEffect.h +++ b/src/light/effects/ParticlesEffect.h @@ -23,10 +23,10 @@ class ParticlesEffect : public EffectBase { uint8_t hue_shift = 0; void defineControls() override { - controls_.addUint8("count", count, 1, 255); - controls_.addUint8("speed", speed, 1, 255); - controls_.addUint8("fade", fade, 1, 255); - controls_.addUint8("hue_shift", hue_shift, 0, 255); + controls_.addControl("count", count, 1, 255); + controls_.addControl("speed", speed, 1, 255); + controls_.addControl("fade", fade, 1, 255); + controls_.addControl("hue_shift", hue_shift, 0, 255); } void prepare() override { diff --git a/src/light/effects/PlasmaEffect.h b/src/light/effects/PlasmaEffect.h index 90977aa5..8450767a 100644 --- a/src/light/effects/PlasmaEffect.h +++ b/src/light/effects/PlasmaEffect.h @@ -22,10 +22,10 @@ class PlasmaEffect : public EffectBase { uint8_t hue_shift = 0; void defineControls() override { - controls_.addUint8("bpm", bpm, 1, 255); - controls_.addUint8("scale_x", scale_x, 1, 255); - controls_.addUint8("scale_y", scale_y, 1, 255); - controls_.addUint8("hue_shift", hue_shift, 0, 255); + controls_.addControl("bpm", bpm, 1, 255); + controls_.addControl("scale_x", scale_x, 1, 255); + controls_.addControl("scale_y", scale_y, 1, 255); + controls_.addControl("hue_shift", hue_shift, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/PolarNoiseEffect.h b/src/light/effects/PolarNoiseEffect.h index c80d05b2..2555060c 100644 --- a/src/light/effects/PolarNoiseEffect.h +++ b/src/light/effects/PolarNoiseEffect.h @@ -41,12 +41,12 @@ class PolarNoiseEffect : public EffectBase { uint8_t twist = 30; // how much the radius shears the angle, giving the field a spiral set void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 60); - controls_.addUint8("scale", scale, 1, 255); - controls_.addUint8("segments", segments, 1, 16); - controls_.addUint8("warp", warp, 0, 255); - controls_.addUint8("octaves", octaves, 1, 4); - controls_.addUint8("twist", twist, 0, 255); + controls_.addControl("bpm", bpm, 0, 60); + controls_.addControl("scale", scale, 1, 255); + controls_.addControl("segments", segments, 1, 16); + controls_.addControl("warp", warp, 0, 255); + controls_.addControl("octaves", octaves, 1, 4); + controls_.addControl("twist", twist, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/PraxisEffect.h b/src/light/effects/PraxisEffect.h index 20ccd5ec..4a6d726e 100644 --- a/src/light/effects/PraxisEffect.h +++ b/src/light/effects/PraxisEffect.h @@ -41,13 +41,13 @@ class PraxisEffect : public EffectBase { uint8_t microMutatorMax = 255; // micro mutator high end (0..255) void defineControls() override { - controls_.addUint8("speed", speed, 1, 64); - controls_.addUint8("macroMutatorFreq", macroMutatorFreq, 0, 15); - controls_.addUint8("macroMutatorMin", macroMutatorMin, 0, 255); - controls_.addUint8("macroMutatorMax", macroMutatorMax, 0, 255); - controls_.addUint8("microMutatorFreq", microMutatorFreq, 0, 15); - controls_.addUint8("microMutatorMin", microMutatorMin, 0, 255); - controls_.addUint8("microMutatorMax", microMutatorMax, 0, 255); + controls_.addControl("speed", speed, 1, 64); + controls_.addControl("macroMutatorFreq", macroMutatorFreq, 0, 15); + controls_.addControl("macroMutatorMin", macroMutatorMin, 0, 255); + controls_.addControl("macroMutatorMax", macroMutatorMax, 0, 255); + controls_.addControl("microMutatorFreq", microMutatorFreq, 0, 15); + controls_.addControl("microMutatorMin", microMutatorMin, 0, 255); + controls_.addControl("microMutatorMax", microMutatorMax, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/RainbowEffect.h b/src/light/effects/RainbowEffect.h index ffa80e4a..3d080fe4 100644 --- a/src/light/effects/RainbowEffect.h +++ b/src/light/effects/RainbowEffect.h @@ -17,7 +17,7 @@ class RainbowEffect : public EffectBase { uint8_t speed = 20; // BPM — one full hue cycle every 3 s; 60 (a whole rainbow per second) reads too fast void defineControls() override { - controls_.addUint8("speed", speed, 1, 255); + controls_.addControl("speed", speed, 1, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/RandomEffect.h b/src/light/effects/RandomEffect.h index df8ed293..b72c8a7d 100644 --- a/src/light/effects/RandomEffect.h +++ b/src/light/effects/RandomEffect.h @@ -28,7 +28,7 @@ class RandomEffect : public EffectBase { uint8_t fade = 70; // per-frame fadeToBlackBy amount (0..255) void defineControls() override { - controls_.addUint8("fade", fade, 0, 255); + controls_.addControl("fade", fade, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/RaymarchEffect.h b/src/light/effects/RaymarchEffect.h index cfcab50e..38739dac 100644 --- a/src/light/effects/RaymarchEffect.h +++ b/src/light/effects/RaymarchEffect.h @@ -40,11 +40,11 @@ class RaymarchEffect : public EffectBase { bool showFloor = true; void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 60); - controls_.addUint8("steps", steps, 8, 96); - controls_.addUint8("blend", blend, 0, 255); - controls_.addUint8("cameraY", cameraY, 0, 255); - controls_.addBool("showFloor", showFloor); + controls_.addControl("bpm", bpm, 0, 60); + controls_.addControl("steps", steps, 8, 96); + controls_.addControl("blend", blend, 0, 255); + controls_.addControl("cameraY", cameraY, 0, 255); + controls_.addControl("showFloor", showFloor); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/RingsEffect.h b/src/light/effects/RingsEffect.h index 31b51c0e..0e3ac2f7 100644 --- a/src/light/effects/RingsEffect.h +++ b/src/light/effects/RingsEffect.h @@ -28,10 +28,10 @@ class RingsEffect : public EffectBase { uint8_t hue_shift = 0; void defineControls() override { - controls_.addUint8("count", count, 1, 255); - controls_.addUint8("speed", speed, 1, 255); - controls_.addUint8("thickness", thickness, 1, 255); - controls_.addUint8("hue_shift", hue_shift, 0, 255); + controls_.addControl("count", count, 1, 255); + controls_.addControl("speed", speed, 1, 255); + controls_.addControl("thickness", thickness, 1, 255); + controls_.addControl("hue_shift", hue_shift, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/RipplesEffect.h b/src/light/effects/RipplesEffect.h index 6ab0fa90..316f4fee 100644 --- a/src/light/effects/RipplesEffect.h +++ b/src/light/effects/RipplesEffect.h @@ -30,8 +30,8 @@ class RipplesEffect : public EffectBase { uint8_t interval = 128; // wavefront spacing: low = tight rings, high = wide void defineControls() override { - controls_.addUint8("speed", speed, 0, 99); - controls_.addUint8("interval", interval, 1, 254); + controls_.addControl("speed", speed, 0, 99); + controls_.addControl("interval", interval, 1, 254); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/RubiksCubeEffect.h b/src/light/effects/RubiksCubeEffect.h index b91b537c..0da70d54 100644 --- a/src/light/effects/RubiksCubeEffect.h +++ b/src/light/effects/RubiksCubeEffect.h @@ -34,10 +34,10 @@ class RubiksCubeEffect : public EffectBase { // of the system-wide palette (advised: a primary-ish palette) void defineControls() override { - controls_.addUint8("turnsPerSecond", turnsPerSecond, 0, 20); - controls_.addUint8("cubeSize", cubeSize, 1, 8); - controls_.addBool("randomTurning", randomTurning); - controls_.addBool("usePalette", usePalette); + controls_.addControl("turnsPerSecond", turnsPerSecond, 0, 20); + controls_.addControl("cubeSize", cubeSize, 1, 8); + controls_.addControl("randomTurning", randomTurning); + controls_.addControl("usePalette", usePalette); } // The 6 face colors drawCube paints with. Classic Rubik's set (red, dark-orange, blue, green, diff --git a/src/light/effects/SdfShapesEffect.h b/src/light/effects/SdfShapesEffect.h index b09611c2..5f29f487 100644 --- a/src/light/effects/SdfShapesEffect.h +++ b/src/light/effects/SdfShapesEffect.h @@ -42,12 +42,12 @@ class SdfShapesEffect : public EffectBase { bool glow = true; // tint the surrounding field by distance void defineControls() override { - controls_.addUint8("bpm", bpm, 1, 120); - controls_.addUint8("radius", radius, 0, 255); - controls_.addUint8("boxSize", boxSize, 0, 255); - controls_.addUint8("blend", blend, 0, 255); - controls_.addUint8("outline", outline, 0, 64); - controls_.addBool("glow", glow); + controls_.addControl("bpm", bpm, 1, 120); + controls_.addControl("radius", radius, 0, 255); + controls_.addControl("boxSize", boxSize, 0, 255); + controls_.addControl("blend", blend, 0, 255); + controls_.addControl("outline", outline, 0, 64); + controls_.addControl("glow", glow); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/SineEffect.h b/src/light/effects/SineEffect.h index def4296b..e3291aae 100644 --- a/src/light/effects/SineEffect.h +++ b/src/light/effects/SineEffect.h @@ -29,9 +29,9 @@ class SineEffect : public EffectBase { uint8_t bpm = 30; // scroll speed (reshuffles per minute of the phase) void defineControls() override { - controls_.addUint8("frequency", frequency, 1, 20); - controls_.addUint8("amplitude", amplitude, 0, 255); - controls_.addUint8("bpm", bpm, 1, 255); + controls_.addControl("frequency", frequency, 1, 20); + controls_.addControl("amplitude", amplitude, 0, 255); + controls_.addControl("bpm", bpm, 1, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/SolidEffect.h b/src/light/effects/SolidEffect.h index 564cac6c..de6293a3 100644 --- a/src/light/effects/SolidEffect.h +++ b/src/light/effects/SolidEffect.h @@ -40,14 +40,14 @@ class SolidEffect : public EffectBase { static constexpr uint8_t kColorModeCount = 5; void defineControls() override { - controls_.addUint8("red", red, 0, 255); - controls_.addUint8("green", green, 0, 255); - controls_.addUint8("blue", blue, 0, 255); - controls_.addUint8("white", white, 0, 255); - controls_.addUint8("brightness", brightness, 0, 255); + controls_.addControl("red", red, 0, 255); + controls_.addControl("green", green, 0, 255); + controls_.addControl("blue", blue, 0, 255); + controls_.addControl("white", white, 0, 255); + controls_.addControl("brightness", brightness, 0, 255); controls_.addSelect("colorMode", colorMode, kColorModeOptions, kColorModeCount); - controls_.addUint8("minRGB", minRGB, 0, 255); - controls_.addBool("randomColors", randomColors); + controls_.addControl("minRGB", minRGB, 0, 255); + controls_.addControl("randomColors", randomColors); } // The band modes (3/4) need a 256-entry table of valid wheel indices. 256 bytes is small, but diff --git a/src/light/effects/SpectrumEffect.h b/src/light/effects/SpectrumEffect.h index fedfcf32..e4059de6 100644 --- a/src/light/effects/SpectrumEffect.h +++ b/src/light/effects/SpectrumEffect.h @@ -41,11 +41,11 @@ class SpectrumEffect : public EffectBase { bool colorByColumn = false; // color per band instead of by height void defineControls() override { - controls_.addUint8("attack", attack, 1, 255); - controls_.addUint8("release", release, 1, 128); - controls_.addUint8("peakDecay", peakDecay, 0, 32); - controls_.addBool("showPeaks", showPeaks); - controls_.addBool("colorByColumn", colorByColumn); + controls_.addControl("attack", attack, 1, 255); + controls_.addControl("release", release, 1, 128); + controls_.addControl("peakDecay", peakDecay, 0, 32); + controls_.addControl("showPeaks", showPeaks); + controls_.addControl("colorByColumn", colorByColumn); } void prepare() override { diff --git a/src/light/effects/SphereMoveEffect.h b/src/light/effects/SphereMoveEffect.h index 0567eec6..e0c40f49 100644 --- a/src/light/effects/SphereMoveEffect.h +++ b/src/light/effects/SphereMoveEffect.h @@ -30,7 +30,7 @@ class SphereMoveEffect : public EffectBase { uint8_t speed = 50; // origin sweep rate (0..99); higher = faster (divisor is 100-speed) void defineControls() override { - controls_.addUint8("speed", speed, 0, 99); + controls_.addControl("speed", speed, 0, 99); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/SpiralEffect.h b/src/light/effects/SpiralEffect.h index b533c727..055ef64d 100644 --- a/src/light/effects/SpiralEffect.h +++ b/src/light/effects/SpiralEffect.h @@ -19,9 +19,9 @@ class SpiralEffect : public EffectBase { uint8_t hue_shift = 0; void defineControls() override { - controls_.addUint8("bpm", bpm, 1, 255); - controls_.addUint8("twist", twist, 1, 255); - controls_.addUint8("hue_shift", hue_shift, 0, 255); + controls_.addControl("bpm", bpm, 1, 255); + controls_.addControl("twist", twist, 1, 255); + controls_.addControl("hue_shift", hue_shift, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/StarFieldEffect.h b/src/light/effects/StarFieldEffect.h index cf89ae8b..0d2afb4d 100644 --- a/src/light/effects/StarFieldEffect.h +++ b/src/light/effects/StarFieldEffect.h @@ -40,10 +40,10 @@ class StarFieldEffect : public EffectBase { bool usePalette = false; // color stars from the palette instead of greyscale void defineControls() override { - controls_.addUint8("speed", speed, 0, 30); - controls_.addUint8("numStars", numStars, 1, 255); - controls_.addUint8("blur", blur, 0, 255); - controls_.addBool("usePalette", usePalette); + controls_.addControl("speed", speed, 0, 30); + controls_.addControl("numStars", numStars, 1, 255); + controls_.addControl("blur", blur, 0, 255); + controls_.addControl("usePalette", usePalette); } // Star table is sized to the control maximum (255) so a live numStars change never reallocates, diff --git a/src/light/effects/StarSkyEffect.h b/src/light/effects/StarSkyEffect.h index fb410192..5513f7a5 100644 --- a/src/light/effects/StarSkyEffect.h +++ b/src/light/effects/StarSkyEffect.h @@ -32,9 +32,9 @@ class StarSkyEffect : public EffectBase { bool usePalette = false; // false → white stars; true → per-star palette color void defineControls() override { - controls_.addUint8("speed", speed, 0, 42); - controls_.addUint8("star_fill_ratio", star_fill_ratio, 0, 255); - controls_.addBool("usePalette", usePalette); + controls_.addControl("speed", speed, 0, 42); + controls_.addControl("star_fill_ratio", star_fill_ratio, 0, 255); + controls_.addControl("usePalette", usePalette); } // Per-star state on the heap, sized to nb_stars = star_fill_ratio*nrOfLights/10000 + 1. Reallocated diff --git a/src/light/effects/TetrixEffect.h b/src/light/effects/TetrixEffect.h index 5c00a476..577cad3b 100644 --- a/src/light/effects/TetrixEffect.h +++ b/src/light/effects/TetrixEffect.h @@ -33,9 +33,9 @@ class TetrixEffect : public EffectBase { bool oneColor = false; // all bricks in a column share one slowly-advancing color void defineControls() override { - controls_.addUint8("speed", speedControl, 0, 255); - controls_.addUint8("width", widthControl, 0, 255); - controls_.addBool("oneColor", oneColor); + controls_.addControl("speed", speedControl, 0, 255); + controls_.addControl("width", widthControl, 0, 255); + controls_.addControl("oneColor", oneColor); } // Per-column falling-brick state (MoonLight's Tetris struct). `step` doubles as the state diff --git a/src/light/effects/TextEffect.h b/src/light/effects/TextEffect.h index 5214bccf..f3e4b39e 100644 --- a/src/light/effects/TextEffect.h +++ b/src/light/effects/TextEffect.h @@ -35,11 +35,11 @@ class TextEffect : public EffectBase { void defineControls() override { controls_.addTextArea("text", text_, sizeof(text_)); - controls_.addBool("scroll", scroll); + controls_.addControl("scroll", scroll); static constexpr const char* kFontOptions[] = {"4x6", "6x8"}; controls_.addSelect("font", font, kFontOptions, fonts::kCount); - controls_.addUint8("speed", speed, 1, 255); - controls_.addUint8("hue", hue, 0, 255); + controls_.addControl("speed", speed, 1, 255); + controls_.addControl("hue", hue, 0, 255); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/TruchetEffect.h b/src/light/effects/TruchetEffect.h index df616038..c705951d 100644 --- a/src/light/effects/TruchetEffect.h +++ b/src/light/effects/TruchetEffect.h @@ -48,12 +48,12 @@ class TruchetEffect : public EffectBase { bool drift = true; // slide the pattern instead of holding still void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 60); - controls_.addUint8("scale", scale, 1, 16); - controls_.addUint8("thickness", thickness, 5, 200); - controls_.addUint8("softness", softness, 1, 200); - controls_.addUint8("shuffle", shuffle, 0, 255); - controls_.addBool("drift", drift); + controls_.addControl("bpm", bpm, 0, 60); + controls_.addControl("scale", scale, 1, 16); + controls_.addControl("thickness", thickness, 5, 200); + controls_.addControl("softness", softness, 1, 200); + controls_.addControl("shuffle", shuffle, 0, 255); + controls_.addControl("drift", drift); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/TunnelEffect.h b/src/light/effects/TunnelEffect.h index 350ab78e..79a4e63c 100644 --- a/src/light/effects/TunnelEffect.h +++ b/src/light/effects/TunnelEffect.h @@ -39,12 +39,12 @@ class TunnelEffect : public EffectBase { bool vignette = true; // darken toward the vanishing point so it reads as receding void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 120); - controls_.addUint8("depth", depth, 1, 255); - controls_.addUint8("twist", twist, 0, 255); - controls_.addUint8("segments", segments, 1, 16); - controls_.addUint8("octaves", octaves, 1, 4); - controls_.addBool("vignette", vignette); + controls_.addControl("bpm", bpm, 0, 120); + controls_.addControl("depth", depth, 1, 255); + controls_.addControl("twist", twist, 0, 255); + controls_.addControl("segments", segments, 1, 16); + controls_.addControl("octaves", octaves, 1, 4); + controls_.addControl("vignette", vignette); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/VectorBallsEffect.h b/src/light/effects/VectorBallsEffect.h index 9eadcf2b..c5fe9bfe 100644 --- a/src/light/effects/VectorBallsEffect.h +++ b/src/light/effects/VectorBallsEffect.h @@ -45,11 +45,11 @@ class VectorBallsEffect : public EffectBase { bool fade = true; // dim the far balls, which is what reads as depth void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 60); - controls_.addUint8("size", size, 1, 12); - controls_.addUint8("spread", spread, 20, 255); - controls_.addUint8("distance", distance, 40, 255); - controls_.addBool("fade", fade); + controls_.addControl("bpm", bpm, 0, 60); + controls_.addControl("size", size, 1, 12); + controls_.addControl("spread", spread, 20, 255); + controls_.addControl("distance", distance, 40, 255); + controls_.addControl("fade", fade); } void tick() MM_NONBLOCKING override { diff --git a/src/light/effects/WaterRippleEffect.h b/src/light/effects/WaterRippleEffect.h index 6ea2e460..a97b391f 100644 --- a/src/light/effects/WaterRippleEffect.h +++ b/src/light/effects/WaterRippleEffect.h @@ -43,13 +43,13 @@ class WaterRippleEffect : public EffectBase { uint8_t hueSpread = 110; // how far a crest and a trough reach from it void defineControls() override { - controls_.addUint8("speed", speed, 1, 120); - controls_.addUint8("dropRate", dropRate, 0, 255); - controls_.addUint8("damping", damping, 1, 64); - controls_.addUint8("strength", strength, 1, 255); - controls_.addBool("colorByHeight", colorByHeight); - controls_.addUint8("hueBase", hueBase, 0, 255); - controls_.addUint8("hueSpread", hueSpread, 0, 127); + controls_.addControl("speed", speed, 1, 120); + controls_.addControl("dropRate", dropRate, 0, 255); + controls_.addControl("damping", damping, 1, 64); + controls_.addControl("strength", strength, 1, 255); + controls_.addControl("colorByHeight", colorByHeight); + controls_.addControl("hueBase", hueBase, 0, 255); + controls_.addControl("hueSpread", hueSpread, 0, 127); } void prepare() override { diff --git a/src/light/effects/WaveEffect.h b/src/light/effects/WaveEffect.h index 56c5fa40..c7d85e55 100644 --- a/src/light/effects/WaveEffect.h +++ b/src/light/effects/WaveEffect.h @@ -38,8 +38,8 @@ class WaveEffect : public EffectBase { uint8_t type = 2; // index into kTypeOptions (default Sine) void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 255); - controls_.addUint8("fade", fade, 0, 255); + controls_.addControl("bpm", bpm, 0, 255); + controls_.addControl("fade", fade, 0, 255); controls_.addSelect("type", type, kTypeOptions, kTypeCount); } diff --git a/src/light/layers/Layer.h b/src/light/layers/Layer.h index 2123050d..39f2ddf2 100644 --- a/src/light/layers/Layer.h +++ b/src/light/layers/Layer.h @@ -59,7 +59,7 @@ class Layer : public MoonModule { void defineControls() override { static constexpr const char* kBlendModeOptions[] = {"alpha", "additive"}; controls_.addSelect("blendMode", blendMode, kBlendModeOptions, 2); - controls_.addUint8("opacity", opacity, 0, 255); + controls_.addControl("opacity", opacity, 0, 255); // Cascade to children (effects and modifiers) — preserves the default // base behaviour we just overrode. MoonModule::defineControls(); diff --git a/src/light/layouts/CarLightsLayout.h b/src/light/layouts/CarLightsLayout.h index 6da71494..2550ebf3 100644 --- a/src/light/layouts/CarLightsLayout.h +++ b/src/light/layouts/CarLightsLayout.h @@ -40,7 +40,7 @@ class CarLightsLayout : public LayoutBase { uint8_t scale = 2; // 1..10 — spacing multiplier out from each centre void defineControls() override { - controls_.addUint8("scale", scale, 1, 10); + controls_.addControl("scale", scale, 1, 10); } const char* tags() const override { return "💫"; } diff --git a/src/light/layouts/CubeLayout.h b/src/light/layouts/CubeLayout.h index 00af79a7..f2d4f020 100644 --- a/src/light/layouts/CubeLayout.h +++ b/src/light/layouts/CubeLayout.h @@ -50,16 +50,16 @@ class CubeLayout : public LayoutBase { bool snakeZ = false; void defineControls() override { - controls_.addInt16("width", width, 1, 128); - controls_.addInt16("height", height, 1, 128); - controls_.addInt16("depth", depth, 1, 128); + controls_.addControl("width", width, 1, 128); + controls_.addControl("height", height, 1, 128); + controls_.addControl("depth", depth, 1, 128); controls_.addSelect("wiringOrder", wiringOrder, kWiringOrderOptions, kWiringOrderCount); - controls_.addBool("X++", incX); - controls_.addBool("Y++", incY); - controls_.addBool("Z++", incZ); - controls_.addBool("snakeX", snakeX); - controls_.addBool("snakeY", snakeY); - controls_.addBool("snakeZ", snakeZ); + controls_.addControl("X++", incX); + controls_.addControl("Y++", incY); + controls_.addControl("Z++", incZ); + controls_.addControl("snakeX", snakeX); + controls_.addControl("snakeY", snakeY); + controls_.addControl("snakeZ", snakeZ); } const char* tags() const override { return "💫"; } // MoonLight origin diff --git a/src/light/layouts/GridBlacksLayout.h b/src/light/layouts/GridBlacksLayout.h index e2cec5b1..6f435c3c 100644 --- a/src/light/layouts/GridBlacksLayout.h +++ b/src/light/layouts/GridBlacksLayout.h @@ -27,12 +27,12 @@ class GridBlacksLayout : public LayoutBase { lengthType blackCount = 0; // number of dark columns; 0 = no gap (renders like a plain Grid) void defineControls() override { - controls_.addInt16("width", width, 1, 512); - controls_.addInt16("height", height, 1, 512); - controls_.addInt16("depth", depth, 1, 512); - controls_.addBool("serpentine", serpentine); - controls_.addInt16("blackCount", blackCount, 0, 512); - controls_.addInt16("blackStart", blackStart, 0, 512); + controls_.addControl("width", width, 1, 512); + controls_.addControl("height", height, 1, 512); + controls_.addControl("depth", depth, 1, 512); + controls_.addControl("serpentine", serpentine); + controls_.addControl("blackCount", blackCount, 0, 512); + controls_.addControl("blackStart", blackStart, 0, 512); controls_.setHidden(controls_.count() - 1, blackCount == 0); // blackStart matters only with a run } diff --git a/src/light/layouts/GridLayout.h b/src/light/layouts/GridLayout.h index 4df64d9a..8d9a3c6e 100644 --- a/src/light/layouts/GridLayout.h +++ b/src/light/layouts/GridLayout.h @@ -22,10 +22,10 @@ class GridLayout : public LayoutBase { // strip layout where the strip snakes back and forth row to row. void defineControls() override { - controls_.addInt16("width", width, 1, 512); - controls_.addInt16("height", height, 1, 512); - controls_.addInt16("depth", depth, 1, 512); - controls_.addBool("serpentine", serpentine); + controls_.addControl("width", width, 1, 512); + controls_.addControl("height", height, 1, 512); + controls_.addControl("depth", depth, 1, 512); + controls_.addControl("serpentine", serpentine); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/HumanSizedCubeLayout.h b/src/light/layouts/HumanSizedCubeLayout.h index 96157d0d..be67dc01 100644 --- a/src/light/layouts/HumanSizedCubeLayout.h +++ b/src/light/layouts/HumanSizedCubeLayout.h @@ -30,9 +30,9 @@ class HumanSizedCubeLayout : public LayoutBase { uint8_t depth = 10; void defineControls() override { - controls_.addUint8("width", width, 1, 20); - controls_.addUint8("height", height, 1, 20); - controls_.addUint8("depth", depth, 1, 20); + controls_.addControl("width", width, 1, 20); + controls_.addControl("height", height, 1, 20); + controls_.addControl("depth", depth, 1, 20); } const char* tags() const override { return "💫"; } diff --git a/src/light/layouts/PanelLayout.h b/src/light/layouts/PanelLayout.h index 1e2d3619..2a87d752 100644 --- a/src/light/layouts/PanelLayout.h +++ b/src/light/layouts/PanelLayout.h @@ -45,12 +45,12 @@ class PanelLayout : public LayoutBase { void defineControls() override { // Geometry only — MoonLight's pin controls (ledPin selects, nextPin) are dropped. // Ranges from MoonLight (1..65536), clamped to lengthType's int16_t max (512-safe). - controls_.addInt16("panelWidth", panelWidth, 1, 512); - controls_.addInt16("panelHeight", panelHeight, 1, 512); + controls_.addControl("panelWidth", panelWidth, 1, 512); + controls_.addControl("panelHeight", panelHeight, 1, 512); controls_.addSelect("wiringOrder", wiringOrder, kWiringOptions, kWiringCount); - controls_.addBool("X++", incX); - controls_.addBool("Y++", incY); - controls_.addBool("snake", snake); + controls_.addControl("X++", incX); + controls_.addControl("Y++", incY); + controls_.addControl("snake", snake); } const char* tags() const override { return "💫"; } diff --git a/src/light/layouts/PanelsLayout.h b/src/light/layouts/PanelsLayout.h index 75e9a7d7..feeb09b6 100644 --- a/src/light/layouts/PanelsLayout.h +++ b/src/light/layouts/PanelsLayout.h @@ -50,20 +50,20 @@ class PanelsLayout : public LayoutBase { void defineControls() override { // Panel grid (outer). MoonLight ranges 1..32; clamped to lengthType (int16_t). - controls_.addInt16("horizontalPanels", horizontalPanels, 1, 32); - controls_.addInt16("verticalPanels", verticalPanels, 1, 32); + controls_.addControl("horizontalPanels", horizontalPanels, 1, 32); + controls_.addControl("verticalPanels", verticalPanels, 1, 32); controls_.addSelect("wiringOrderP", wiringOrderP, kWiringOptions, kWiringCount); - controls_.addBool("X++P", incXP); - controls_.addBool("Y++P", incYP); - controls_.addBool("snakeP", snakeP); + controls_.addControl("X++P", incXP); + controls_.addControl("Y++P", incYP); + controls_.addControl("snakeP", snakeP); // Per-panel (inner). MoonLight ranges 1..65536; clamped to int16_t max (512-safe). - controls_.addInt16("panelWidth", panelWidth, 1, 512); - controls_.addInt16("panelHeight", panelHeight, 1, 512); + controls_.addControl("panelWidth", panelWidth, 1, 512); + controls_.addControl("panelHeight", panelHeight, 1, 512); controls_.addSelect("wiringOrder", wiringOrder, kWiringOptions, kWiringCount); - controls_.addBool("X++", incX); - controls_.addBool("Y++", incY); - controls_.addBool("snake", snake); + controls_.addControl("X++", incX); + controls_.addControl("Y++", incY); + controls_.addControl("snake", snake); } const char* tags() const override { return "💫"; } diff --git a/src/light/layouts/RingLayout.h b/src/light/layouts/RingLayout.h index 4328bfc4..8c8cfb9e 100644 --- a/src/light/layouts/RingLayout.h +++ b/src/light/layouts/RingLayout.h @@ -31,11 +31,11 @@ class RingLayout : public LayoutBase { uint8_t scale = 1; // 1..10 — spacing multiplier out from the centre void defineControls() override { - controls_.addUint8("nrOfLEDs", nrOfLEDs, 1, 255); - controls_.addUint16("angleFirst", angleFirst, 0, 359); - controls_.addUint16("rotation", rotation, 0, 360); - controls_.addBool("clockwise", clockwise); - controls_.addUint8("scale", scale, 1, 10); + controls_.addControl("nrOfLEDs", nrOfLEDs, 1, 255); + controls_.addControl("angleFirst", angleFirst, 0, 359); + controls_.addControl("rotation", rotation, 0, 360); + controls_.addControl("clockwise", clockwise); + controls_.addControl("scale", scale, 1, 10); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/Rings241Layout.h b/src/light/layouts/Rings241Layout.h index e13dfc80..6640906b 100644 --- a/src/light/layouts/Rings241Layout.h +++ b/src/light/layouts/Rings241Layout.h @@ -36,7 +36,7 @@ class Rings241Layout : public LayoutBase { uint8_t scale = 2; void defineControls() override { - controls_.addUint8("scale", scale, 1, 10); + controls_.addControl("scale", scale, 1, 10); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/SingleColumnLayout.h b/src/light/layouts/SingleColumnLayout.h index 1f6084b6..a2cb3026 100644 --- a/src/light/layouts/SingleColumnLayout.h +++ b/src/light/layouts/SingleColumnLayout.h @@ -22,10 +22,10 @@ class SingleColumnLayout : public LayoutBase { bool reversed_order = false; // "reversed order" void defineControls() override { - controls_.addUint8("starting Y", start_y, 0, 255); - controls_.addUint16("height", height, 1, 1000); - controls_.addUint16("X position", xposition, 0, 255); - controls_.addBool("reversed order", reversed_order); + controls_.addControl("starting Y", start_y, 0, 255); + controls_.addControl("height", height, 1, 1000); + controls_.addControl("X position", xposition, 0, 255); + controls_.addControl("reversed order", reversed_order); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/SingleRowLayout.h b/src/light/layouts/SingleRowLayout.h index 4fd38e3d..f130d817 100644 --- a/src/light/layouts/SingleRowLayout.h +++ b/src/light/layouts/SingleRowLayout.h @@ -31,10 +31,10 @@ class SingleRowLayout : public LayoutBase { bool reversedOrder = false; void defineControls() override { - controls_.addUint8("starting X", startX, 0, 255); - controls_.addUint16("width", width, 1, 1000); - controls_.addUint16("Y position", yPosition, 0, 255); - controls_.addBool("reversed order", reversedOrder); + controls_.addControl("starting X", startX, 0, 255); + controls_.addControl("width", width, 1, 1000); + controls_.addControl("Y position", yPosition, 0, 255); + controls_.addControl("reversed order", reversedOrder); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/SphereLayout.h b/src/light/layouts/SphereLayout.h index e7e66858..93eb278b 100644 --- a/src/light/layouts/SphereLayout.h +++ b/src/light/layouts/SphereLayout.h @@ -26,7 +26,7 @@ class SphereLayout : public LayoutBase { lengthType radius = 4; void defineControls() override { - controls_.addInt16("radius", radius, 1, 64); + controls_.addControl("radius", radius, 1, 64); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/SpiralLayout.h b/src/light/layouts/SpiralLayout.h index 4a65ae8a..daca8563 100644 --- a/src/light/layouts/SpiralLayout.h +++ b/src/light/layouts/SpiralLayout.h @@ -29,9 +29,9 @@ class SpiralLayout : public LayoutBase { lengthType height = 25; // vertical rise from base to tip void defineControls() override { - controls_.addInt16("ledCount", ledCount, 1, 2048); - controls_.addInt16("bottomRadius", bottomRadius, 1, 100); - controls_.addInt16("height", height, 1, 200); + controls_.addControl("ledCount", ledCount, 1, 2048); + controls_.addControl("bottomRadius", bottomRadius, 1, 100); + controls_.addControl("height", height, 1, 200); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/TorontoBarGourdsLayout.h b/src/light/layouts/TorontoBarGourdsLayout.h index 8ed2e15e..21d3b1aa 100644 --- a/src/light/layouts/TorontoBarGourdsLayout.h +++ b/src/light/layouts/TorontoBarGourdsLayout.h @@ -42,7 +42,7 @@ class TorontoBarGourdsLayout : public LayoutBase { void defineControls() override { controls_.addSelect("granularity", granularity, kGranularityOptions, kGranularityCount); // Mode 0 only; MoonLight range 1..128. - controls_.addUint8("nrOfLightsPerGourd", nrOfLightsPerGourd, 1, 128); + controls_.addControl("nrOfLightsPerGourd", nrOfLightsPerGourd, 1, 128); } const char* tags() const override { return "💫"; } diff --git a/src/light/layouts/TubesLayout.h b/src/light/layouts/TubesLayout.h index 0fd46a19..686836ca 100644 --- a/src/light/layouts/TubesLayout.h +++ b/src/light/layouts/TubesLayout.h @@ -30,10 +30,10 @@ class TubesLayout : public LayoutBase { // MoonLight's counterparts are bare "slider" controls (uint8_t, 0..255). // These explicit ranges hold the geometry (≥1 tube of ≥1 light, // non-negative spacing) while keeping the box bounded. - controls_.addInt16("nrOfTubes", nrOfTubes, 1, 64); - controls_.addInt16("ledsPerTube", ledsPerTube, 1, 255); - controls_.addInt16("tubeDistance", tubeDistance, 0, 255); - controls_.addBool("reversed", reversed); + controls_.addControl("nrOfTubes", nrOfTubes, 1, 64); + controls_.addControl("ledsPerTube", ledsPerTube, 1, 255); + controls_.addControl("tubeDistance", tubeDistance, 0, 255); + controls_.addControl("reversed", reversed); } nrOfLightsType lightCount() const override { diff --git a/src/light/layouts/WheelLayout.h b/src/light/layouts/WheelLayout.h index aacf8f62..7232c4fd 100644 --- a/src/light/layouts/WheelLayout.h +++ b/src/light/layouts/WheelLayout.h @@ -26,8 +26,8 @@ class WheelLayout : public LayoutBase { uint16_t ledsPerSpoke = 10; // LEDs along each spoke, 1..256 void defineControls() override { - controls_.addUint16("spokes", spokes, 2, 64); - controls_.addUint16("ledsPerSpoke", ledsPerSpoke, 1, 256); + controls_.addControl("spokes", spokes, 2, 64); + controls_.addControl("ledsPerSpoke", ledsPerSpoke, 1, 256); } nrOfLightsType lightCount() const override { diff --git a/src/light/modifiers/CheckerboardModifier.h b/src/light/modifiers/CheckerboardModifier.h index 7aa8d185..39008387 100644 --- a/src/light/modifiers/CheckerboardModifier.h +++ b/src/light/modifiers/CheckerboardModifier.h @@ -22,8 +22,8 @@ class CheckerboardModifier : public ModifierBase { bool invert = false; // flip which squares pass through void defineControls() override { - controls_.addUint8("size", size, 1, 64); - controls_.addBool("invert", invert); + controls_.addControl("size", size, 1, 64); + controls_.addControl("invert", invert); } // A mask leaves the logical box unchanged (no modifyLogicalSize override). diff --git a/src/light/modifiers/MirrorModifier.h b/src/light/modifiers/MirrorModifier.h index 1a002bdd..d0cdb433 100644 --- a/src/light/modifiers/MirrorModifier.h +++ b/src/light/modifiers/MirrorModifier.h @@ -37,9 +37,9 @@ class MirrorModifier : public ModifierBase { bool mirrorZ = true; void defineControls() override { - controls_.addBool("mirrorX", mirrorX); - controls_.addBool("mirrorY", mirrorY); - controls_.addBool("mirrorZ", mirrorZ); + controls_.addControl("mirrorX", mirrorX); + controls_.addControl("mirrorY", mirrorY); + controls_.addControl("mirrorZ", mirrorZ); } void modifyLogicalSize(Coord3D& size) override { diff --git a/src/light/modifiers/MultiplyModifier.h b/src/light/modifiers/MultiplyModifier.h index 792a33ea..5ddab52c 100644 --- a/src/light/modifiers/MultiplyModifier.h +++ b/src/light/modifiers/MultiplyModifier.h @@ -40,12 +40,12 @@ class MultiplyModifier : public ModifierBase { void defineControls() override { // 1–64 tiles per axis. More tiles than the grid has pixels just yields // 1-pixel tiles (the effective multiplier clamps to the axis extent). - controls_.addUint8("multiplyX", multiplyX, 1, 64); - controls_.addUint8("multiplyY", multiplyY, 1, 64); - controls_.addUint8("multiplyZ", multiplyZ, 1, 64); - controls_.addBool("mirrorX", mirrorX); - controls_.addBool("mirrorY", mirrorY); - controls_.addBool("mirrorZ", mirrorZ); + controls_.addControl("multiplyX", multiplyX, 1, 64); + controls_.addControl("multiplyY", multiplyY, 1, 64); + controls_.addControl("multiplyZ", multiplyZ, 1, 64); + controls_.addControl("mirrorX", mirrorX); + controls_.addControl("mirrorY", mirrorY); + controls_.addControl("mirrorZ", mirrorZ); } void modifyLogicalSize(Coord3D& size) override { diff --git a/src/light/modifiers/PinwheelModifier.h b/src/light/modifiers/PinwheelModifier.h index 7f7e5648..98e1cee7 100644 --- a/src/light/modifiers/PinwheelModifier.h +++ b/src/light/modifiers/PinwheelModifier.h @@ -30,18 +30,18 @@ class PinwheelModifier : public ModifierBase { uint8_t petals = 60; // Signed: negative values reverse the swirl direction. MoonLight's slider // range is -127..127. int16_t because the control system's signed type is - // addInt16 (there is no addInt8); the value stays within ±127. + // an int16 control (there is deliberately no int8 one — see Control.h); the value stays within ±127. int16_t swirl = 30; bool reverse = false; uint8_t symmetry = 1; uint8_t zTwist = 0; void defineControls() override { - controls_.addUint8("petals", petals); - controls_.addInt16("swirl", swirl, -127, 127); - controls_.addBool("reverse", reverse); - controls_.addUint8("symmetry", symmetry); - controls_.addUint8("zTwist", zTwist); + controls_.addControl("petals", petals); + controls_.addControl("swirl", swirl, -127, 127); + controls_.addControl("reverse", reverse); + controls_.addControl("symmetry", symmetry); + controls_.addControl("zTwist", zTwist); } void modifyLogicalSize(Coord3D& size) override { diff --git a/src/light/modifiers/RandomMapModifier.h b/src/light/modifiers/RandomMapModifier.h index f9955509..7511c39c 100644 --- a/src/light/modifiers/RandomMapModifier.h +++ b/src/light/modifiers/RandomMapModifier.h @@ -34,7 +34,7 @@ class RandomMapModifier : public ModifierBase { ~RandomMapModifier() override { releasePerm(); } void defineControls() override { - controls_.addUint8("bpm", bpm, 0, 60); + controls_.addControl("bpm", bpm, 0, 60); } // `bpm` only changes future reshuffle timing — the current permutation is unchanged, diff --git a/src/light/modifiers/RegionModifier.h b/src/light/modifiers/RegionModifier.h index 08697568..c455e614 100644 --- a/src/light/modifiers/RegionModifier.h +++ b/src/light/modifiers/RegionModifier.h @@ -44,12 +44,12 @@ class RegionModifier : public ModifierBase { void defineControls() override { // Int16 so negative / >100 percentages round-trip; the carve math clamps. - controls_.addInt16("startX", startX); - controls_.addInt16("startY", startY); - controls_.addInt16("startZ", startZ); - controls_.addInt16("endX", endX); - controls_.addInt16("endY", endY); - controls_.addInt16("endZ", endZ); + controls_.addControl("startX", startX); + controls_.addControl("startY", startY); + controls_.addControl("startZ", startZ); + controls_.addControl("endX", endX); + controls_.addControl("endY", endY); + controls_.addControl("endZ", endZ); } void modifyLogicalSize(Coord3D& size) override { diff --git a/src/light/modifiers/RippleXZModifier.h b/src/light/modifiers/RippleXZModifier.h index 13e89837..de33c3bc 100644 --- a/src/light/modifiers/RippleXZModifier.h +++ b/src/light/modifiers/RippleXZModifier.h @@ -35,9 +35,9 @@ class RippleXZModifier : public ModifierBase { bool towardsZ = false; void defineControls() override { - controls_.addBool("shrink", shrink); - controls_.addBool("towardsX", towardsX); - controls_.addBool("towardsZ", towardsZ); + controls_.addControl("shrink", shrink); + controls_.addControl("towardsX", towardsX); + controls_.addControl("towardsZ", towardsZ); } void modifyLogicalSize(Coord3D& size) override { diff --git a/src/light/modifiers/RotateModifier.h b/src/light/modifiers/RotateModifier.h index 8ed408d7..52714a7f 100644 --- a/src/light/modifiers/RotateModifier.h +++ b/src/light/modifiers/RotateModifier.h @@ -37,7 +37,7 @@ class RotateModifier : public ModifierBase { uint8_t speed = 1; // rotation speed, 1..255 (turns faster as it rises) void defineControls() override { - controls_.addUint8("speed", speed, 1, 255); + controls_.addControl("speed", speed, 1, 255); } // `speed` only changes how fast the angle advances; rotation is applied live in diff --git a/src/light/modifiers/TransposeModifier.h b/src/light/modifiers/TransposeModifier.h index 3afeba55..a4f54bd1 100644 --- a/src/light/modifiers/TransposeModifier.h +++ b/src/light/modifiers/TransposeModifier.h @@ -32,12 +32,12 @@ class TransposeModifier : public ModifierBase { bool inverseZ = false; void defineControls() override { - controls_.addBool("XY", transposeXY); - controls_.addBool("XZ", transposeXZ); - controls_.addBool("YZ", transposeYZ); - controls_.addBool("inverse X", inverseX); - controls_.addBool("inverse Y", inverseY); - controls_.addBool("inverse Z", inverseZ); + controls_.addControl("XY", transposeXY); + controls_.addControl("XZ", transposeXZ); + controls_.addControl("YZ", transposeYZ); + controls_.addControl("inverse X", inverseX); + controls_.addControl("inverse Y", inverseY); + controls_.addControl("inverse Z", inverseZ); } void modifyLogicalSize(Coord3D& size) override { diff --git a/src/light/moonlive/MoonLiveBuiltins_light.h b/src/light/moonlive/MoonLiveBuiltins_light.h index 103f897a..ec363fbd 100644 --- a/src/light/moonlive/MoonLiveBuiltins_light.h +++ b/src/light/moonlive/MoonLiveBuiltins_light.h @@ -621,9 +621,9 @@ inline void setPoolSink(particles::Pool* pool, uint32_t scale) MM_NONBLOCKING { if (!pool) detail::releaseIfEmpty(s); } -/// Point addUint8 at a consumer for the duration of one defineControls() run; nullptr to detach. +/// Point addControl at a consumer for the duration of one defineControls() run; nullptr to detach. /// False when the two-slot table is full, which the caller must not treat as an installed sink: -/// every addUint8 would then be a silent no-op and the script would publish no controls at all. +/// every addControl would then be a silent no-op and the script would publish no controls at all. inline bool setAddControlSink(AddControlFn fn, void* ctx) { detail::SinkSlot* s = detail::ownedSlot(fn != nullptr); if (!s) return false; @@ -652,7 +652,7 @@ inline void setAddLightSink(AddLightFn fn, void* ctx) { if (s) s->sink = {fn, ctx}; } -// addUint8(name, memberOffset, min, max): the run-time half of declaring a control. +// addControl(name, memberOffset, min, max): the run-time half of declaring a control. // // The CONTROL RECORD is built by the compiler, which knows the name span and the member's offset, // so nothing has to travel through a frame slot into a source buffer that is freed by the time @@ -660,9 +660,7 @@ inline void setAddLightSink(AddLightFn fn, void* ctx) { // way a compiled module does: `defineControls()` is an ordinary function the binding calls after a // successful compile, and this is an ordinary builtin it calls. // The one control declaration. What kind of control it becomes is read from the MEMBER'S declared -// type rather than chosen by the call, which is what removed the width-matched pair this replaces: -// addUint8 on a wide member drove only its low byte and addUint16 on a narrow one wrote past it, -// both silently, and the script author had to keep call and declaration in agreement by hand. +// type rather than chosen by the call, so a call and a declaration cannot disagree. inline uint32_t addControlDecl(const uintptr_t* args, CtrlType type) { // args: (name, memberOffset, min, max). The name is a pointer into the compiled program's // string pool, which outlives the run; the offset is the member's arena byte, which the @@ -1159,7 +1157,7 @@ inline BuiltinTable lightBuiltins() { /// /// A compiled module's controls exist because `defineControls()` RAN: the Scheduler calls it on /// every module at setup, and again whenever a Select reshapes the visible set. A scripted one -/// works the same way. This calls the entry point, each `addUint8` inside it reaches the engine +/// works the same way. This calls the entry point, each `addControl` inside it reaches the engine /// through the control sink, and the binding's `rebuildControls()` then finds a populated list. /// /// Re-runnable, like its compiled counterpart: the list is cleared first, so calling it twice diff --git a/src/light/moonlive/MoonLiveEffect.h b/src/light/moonlive/MoonLiveEffect.h index a89c4ae9..7520593d 100644 --- a/src/light/moonlive/MoonLiveEffect.h +++ b/src/light/moonlive/MoonLiveEffect.h @@ -27,7 +27,7 @@ class MoonLiveEffect : public EffectBase { Dim dimensions() const override { return Dim::D2; } // The effect carries its script's NAME as an editable, persisted text control, plus a control - // for every control the script declared (`addUint8("speed", speed, 0, 99)`). The + // for every control the script declared (`addControl("speed", speed, 0, 99)`). The // engine exposes the declared list after a compile; each becomes a real uint8 control bound by // reference to the engine's live control-arena slot, so a slider write lands in the slot the // next render tick reads, with no recompile (the live-edit guarantee). Naming a different diff --git a/src/light/moonlive/MoonLiveLayout.h b/src/light/moonlive/MoonLiveLayout.h index 1f0117bb..2f4c8eb9 100644 --- a/src/light/moonlive/MoonLiveLayout.h +++ b/src/light/moonlive/MoonLiveLayout.h @@ -48,7 +48,7 @@ class MoonLiveLayout : public LayoutBase { // RECEIVE a width: the pipeline derives its bounding box from the coordinates the layouts // actually place (Layouts::prepare, "max coordinate + 1 per axis"), so a width handed in // from outside would be a second, disagreeing source of truth. A script that wants one - // declares it under its OWN name (`addUint8("cols", cols, 1, 64)`) and it becomes a real + // declares it under its OWN name (`addControl("cols", cols, 1, 64)`) and it becomes a real // slider. Not `width`: that is a system variable the engine writes, so a script cannot // declare it and the compiler refuses the name. script_.publishDeclaredControls(controls_); diff --git a/src/light/moonlive/MoonLiveScript.h b/src/light/moonlive/MoonLiveScript.h index 1ad6e9ee..e09bde84 100644 --- a/src/light/moonlive/MoonLiveScript.h +++ b/src/light/moonlive/MoonLiveScript.h @@ -172,15 +172,15 @@ class MoonLiveScript { // One write at publish time settles it; every later write comes through // applyControlValue's parseBool, which yields 0 or 1 by construction. *slot = (*slot != 0) ? 1 : 0; - controls.addBool(decls[i].name, *reinterpret_cast(slot)); + controls.addControl(decls[i].name, *reinterpret_cast(slot)); break; case moonlive::CtrlType::Byte: - controls.addUint8(decls[i].name, *slot, + controls.addControl(decls[i].name, *slot, static_cast(decls[i].min), static_cast(decls[i].max)); break; default: // Int; Fixed and Str never reach here (the compiler refuses to bind one) - controls.addInt32(decls[i].name, *reinterpret_cast(slot), + controls.addControl(decls[i].name, *reinterpret_cast(slot), decls[i].min, decls[i].max); break; } diff --git a/src/light/moonlive/MoonLiveScriptFile.h b/src/light/moonlive/MoonLiveScriptFile.h index dd373d1b..71831622 100644 --- a/src/light/moonlive/MoonLiveScriptFile.h +++ b/src/light/moonlive/MoonLiveScriptFile.h @@ -36,10 +36,10 @@ inline constexpr const char* kModifierExt = ".mlm"; /// shape of a script is visible before a line is typed. inline constexpr const char* kEffectTemplate = "class NewEffect {\n" - " uint8_t bpm = 60;\n" + " byte bpm = 60;\n" "\n" " defineControls() {\n" - " addUint8(\"bpm\", bpm, 1, 255);\n" + " addControl(\"bpm\", bpm, 1, 255);\n" " }\n" "\n" " tick() {\n" @@ -49,12 +49,12 @@ inline constexpr const char* kEffectTemplate = inline constexpr const char* kLayoutTemplate = "class NewLayout {\n" - " uint8_t cols = 16;\n" - " uint8_t rows = 16;\n" + " byte cols = 16;\n" + " byte rows = 16;\n" "\n" " defineControls() {\n" - " addUint8(\"cols\", cols, 1, 64);\n" - " addUint8(\"rows\", rows, 1, 64);\n" + " addControl(\"cols\", cols, 1, 64);\n" + " addControl(\"rows\", rows, 1, 64);\n" " }\n" "\n" " placeLights() {\n" diff --git a/src/platform/desktop/moonlive_asm_x86_64.cpp b/src/platform/desktop/moonlive_asm_x86_64.cpp index 693819d0..b5bb6eee 100644 --- a/src/platform/desktop/moonlive_asm_x86_64.cpp +++ b/src/platform/desktop/moonlive_asm_x86_64.cpp @@ -253,7 +253,7 @@ static void emitSubRspImm32(HostAssembler* A, int32_t imm) { // the slot index, exactly like arm64's x29 + 16 + n*8: IrOp::Call passes slotAddr(firstSlot) and // the host builtin reads its arguments as an ARRAY walking upward from that address // (moonlive_lower.h), so consecutive slot indices MUST be consecutive ascending memory. Reverse -// this and every non-inlined builtin (random16, addLight, addUint8, line, palette reads) +// this and every non-inlined builtin (random16, addLight, addControl, line, palette reads) // receives its arguments backwards, while everything inlined keeps working. // // The region is sized at kTotalSlots whatever the script uses: a fixed 168 bytes of desktop stack diff --git a/test/python/test_check_specs_drift.py b/test/python/test_check_specs_drift.py index 3cd81bca..f1014331 100644 --- a/test/python/test_check_specs_drift.py +++ b/test/python/test_check_specs_drift.py @@ -18,20 +18,20 @@ # ---- range drift ---- def test_range_drift_flags_a_conflicting_range(): - src = 'controls_.addUint8("floor", floor, 0, 255);' + src = 'controls_.addControl("floor", floor, 0, 255);' md = "- `floor` — noise floor (0–128)." # .md says 0–128, .h says 0–255 issues = _check_range_drift(src, md) assert issues and "floor" in issues[0] and "0–255" in issues[0] def test_range_drift_silent_when_ranges_match(): - src = 'controls_.addUint8("freq_x", freq_x, 1, 8);' + src = 'controls_.addControl("freq_x", freq_x, 1, 8);' md = "- `freq_x` — wave frequency (1–8)." assert _check_range_drift(src, md) == [] def test_range_drift_tolerates_hyphen_and_to_spellings(): - src = 'controls_.addUint8("count", count, 1, 255);' + src = 'controls_.addControl("count", count, 1, 255);' for prose in ("(1-255)", "1 to 255", "(1–255)"): md = f"- `count` — rings {prose}." assert _check_range_drift(src, md) == [], prose @@ -39,14 +39,14 @@ def test_range_drift_tolerates_hyphen_and_to_spellings(): def test_range_drift_silent_when_prose_states_no_range(): # Many controls legitimately don't restate their range (pins, obvious 0–255). - src = 'controls_.addUint8("gain", gain, 1, 255);' + src = 'controls_.addControl("gain", gain, 1, 255);' md = "- `gain` — microphone gain." assert _check_range_drift(src, md) == [] def test_range_drift_ignores_non_range_controls(): - # addBool / addSelect / addPin carry no numeric range → nothing to check. - src = 'controls_.addBool("enabled", enabled);\ncontrols_.addPin("sdPin", sdPin);' + # A bool addControl / addSelect / addPin carry no numeric range → nothing to check. + src = 'controls_.addControl("enabled", enabled);\ncontrols_.addPin("sdPin", sdPin);' assert _check_range_drift(src, "- `enabled` — on/off. - `sdPin` — data pin.") == [] diff --git a/test/unit/core/moonlive_device_codegen.inc b/test/unit/core/moonlive_device_codegen.inc index d2dbce0c..84c2bb0a 100644 --- a/test/unit/core/moonlive_device_codegen.inc +++ b/test/unit/core/moonlive_device_codegen.inc @@ -72,7 +72,7 @@ std::vector emitBytes(const char* src, const mm::moonlive::SysVarTable& // Sized the way production sizes it, from the script's own token count — a test that always // allocated the sanity bound would pass while a right-sized caller overflowed. std::vector out(mm::moonlive::codeCapFor(mm::moonlive::countTokens(src))); - // A string pool, as the engine supplies one: a script with a literal (every `addUint8` name) + // A string pool, as the engine supplies one: a script with a literal (every `addControl` name) // interns into it, and the emitted code carries pointers there. Static so those pointers stay // valid for as long as a test might look at the bytes. static char strings[mm::moonlive::CompileResult::kStringPool]; diff --git a/test/unit/core/unit_Control_apply_absent_key.cpp b/test/unit/core/unit_Control_apply_absent_key.cpp index 7310d1b3..f6e63ef1 100644 --- a/test/unit/core/unit_Control_apply_absent_key.cpp +++ b/test/unit/core/unit_Control_apply_absent_key.cpp @@ -49,9 +49,9 @@ TEST_CASE("applyControlValue leaves a control untouched when its key is absent") bool flag = true; static const char* const opts[] = {"None", "LAN8720", "IP101", "W5500"}; controls.addSelect("ethType", ethType, opts, 4); - controls.addInt16("ethMdcGpio", mdcGpio, -1, 48); - controls.addUint8("small", small, 0, 100); - controls.addBool("flag", flag); + controls.addControl("ethMdcGpio", mdcGpio, -1, 48); + controls.addControl("small", small, 0, 100); + controls.addControl("flag", flag); // A persisted file that contains an UNRELATED key only — none of our controls. const char* partialJson = "{\"ssid\":\"home\"}"; @@ -76,7 +76,7 @@ TEST_CASE("applyControlValue still applies a present key") { int16_t mdcGpio = 31; static const char* const opts[] = {"None", "LAN8720", "IP101", "W5500"}; controls.addSelect("ethType", ethType, opts, 4); - controls.addInt16("ethMdcGpio", mdcGpio, -1, 48); + controls.addControl("ethMdcGpio", mdcGpio, -1, 48); // Saved file carries new values for both. const char* json = "{\"ethType\":3,\"ethMdcGpio\":23}"; diff --git a/test/unit/core/unit_Control_int32.cpp b/test/unit/core/unit_Control_int32.cpp index 419812bc..ac322024 100644 --- a/test/unit/core/unit_Control_int32.cpp +++ b/test/unit/core/unit_Control_int32.cpp @@ -14,7 +14,7 @@ TEST_CASE("an int32 control carries a value no 16-bit control could hold") { mm::ControlList controls; int32_t big = 0; - controls.addInt32("offset", big, -1000000, 1000000); + controls.addControl("offset", big, -1000000, 1000000); const mm::ControlDescriptor& c = controls[0]; CHECK(c.type == mm::ControlType::Int32); @@ -34,7 +34,7 @@ TEST_CASE("an int32 control carries a value no 16-bit control could hold") { TEST_CASE("an int32 control round-trips a negative value") { mm::ControlList controls; int32_t v = 0; - controls.addInt32("delta", v, -2000000, 2000000); + controls.addControl("delta", v, -2000000, 2000000); auto r = mm::applyControlValue(controls[0], "{\"delta\":-1500000}", "delta", mm::ApplyPolicy::Clamp); @@ -50,7 +50,7 @@ TEST_CASE("an int32 control round-trips a negative value") { TEST_CASE("an int32 control clamps a write past its range and refuses it under Strict") { mm::ControlList controls; int32_t v = 0; - controls.addInt32("bounded", v, 0, 1000); + controls.addControl("bounded", v, 0, 1000); CHECK(mm::applyControlValue(controls[0], "{\"bounded\":5000}", "bounded", mm::ApplyPolicy::Clamp) == mm::ApplyResult::Ok); @@ -65,7 +65,7 @@ TEST_CASE("an int32 control clamps a write past its range and refuses it under S TEST_CASE("an int32 control publishes its range to the UI") { mm::ControlList controls; int32_t v = 0; - controls.addInt32("span", v, -70000, 70000); + controls.addControl("span", v, -70000, 70000); char buf[128]; mm::JsonSink sink(buf, sizeof(buf)); @@ -73,3 +73,75 @@ TEST_CASE("an int32 control publishes its range to the UI") { CHECK(std::strstr(buf, "\"min\":-70000") != nullptr); CHECK(std::strstr(buf, "\"max\":70000") != nullptr); } + +// --- addControl: one name, the widget from the type --------------------------------------------- + +// A compiled module and a MoonLive script now spell a control the same way. The widget follows the +// VARIABLE'S TYPE, which the compiler already knows — so a call cannot disagree with the +// declaration, and there is one vocabulary to learn rather than five names carrying a width. +TEST_CASE("addControl binds the widget its member's type calls for") { + mm::ControlList controls; + uint8_t u8 = 1; + uint16_t u16 = 2; + int16_t i16 = 3; + int32_t i32 = 4; + bool b = true; + + controls.addControl("u8", u8); + controls.addControl("u16", u16); + controls.addControl("i16", i16); + controls.addControl("i32", i32); + controls.addControl("b", b); + + REQUIRE(controls.count() == 5); + CHECK(controls[0].type == mm::ControlType::Uint8); + CHECK(controls[1].type == mm::ControlType::Uint16); + CHECK(controls[2].type == mm::ControlType::Int16); + CHECK(controls[3].type == mm::ControlType::Int32); + CHECK(controls[4].type == mm::ControlType::Bool); + // The wire names the UI keys off, so a widget change would be visible here too. + CHECK(std::strcmp(mm::controlTypeName(controls[0].type), "uint8") == 0); + CHECK(std::strcmp(mm::controlTypeName(controls[3].type), "int32") == 0); + CHECK(std::strcmp(mm::controlTypeName(controls[4].type), "bool") == 0); +} + +// A call that omits min/max means "no UI constraint", and what that means DIFFERS per type: each +// overload defaults to its own type's full range. Unifying them would silently move the bounds of +// every control that relies on the default, which is why the overloads keep separate signatures. +TEST_CASE("addControl without a range gets its own type's full range") { + mm::ControlList controls; + uint8_t u8 = 0; + uint16_t u16 = 0; + int16_t i16 = 0; + int32_t i32 = 0; + bool b = false; + + controls.addControl("u8", u8); + controls.addControl("u16", u16); + controls.addControl("i16", i16); + controls.addControl("i32", i32); + controls.addControl("b", b); + + CHECK(controls[0].min == 0); CHECK(controls[0].max == 255); + CHECK(controls[1].min == 0); CHECK(controls[1].max == UINT16_MAX); + CHECK(controls[2].min == INT16_MIN); CHECK(controls[2].max == INT16_MAX); + CHECK(controls[3].min == INT32_MIN); CHECK(controls[3].max == INT32_MAX); + CHECK(controls[4].min == 0); CHECK(controls[4].max == 1); +} + +// An explicit range is carried through unchanged, per type. +TEST_CASE("addControl carries an explicit range to the descriptor") { + mm::ControlList controls; + uint8_t speed = 50; + int32_t span = 0; + controls.addControl("speed", speed, 1, 99); + controls.addControl("span", span, -70000, 70000); + CHECK(controls[0].min == 1); CHECK(controls[0].max == 99); + CHECK(controls[1].min == -70000); CHECK(controls[1].max == 70000); +} + +// NOT TESTABLE AT RUN TIME, stated here so the intent survives: `addControl(name, int8_t&)` is +// `= delete`d in Control.h. An int8_t is either a GPIO (addPin — PinsModule scans for +// ControlType::Pin to collect claimed pins) or telemetry (addReadOnlyInt, which needs a unit), and +// deducing one from the type would make any future small signed control register as a claimed +// GPIO. A compile failure cannot be a doctest case; the deleted overload IS the test. diff --git a/test/unit/core/unit_FilesystemModule_persistence.cpp b/test/unit/core/unit_FilesystemModule_persistence.cpp index 890ff6db..285b9b41 100644 --- a/test/unit/core/unit_FilesystemModule_persistence.cpp +++ b/test/unit/core/unit_FilesystemModule_persistence.cpp @@ -594,7 +594,7 @@ struct ModeDependentMock : public mm::MoonModule { controls_.addSelect("mode", mode, kModes, 2); // `param` binds to a DIFFERENT variable depending on mode — exactly like clockPin binding to // whichever peripheral backend is live. A rebuild after `mode` changes re-binds it. - controls_.addUint8("param", mode == 0 ? paramA : paramB, 0, 255); + controls_.addControl("param", mode == 0 ? paramA : paramB, 0, 255); } }; } // namespace @@ -895,10 +895,10 @@ class LateSchemaModule : public mm::MoonModule { void defineControls() override { mm::MoonModule::defineControls(); - controls_.addUint8("always", always, 0, 255); + controls_.addControl("always", always, 0, 255); // Only published once prepare() has run, exactly as publishDeclaredControls is empty until // the engine holds a compiled program. - if (prepared) controls_.addUint8("late", late, 0, 255); + if (prepared) controls_.addControl("late", late, 0, 255); } void prepare() override { prepared = true; diff --git a/test/unit/core/unit_HttpServerModule_apply.cpp b/test/unit/core/unit_HttpServerModule_apply.cpp index a5cbbb10..15e6b26d 100644 --- a/test/unit/core/unit_HttpServerModule_apply.cpp +++ b/test/unit/core/unit_HttpServerModule_apply.cpp @@ -27,8 +27,8 @@ struct Knob : public mm::MoonModule { uint8_t value = 10; bool showExtra = false; // toggling this ADDS/REMOVES a control → a real schema change void defineControls() override { - controls_.addUint8("value", value, 0, 100); - if (showExtra) controls_.addUint8("extra", value, 0, 100); + controls_.addControl("value", value, 0, 100); + if (showExtra) controls_.addControl("extra", value, 0, 100); } }; struct Box : public mm::MoonModule { @@ -61,8 +61,8 @@ struct FakeDrivers : public mm::MoonModule { bool on = true; uint8_t brightness = 20; void defineControls() override { - controls_.addBool("on", on); - controls_.addUint8("brightness", brightness, 0, 255); + controls_.addControl("on", on); + controls_.addControl("brightness", brightness, 0, 255); } }; diff --git a/test/unit/core/unit_IrService.cpp b/test/unit/core/unit_IrService.cpp index b1ffe0bd..169fe608 100644 --- a/test/unit/core/unit_IrService.cpp +++ b/test/unit/core/unit_IrService.cpp @@ -27,8 +27,8 @@ struct FakeDrivers : public MoonModule { uint8_t brightness = 100; uint8_t palette = 1; void defineControls() override { - controls_.addBool("on", on); - controls_.addUint8("brightness", brightness, 0, 255); + controls_.addControl("on", on); + controls_.addControl("brightness", brightness, 0, 255); // A Select's max is (optionCount - 1); addSelect binds min 0 / max count-1. static const char* kPalettes[] = {"A", "B", "C", "D"}; controls_.addSelect("palette", palette, kPalettes, 4); diff --git a/test/unit/core/unit_MoonModule.cpp b/test/unit/core/unit_MoonModule.cpp index f70197de..017b903b 100644 --- a/test/unit/core/unit_MoonModule.cpp +++ b/test/unit/core/unit_MoonModule.cpp @@ -20,9 +20,9 @@ class TestModule : public mm::MoonModule { void release() override { releaseCalled = true; } void defineControls() override { - controls_.addUint8("brightness", brightness, 0, 255); - controls_.addUint8("speed", speed, 1, 255); - controls_.addBool("enabled", enabled); + controls_.addControl("brightness", brightness, 0, 255); + controls_.addControl("speed", speed, 1, 255); + controls_.addControl("enabled", enabled); } }; @@ -134,7 +134,7 @@ class SelectModule : public mm::MoonModule { char opt1[16] = "beta"; const char* options[2] = {opt0, opt1}; void defineControls() override { - controls_.addUint8("value", value, 0, 255); + controls_.addControl("value", value, 0, 255); controls_.addSelect("preset", sel, options, 2); } }; @@ -235,7 +235,7 @@ TEST_CASE("Module enabled property") { CHECK(mod.enabled() == true); } -// addBool binds a bool field — toggling the field updates control.ptr's view. +// a bool addControl binds a bool field — toggling the field updates control.ptr's view. TEST_CASE("Bool control binding") { TestModule mod; mod.defineControls(); diff --git a/test/unit/core/unit_MqttModule.cpp b/test/unit/core/unit_MqttModule.cpp index 3dd8dd24..6047a1ee 100644 --- a/test/unit/core/unit_MqttModule.cpp +++ b/test/unit/core/unit_MqttModule.cpp @@ -40,12 +40,12 @@ struct FakeDrivers : public MoonModule { uint8_t brightness = 100; uint8_t palette = 0; void defineControls() override { - controls_.addBool("on", on); - controls_.addUint8("brightness", brightness, 0, 255); + controls_.addControl("on", on); + controls_.addControl("brightness", brightness, 0, 255); // A Uint8 palette with the real built-in range (0..255 is a superset of the ~60 built-ins), // so a nearest-palette index the MQTT map returns isn't clamped away by an artificially small // Select — the real Drivers.palette binds 0..kCount-1. - controls_.addUint8("palette", palette, 0, 255); + controls_.addControl("palette", palette, 0, 255); } }; diff --git a/test/unit/core/unit_NetworkModule_ethernet.cpp b/test/unit/core/unit_NetworkModule_ethernet.cpp index 12dd493d..a7808ca9 100644 --- a/test/unit/core/unit_NetworkModule_ethernet.cpp +++ b/test/unit/core/unit_NetworkModule_ethernet.cpp @@ -88,17 +88,17 @@ TEST_CASE("Desktop Ethernet seam is a safe no-op") { // Regression: the `ethPhyAddr` control MUST be a SIGNED int16 whose range starts at -1 and // which renders as a number field (not a slider). -1 is ESP_ETH_PHY_ADDR_AUTO (scan the MDIO -// bus — the RGMII default). It was once an addUint8(0,31): the uint8 mangled the platform's -1 +// bus — the RGMII default). It was once a uint8 control: the uint8 mangled the platform's -1 // default to 255 and the 0..31 control clamped it to 31, a fixed address no PHY answered, so // the S31's RGMII never linked. This pins the control-metadata contract that fixed it — signed // storage so -1 round-trips, min == -1 so the sentinel is in-range, and numberField because an -// MDIO address is an identity, not a magnitude. Tests the addInt16 + setNumberField seam +// MDIO address is an identity, not a magnitude. Tests the int16 control + setNumberField seam // directly (the NetworkModule control is `if constexpr (hasEthernet)`-gated, absent on desktop), // so a future edit that reverts to a slider or an unsigned type fails here, off-hardware. TEST_CASE("ethPhyAddr-style control: signed int16, -1 sentinel in range, number field") { mm::ControlList controls; int16_t phyAddr = -1; // ESP_ETH_PHY_ADDR_AUTO — must survive as -1, not become 255/31 - controls.addInt16("ethPhyAddr", phyAddr, -1, 31); + controls.addControl("ethPhyAddr", phyAddr, -1, 31); controls.setNumberField(controls.count() - 1); const auto& c = controls[controls.count() - 1]; diff --git a/test/unit/core/unit_moonlive_compiler.cpp b/test/unit/core/unit_moonlive_compiler.cpp index c611aabd..1afb9816 100644 --- a/test/unit/core/unit_moonlive_compiler.cpp +++ b/test/unit/core/unit_moonlive_compiler.cpp @@ -311,8 +311,8 @@ TEST_CASE("MoonLive recompiling swaps the program live (fill <-> setRGB)") { CHECK(buf[1*3+0] == 255); CHECK(buf[0] == 0); } -// CONTROLS: a declaration is a member, and `addUint8("name", name, lo, hi)` in defineControls -// A control is declared by CALLING addUint8 inside defineControls, the same call a compiled module +// CONTROLS: a declaration is a member, and `addControl("name", name, lo, hi)` in defineControls +// A control is declared by CALLING addControl inside defineControls, the same call a compiled module // makes. The declaration alone is a member: state the script owns, which the UI never sees unless // the script asks for it. That split is the whole point, so both halves are checked here. // @@ -345,7 +345,7 @@ TEST_CASE("a control is declared by calling addControl, and a plain member is no } // A control's range is an ORDINARY EXPRESSION, like every other argument in the language. Making -// addUint8 the one call whose arguments must be literals would be a special case wearing a +// addControl the one call whose arguments must be literals would be a special case wearing a // disguise, so this pins that it is not one. TEST_CASE("a control's range can be computed, not just written as a literal") { moonlive::MoonLive eng; diff --git a/test/unit/core/unit_moonlive_fill.cpp b/test/unit/core/unit_moonlive_fill.cpp index 4ff259f4..0fd83c1d 100644 --- a/test/unit/core/unit_moonlive_fill.cpp +++ b/test/unit/core/unit_moonlive_fill.cpp @@ -683,7 +683,7 @@ TEST_CASE("an if inside a for runs the body every iteration") { } // The condition is an ordinary expression on both sides, not a name-against-literal special case: -// the same orthogonality that lets addUint8 take a computed range. +// the same orthogonality that lets addControl take a computed range. TEST_CASE("an if condition may be an expression on both sides") { moonlive::MoonLive eng; REQUIRE(eng.compile("class T {\n" @@ -921,7 +921,7 @@ TEST_CASE("an int member is published as a control spanning its full range") { // // The initial READ is fine, here and everywhere: what looked like a broken read was run() // being called without an entry name, which starts at the block start — `defineControls`, - // not `tick`. addUint16 itself is hardware-verified on both ISAs (S3 and S31 drive ember's + // not `tick`. a wide control is hardware-verified on both ISAs (S3 and S31 drive ember's // `cycle` to 2000 and back). } @@ -1056,7 +1056,7 @@ TEST_CASE("an out-of-range array read is clamped and leaves system variables int } // The index is an arbitrary EXPRESSION, not a bare loop counter: the same orthogonality that lets -// addUint8 take a computed range and an if condition take one on both sides. +// addControl take a computed range and an if condition take one on both sides. TEST_CASE("an array index may be an expression") { moonlive::MoonLive eng; REQUIRE(eng.compile("class T {\n" diff --git a/test/unit/light/unit_MoonLiveLayout.cpp b/test/unit/light/unit_MoonLiveLayout.cpp index f5fabaf7..c2df9a30 100644 --- a/test/unit/light/unit_MoonLiveLayout.cpp +++ b/test/unit/light/unit_MoonLiveLayout.cpp @@ -31,9 +31,9 @@ using namespace mm; // Every case here compiles a script and runs the emitted native code, so all of them need a JIT -// backend for the host ISA. `MM_MOONLIVE_HAS_HOST_JIT` is 0 on x86_64 — which is what CI runs — and -// there a layout reports zero lights for a reason that has nothing to do with the layout. Gated as a -// block, the same way unit_moonlive_fill / unit_moonlive_ir do it. +// backend for the host ISA. arm64 and x86-64 both have one; a --no-jit build does not, and there a +// layout reports zero lights for a reason that has nothing to do with the layout. Gated as a block, +// the same way unit_moonlive_fill / unit_moonlive_ir do it. #if MM_MOONLIVE_HAS_HOST_JIT namespace { diff --git a/test/unit/light/unit_MoonLiveModifier.cpp b/test/unit/light/unit_MoonLiveModifier.cpp index 324bc84c..b8e6194b 100644 --- a/test/unit/light/unit_MoonLiveModifier.cpp +++ b/test/unit/light/unit_MoonLiveModifier.cpp @@ -25,9 +25,9 @@ using namespace mm; // Every case here compiles a script and runs the emitted native code, so all of them need a JIT -// backend for the host ISA. `MM_MOONLIVE_HAS_HOST_JIT` is 0 on x86_64 — which is what CI runs — and -// there a modifier maps nothing for a reason that has nothing to do with the modifier. Gated as a -// block, the same way unit_moonlive_fill / unit_moonlive_ir do it. +// backend for the host ISA. arm64 and x86-64 both have one; a --no-jit build does not, and there a +// modifier maps nothing for a reason that has nothing to do with the modifier. Gated as a block, +// the same way unit_moonlive_fill / unit_moonlive_ir do it. #if MM_MOONLIVE_HAS_HOST_JIT namespace { diff --git a/test/unit/light/unit_MoonLiveScripts.cpp b/test/unit/light/unit_MoonLiveScripts.cpp index 7705ff03..aada36d7 100644 --- a/test/unit/light/unit_MoonLiveScripts.cpp +++ b/test/unit/light/unit_MoonLiveScripts.cpp @@ -81,10 +81,11 @@ TEST_CASE("every script in moonlive/ compiles") { moonlive::MoonLive engine; const bool ok = engine.compile(src.c_str(), moonlive::lightBuiltins(), sys); if (!ok) std::printf("FAIL %-28s %s\n", label.c_str(), engine.error()); - // compile() both PARSES and emits native code, and only the second half needs a backend - // for this host's ISA (MM_MOONLIVE_HAS_HOST_JIT — 0 on x86_64, which is what CI runs). - // Requiring success there would fail every script for a reason that has nothing to do - // with the script, so without a backend the only failure allowed is the codegen one. + // compile() both PARSES and emits native code, and only the second half needs a + // backend for this host's ISA. arm64 and x86-64 both have one; a --no-jit build and any + // other host do not, and there requiring success would fail every script for a reason + // that has nothing to do with the script. Without a backend the only failure allowed is + // the codegen one. #if MM_MOONLIVE_HAS_HOST_JIT CHECK(ok); #else @@ -139,8 +140,8 @@ TEST_CASE("every script reads the same system-variable vocabulary") { auto r = moonlive::compileSource(c.src, moonlive::lightBuiltins(), moonlive::lightSysVars(), out, sizeof(out)); // Where a backend exists, a valid script must actually EMIT — accepting kCodegenFailed - // everywhere would let a codegen regression pass as a pass. Only a host with no assembler - // for its ISA (x86_64, which is what CI runs) is allowed that answer. + // everywhere would let a codegen regression pass as a pass. Only a build with no assembler + // for its ISA (--no-jit, or an unsupported host) is allowed that answer. #if MM_MOONLIVE_HAS_HOST_JIT if (c.ok) CHECK(r.ok); #else @@ -331,9 +332,9 @@ TEST_CASE("sequential loops reuse the same register, so a script is not billed p moonlive::lightBuiltins(), moonlive::modifierSysVars(), code, sizeof(code)); if (!r.ok) INFO(r.error); // What this pins is REGISTER REUSE, which the front-end does on every host — but proving it - // needs code to come out, and only a host with an assembler for its ISA emits any - // (MM_MOONLIVE_HAS_HOST_JIT is 0 on x86_64, which is what CI runs). Requiring success there - // fails for the one reason that has nothing to do with register reuse. + // needs code to come out, and only a host with an assembler for its ISA emits any. On a build + // without one (--no-jit, or an unsupported host) requiring success fails for the one reason + // that has nothing to do with register reuse. #if MM_MOONLIVE_HAS_HOST_JIT CHECK(r.ok); #else diff --git a/test/unit/light/unit_ParallelLedDriver_ring.cpp b/test/unit/light/unit_ParallelLedDriver_ring.cpp index 024ebc04..ef0d73bb 100644 --- a/test/unit/light/unit_ParallelLedDriver_ring.cpp +++ b/test/unit/light/unit_ParallelLedDriver_ring.cpp @@ -65,7 +65,7 @@ class MockRingPeripheral : public mm::LedPeripheral { // whole-frame-only backends). Mirror the real driver: the source-snapshot knob under the path, gated // on wantsRing() — this mock's controllable wantRing_ drives the visibility the hide test checks. void addRingControls(mm::ControlList& controls) override { - controls.addBool("ringSnapshot", owner_->ringSnapshotRef()); + controls.addControl("ringSnapshot", owner_->ringSnapshotRef()); controls.setHidden(controls.count() - 1, !wantsRing()); } bool busControlTriggersBuild(const char*) const override { return false; }