diff --git a/.claude/workflows/write-behaviour-tests.js b/.claude/workflows/write-behaviour-tests.js index c4bc8bfc..f2ddbaf9 100644 --- a/.claude/workflows/write-behaviour-tests.js +++ b/.claude/workflows/write-behaviour-tests.js @@ -52,9 +52,9 @@ phase('Study + write') const results = await parallel(MODULES.map(([cls, header, kind]) => () => agent( -`Write a behaviour-specific doctest unit test for the projectMM module **${cls}** (${kind}). +`Write a behaviour-specific doctest unit test for the MoonLight module **${cls}** (${kind}). -Repo: the current workspace root (the projectMM checkout you're running in) — all paths below are relative to it. +Repo: the current workspace root (the MoonLight checkout you're running in) — all paths below are relative to it. ## Study first (do NOT guess behaviour) 1. Read the module header: ${header} — understand what it ACTUALLY does: its controls, its render/modify logic, what it writes to the buffer or how it transforms coordinates. Behaviour is the spec. diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml index 85f5d7eb..a516a9b9 100644 --- a/.github/codeql-config.yml +++ b/.github/codeql-config.yml @@ -1,4 +1,4 @@ -name: "projectMM CodeQL config" +name: "MoonLight CodeQL config" # Code we did not write and will not change is not a finding we can act on. doctest.h is the # vendored single-header test framework (test/doctest.h) and the only third-party source in the diff --git a/.github/workflows/prose.yml b/.github/workflows/prose.yml index e71fb760..d27b7849 100644 --- a/.github/workflows/prose.yml +++ b/.github/workflows/prose.yml @@ -1,6 +1,6 @@ name: Prose -# Vale over the lines a PR ADDS or changes, with the rules in .vale/styles/projectMM/. Diff-scoped +# Vale over the lines a PR ADDS or changes, with the rules in .vale/styles/MoonLight/. Diff-scoped # on purpose: the tree still holds pre-existing violations, and a contributor is answerable for # what they wrote, not for inherited prose. An `error` (em-dash, British spelling, "e.g.") fails # the check; warnings and suggestions land as inline annotations only. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 399f0ab3..0cfee98c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -948,7 +948,7 @@ jobs: run: | set -euo pipefail # Render the docs/ tree (Material for MkDocs) as the Pages ROOT - # (moonmodules.github.io/projectMM/) — the project's front door. + # (moonmodules.github.io/MoonLight/) — the project's front door. # Config: mkdocs.yml; deps declared inline in the build script (uv # provisions them). Build to a temp dir, then copy INTO pages/ so the # installer staged above under pages/install/ survives (a plain diff --git a/.gitignore b/.gitignore index 258084eb..f9eee7e4 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ __pycache__/ # listed media types one by one, to protect run files that used to live here; they no # longer do, so the rule can say what it means. /media/ +.playwright-profile/ diff --git a/.vale.ini b/.vale.ini index d09d6b6e..95eb1c1f 100644 --- a/.vale.ini +++ b/.vale.ini @@ -2,13 +2,13 @@ # this is their mechanical half. Run: `vale docs/ CLAUDE.md README.md` or via check_prose.py. StylesPath = .vale/styles MinAlertLevel = suggestion -# Proper nouns the rules must not correct: see .vale/styles/config/vocabularies/projectMM. -Vocab = projectMM +# Proper nouns the rules must not correct: see .vale/styles/config/vocabularies/MoonLight. +Vocab = MoonLight # Defaults first: in Vale a LATER [glob] overrides an earlier one, so the general rule leads and # the specific pages below it win. [*.md] -BasedOnStyles = projectMM +BasedOnStyles = MoonLight # CODE CARRIES PROSE TOO: a `///` becomes a page, a `//` is read beside the line it explains, and # documentation-standards.md governs both. Held to the same rules as a page, so that when @@ -18,7 +18,7 @@ BasedOnStyles = projectMM # header and reports ZERO FILES, which reads exactly like a clean run. CComments hands it the # tree-sitter syntax tree and lints the comment nodes alone, so code is never read as prose. [*.{c,h,hpp,cpp,inc}] -BasedOnStyles = projectMM +BasedOnStyles = MoonLight View = CComments # Upstream code, vendored rather than written here: not our prose to rule on. @@ -50,26 +50,26 @@ BasedOnStyles = # Add a page here when `vale ` reports nothing; the sweep plan in docs/work/present/ is the # list. When every page is here, the two prose scripts go and this section becomes the default. [docs/tutorials/installing-on-linux.md] -BasedOnStyles = projectMM -projectMM.SentenceLength = error -projectMM.Weasel = error -projectMM.NegatedHeading = error -projectMM.SelfReference = error +BasedOnStyles = MoonLight +MoonLight.SentenceLength = error +MoonLight.Weasel = error +MoonLight.NegatedHeading = error +MoonLight.SelfReference = error [docs/contributing/coding-standards.md] -BasedOnStyles = projectMM -projectMM.SentenceLength = error -projectMM.Weasel = error -projectMM.NegatedHeading = error -projectMM.SelfReference = error +BasedOnStyles = MoonLight +MoonLight.SentenceLength = error +MoonLight.Weasel = error +MoonLight.NegatedHeading = error +MoonLight.SelfReference = error # The rules page must QUOTE the things it bans (an em-dash, "analyse", "this page", "really"), so # the four rules whose examples it carries stay off here. The rest are strict. [docs/contributing/documentation-standards.md] -BasedOnStyles = projectMM -projectMM.EmDash = NO -projectMM.Spelling = NO -projectMM.SelfReference = NO -projectMM.Weasel = NO -projectMM.SentenceLength = error -projectMM.NegatedHeading = error +BasedOnStyles = MoonLight +MoonLight.EmDash = NO +MoonLight.Spelling = NO +MoonLight.SelfReference = NO +MoonLight.Weasel = NO +MoonLight.SentenceLength = error +MoonLight.NegatedHeading = error diff --git a/.vale/styles/projectMM/EmDash.yml b/.vale/styles/MoonLight/EmDash.yml similarity index 100% rename from .vale/styles/projectMM/EmDash.yml rename to .vale/styles/MoonLight/EmDash.yml diff --git a/.vale/styles/projectMM/NegatedHeading.yml b/.vale/styles/MoonLight/NegatedHeading.yml similarity index 100% rename from .vale/styles/projectMM/NegatedHeading.yml rename to .vale/styles/MoonLight/NegatedHeading.yml diff --git a/.vale/styles/projectMM/SelfReference.yml b/.vale/styles/MoonLight/SelfReference.yml similarity index 100% rename from .vale/styles/projectMM/SelfReference.yml rename to .vale/styles/MoonLight/SelfReference.yml diff --git a/.vale/styles/projectMM/SentenceLength.yml b/.vale/styles/MoonLight/SentenceLength.yml similarity index 100% rename from .vale/styles/projectMM/SentenceLength.yml rename to .vale/styles/MoonLight/SentenceLength.yml diff --git a/.vale/styles/projectMM/Spelling.yml b/.vale/styles/MoonLight/Spelling.yml similarity index 100% rename from .vale/styles/projectMM/Spelling.yml rename to .vale/styles/MoonLight/Spelling.yml diff --git a/.vale/styles/projectMM/SuchAs.yml b/.vale/styles/MoonLight/SuchAs.yml similarity index 100% rename from .vale/styles/projectMM/SuchAs.yml rename to .vale/styles/MoonLight/SuchAs.yml diff --git a/.vale/styles/projectMM/Weasel.yml b/.vale/styles/MoonLight/Weasel.yml similarity index 100% rename from .vale/styles/projectMM/Weasel.yml rename to .vale/styles/MoonLight/Weasel.yml diff --git a/.vale/styles/config/vocabularies/projectMM/accept.txt b/.vale/styles/config/vocabularies/MoonLight/accept.txt similarity index 100% rename from .vale/styles/config/vocabularies/projectMM/accept.txt rename to .vale/styles/config/vocabularies/MoonLight/accept.txt diff --git a/CLAUDE.md b/CLAUDE.md index d5b0a7ee..efcf6b91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ A high-performance system driving large LED installations and DMX fixtures. One 3. **Architecture first.** The domain-neutral core owns the hard constructs, written once; the light domain stays simple on top. Platform-specific code lives only in the platform layer. When core enforces a rule on one path, extend core to the next. No hacks: fix it the standard way when spotted, or backlog the real fix by name. Default to subtraction: the first question on any change is what it can remove. - **Build the best solution, not the compatible one.** projectMM has no installed base to protect, so "it would break existing configs" is not an argument for a worse design. When a better shape replaces an older one, the old one goes: two mechanisms doing one job is the debt this project exists to avoid. The break is documented rather than carried, which costs a [MIGRATING](docs/reference/MIGRATING.md) entry and buys one way to do each thing. Weigh what a user loses, not what changes. + **Build the best solution, not the compatible one.** MoonLight has no installed base to protect, so "it would break existing configs" is not an argument for a worse design. When a better shape replaces an older one, the old one goes: two mechanisms doing one job is the debt this project exists to avoid. The break is documented rather than carried, which costs a [MIGRATING](docs/reference/MIGRATING.md) entry and buys one way to do each thing. Weigh what a user loses, not what changes. 4. **Guardrails everywhere.** Every behavior is pinned by tests whose descriptions read as functional documentation. Every commit is measured, so growth and regression are visible as they happen. Judgment is reviewed; everything else is checked per event below. The final guardrail is physical: verified means it ran on real hardware, with the product owner's eyes as the measurement. diff --git a/CMakeLists.txt b/CMakeLists.txt index b25486c8..99d15095 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -307,7 +307,9 @@ if(WIN32) endif() # Application -add_executable(projectMM src/main.cpp src/platform/desktop/main_desktop.cpp ${MM_WIN_RESOURCES}) +# module_types.cpp is the shared type registry, compiled into the app and the scenario runner +# alike so one list serves both: src/module_types.cpp carries the reason. +add_executable(projectMM src/main.cpp src/module_types.cpp src/platform/desktop/main_desktop.cpp ${MM_WIN_RESOURCES}) target_link_libraries(projectMM PRIVATE mm_core mm_platform) add_dependencies(projectMM ui_embed build_info_gen moonlive_catalog) diff --git a/README.md b/README.md index cbc83733..5cc6be87 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# projectMM +# MoonLight Drive large LED installations and DMX fixtures. One source tree drives ESP32, Teensy, Raspberry Pi, macOS, Windows and Linux. @@ -8,7 +8,7 @@ Drive large LED installations and DMX fixtures. One source tree drives ESP32, Te No hardware handy? The [desktop build](https://github.com/MoonModules/projectMM/releases/latest) runs the same UI and effect pipeline on macOS, Windows and Linux, driving fixtures over Art-Net, DDP or E1.31. -If you like projectMM, give it a star, fork it, or open an issue. It helps the project get noticed. +If you like MoonLight, give it a star, fork it, or open an issue. It helps the project get noticed. ## What you get @@ -38,7 +38,7 @@ Written against ESP-IDF directly with no third-party libraries, and with our own ## The parts -projectMM is one system in nine named parts. Each has a page that owns its detail. +MoonLight is one system in nine named parts. Each has a page that owns its detail. ```mermaid flowchart TB @@ -164,7 +164,7 @@ Per-grid and per-device tables, free-heap figures, and why WiFi costs what it do **ESP32**: open the [web installer](https://moonmodules.org/projectMM/install/) in Chrome or Edge ([MoonInstaller](#mooninstaller)). It walks you through device, firmware, flashing and network setup. -**Desktop**: download your build from the [releases page](https://github.com/MoonModules/projectMM/releases), then open `http://localhost:8080/`. Step by step with screenshots: [Installing projectMM on a desktop](docs/how-to/installing-to-desktop.md). +**Desktop**: download your build from the [releases page](https://github.com/MoonModules/projectMM/releases), then open `http://localhost:8080/`. Step by step with screenshots: [Installing MoonLight on a desktop](docs/how-to/installing-to-desktop.md). - **macOS arm64**: `.dmg`, drag to Applications. Ad-hoc signed, so right-click and Open the first time. - **Windows x64**: `-setup.exe` installs for your user without an admin prompt. Unsigned, so SmartScreen asks once. @@ -194,7 +194,7 @@ Open `http://localhost:8420` to build, run, test, flash and discover devices. Fu ## How we work -projectMM is built by AI agents under tight human direction. Everything in this repository is authored by agents; the **product owner** writes none of it directly. What the product owner authors is the [process](CLAUDE.md), the [architecture](docs/explanation/architecture/index.md), and the [module specifications](docs/moonmodules/), then decides what to build, reviews every line, runs the hardware tests, and controls every commit and release. Agents write; the product owner thinks. +MoonLight is built by AI agents under tight human direction. Everything in this repository is authored by agents; the **product owner** writes none of it directly. What the product owner authors is the [process](CLAUDE.md), the [architecture](docs/explanation/architecture/index.md), and the [module specifications](docs/moonmodules/), then decides what to build, reviews every line, runs the hardware tests, and controls every commit and release. Agents write; the product owner thinks. The roles, the principles and the full process: [CLAUDE.md](CLAUDE.md). @@ -209,20 +209,20 @@ This is the current iteration of years of LED and light-system development, and | **StarLight** | Standalone LED firmware | [ewowi/StarLight](https://github.com/ewowi/StarLight) | | **MoonLight** | Ground-up build: 60+ effects, memory-optimized mapping, 11 driver types | [ewowi/MoonLight](https://github.com/ewowi/MoonLight) | -We built and maintained these, so projectMM rests on our own hands-on experience. Their lessons are distilled in [`docs/work/past/`](docs/work/past/index.md). We carry the ideas forward and write our own code, crediting by name whoever inspired a feature. +We built and maintained these, so MoonLight rests on our own hands-on experience. Their lessons are distilled in [`docs/work/past/`](docs/work/past/index.md). We carry the ideas forward and write our own code, crediting by name whoever inspired a feature. ## Credits -People whose work directly shaped parts of projectMM. We study their thinking with respect and write our own code against our architecture: +People whose work directly shaped parts of MoonLight. We study their thinking with respect and write our own code against our architecture: -- **[WLED](https://github.com/wled/WLED) and [WLED-MM](https://github.com/MoonModules/WLED)**: projectMM is born out of WLED and takes the usermod idea further, where everything is a module. A projectMM device also acts as a WLED device and talks to WLED devices. +- **[WLED](https://github.com/wled/WLED) and [WLED-MM](https://github.com/MoonModules/WLED)**: MoonLight is born out of WLED and takes the usermod idea further, where everything is a module. A MoonLight device also acts as a WLED device and talks to WLED devices. - **Frank ([softhack007](https://github.com/softhack007))**: main author of the WLED-MM audio-reactive usermod. The ideas behind [AudioService](docs/moonmodules/core/moxygen/AudioService.md), including the adaptive noise gate analyzed with his permission, descend from years of collaboration. - **[troyhacks](https://github.com/troyhacks/WLED)**: reworked the WLED-MM audio DSP onto Espressif's [esp-dsp](https://github.com/espressif/esp-dsp) FFT, the same choice AudioService makes. - **[Stefan Petrick](https://github.com/StefanPetrick)**: the generative-field vocabulary from [Animartrix](https://github.com/StefanPetrick/animartrix), [FunkyNoise](https://github.com/StefanPetrick/FunkyNoise) and [ColorTrails](https://github.com/StefanPetrick/ColorTrails). [Aurora](docs/moonmodules/light/effects.md#aurora), [PolarNoise](docs/moonmodules/light/effects.md#polarnoise), [Tunnel](docs/moonmodules/light/effects.md#tunnel) and [Trails](docs/moonmodules/light/effects.md#trails) sit in that tradition, written on the published algorithms underneath. - **[hpwit](https://github.com/hpwit) (Yves Bazin)**: the clockless I2S, RMT and Parlio driver techniques, and the [ESPLiveScript](https://github.com/hpwit/ESPLiveScript) engine behind MoonLive. -- **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**: author of the native [WLED-Android](https://github.com/Moustachauve/WLED-Android) and [WLED-iOS](https://github.com/Moustachauve/WLED-iOS) apps, whose source let projectMM devices appear in them. +- **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**: author of the native [WLED-Android](https://github.com/Moustachauve/WLED-Android) and [WLED-iOS](https://github.com/Moustachauve/WLED-iOS) apps, whose source let MoonLight devices appear in them. - **The [Improv Wi-Fi](https://github.com/improv-wifi) project**: the open serial provisioning standard the web installer uses. -- **[FastLED](https://github.com/FastLED/FastLED)**: the canonical LED-effects library whose names and models projectMM carries forward (`scale8`, `sin8`, the gradient-palette model, the `beatsin8` family) so a contributor recognizes them on sight. The implementations are our own, integer-only and hot-path-tuned. +- **[FastLED](https://github.com/FastLED/FastLED)**: the canonical LED-effects library whose names and models MoonLight carries forward (`scale8`, `sin8`, the gradient-palette model, the `beatsin8` family) so a contributor recognizes them on sight. The implementations are our own, integer-only and hot-path-tuned. - **[FPP](https://github.com/FalconChristmas/fpp) (Falcon Player)**: the show player that prompted [PanelCardDriver](docs/moonmodules/light/drivers.md#panelcard): if a Linux host can feed a wall of HUB75 panels, so can the board already rendering them. - **[Tasmota](https://github.com/arendst/Tasmota) and Mathieu Carbou's [MycilaSafeBoot](https://github.com/mathieucarbou/MycilaSafeBoot)**: the safeboot pattern behind [MoonBase](docs/explanation/architecture/moonbase.md), our from-scratch minimal take on it. - **Damian Schneider ([dedehai](https://github.com/DedeHai))**: author of the WLED Particle System, whose shape our [particle kernel](docs/moonmodules/light/power-functions.md#particles) follows in fixed point. @@ -230,7 +230,7 @@ People whose work directly shaped parts of projectMM. We study their thinking wi ## Contributing -projectMM is a community project, shaped by the people who use it: +MoonLight is a community project, shaped by the people who use it: - **Ideas and requests**: an effect, a layout, a driver, a fixture you want supported? [Open an issue](https://github.com/MoonModules/projectMM/issues). - **Help build it**: pick something from the [issues](https://github.com/MoonModules/projectMM/issues), or propose a module. The process is in [CLAUDE.md](CLAUDE.md). diff --git a/docker-compose.yml b/docker-compose.yml index 63e74c39..d7f7d3af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# projectMM in a container: the desktop firmware, which is the whole system without an ESP32. +# MoonLight in a container: the desktop firmware, which is the whole system without an ESP32. # # docker compose up -d start it, in the background # docker compose logs -f watch it @@ -24,13 +24,13 @@ services: ports: # CHANGE ME: the port YOU open in the browser is the left one. 8081 rather than 8080 so a - # container never fights a projectMM already installed on the machine. The right side is the + # container never fights a MoonLight already installed on the machine. The right side is the # port inside the container and does not change: containers do not share a port space, so # several instances can all listen on 8080 internally with different left-hand numbers. - "8081:8080" volumes: - # Config, presets and scripts. Everything the app writes lands in /data/projectMM, so this one + # Config, presets and scripts. Everything the app writes lands in /data/MoonLight, so this one # mount is the whole of its state: remove it and a restart comes up factory-fresh. - projectmm-data:/data diff --git a/docs/assets/light/effects/PulseEffect.gif b/docs/assets/light/effects/PulseEffect.gif new file mode 100644 index 00000000..339b42d0 Binary files /dev/null and b/docs/assets/light/effects/PulseEffect.gif differ diff --git a/docs/assets/light/effects/PulseEffect.png b/docs/assets/light/effects/PulseEffect.png new file mode 100644 index 00000000..1ac053cb Binary files /dev/null and b/docs/assets/light/effects/PulseEffect.png differ diff --git a/docs/assets/uiscenarios/01-install-desktop.webm b/docs/assets/uiscenarios/01-install-desktop.webm new file mode 100644 index 00000000..0186c543 Binary files /dev/null and b/docs/assets/uiscenarios/01-install-desktop.webm differ diff --git a/docs/assets/uiscenarios/01-install-esp32.webm b/docs/assets/uiscenarios/01-install-esp32.webm new file mode 100644 index 00000000..76fab1ab Binary files /dev/null and b/docs/assets/uiscenarios/01-install-esp32.webm differ diff --git a/docs/assets/uiscenarios/02-first-look-desktop.webm b/docs/assets/uiscenarios/02-first-look-desktop.webm new file mode 100644 index 00000000..107c2ce8 Binary files /dev/null and b/docs/assets/uiscenarios/02-first-look-desktop.webm differ diff --git a/docs/assets/uiscenarios/02-first-look-esp32.webm b/docs/assets/uiscenarios/02-first-look-esp32.webm new file mode 100644 index 00000000..7be91a46 Binary files /dev/null and b/docs/assets/uiscenarios/02-first-look-esp32.webm differ diff --git a/docs/assets/uiscenarios/03-second-look.webm b/docs/assets/uiscenarios/03-second-look.webm new file mode 100644 index 00000000..571e2459 Binary files /dev/null and b/docs/assets/uiscenarios/03-second-look.webm differ diff --git a/docs/assets/uiscenarios/04-scenario-testing.webm b/docs/assets/uiscenarios/04-scenario-testing.webm new file mode 100644 index 00000000..d7b0e99d Binary files /dev/null and b/docs/assets/uiscenarios/04-scenario-testing.webm differ diff --git a/docs/assets/uiscenarios/show-the-preview.webm b/docs/assets/uiscenarios/91-show-the-preview.webm similarity index 100% rename from docs/assets/uiscenarios/show-the-preview.webm rename to docs/assets/uiscenarios/91-show-the-preview.webm diff --git a/docs/assets/uiscenarios/change-layout.webm b/docs/assets/uiscenarios/92-change-layout.webm similarity index 100% rename from docs/assets/uiscenarios/change-layout.webm rename to docs/assets/uiscenarios/92-change-layout.webm diff --git a/docs/assets/uiscenarios/add-an-effect.webm b/docs/assets/uiscenarios/93-add-an-effect.webm similarity index 100% rename from docs/assets/uiscenarios/add-an-effect.webm rename to docs/assets/uiscenarios/93-add-an-effect.webm diff --git a/docs/assets/uiscenarios/add-a-modifier.webm b/docs/assets/uiscenarios/94-add-a-modifier.webm similarity index 100% rename from docs/assets/uiscenarios/add-a-modifier.webm rename to docs/assets/uiscenarios/94-add-a-modifier.webm diff --git a/docs/assets/uiscenarios/add-a-layer.webm b/docs/assets/uiscenarios/95-add-a-layer.webm similarity index 100% rename from docs/assets/uiscenarios/add-a-layer.webm rename to docs/assets/uiscenarios/95-add-a-layer.webm diff --git a/docs/assets/uiscenarios/swap-an-effect.webm b/docs/assets/uiscenarios/96-swap-an-effect.webm similarity index 100% rename from docs/assets/uiscenarios/swap-an-effect.webm rename to docs/assets/uiscenarios/96-swap-an-effect.webm diff --git a/docs/assets/uiscenarios/write-an-effect.webm b/docs/assets/uiscenarios/97-write-an-effect.webm similarity index 100% rename from docs/assets/uiscenarios/write-an-effect.webm rename to docs/assets/uiscenarios/97-write-an-effect.webm diff --git a/docs/assets/uiscenarios/react-to-sound.webm b/docs/assets/uiscenarios/98-react-to-sound.webm similarity index 100% rename from docs/assets/uiscenarios/react-to-sound.webm rename to docs/assets/uiscenarios/98-react-to-sound.webm diff --git a/docs/assets/uiscenarios/install-firmware.webm b/docs/assets/uiscenarios/install-firmware.webm deleted file mode 100644 index 19f9188c..00000000 Binary files a/docs/assets/uiscenarios/install-firmware.webm and /dev/null differ diff --git a/docs/contributing/documentation-standards.md b/docs/contributing/documentation-standards.md index 71eda51d..58fc019b 100644 --- a/docs/contributing/documentation-standards.md +++ b/docs/contributing/documentation-standards.md @@ -123,7 +123,7 @@ Every page serves one of four reader needs, and only one. This is [Diátaxis](ht | **Learning** | **Tutorial**: a lesson to follow. `gettingstarted.md`, `tutorials/`. | **Explanation**: why it is shaped this way. `explanation/`. | | **Working** | **How-to**: one task you already have. `how-to/`. | **Reference**: facts, fast. `reference/`, the generated technical pages, the catalog rows. | -**The folder under `docs/` is the type.** A page's path says which cell it sits in. So `how-to/building.md` is a how-to by location, and a reader never has to be told. The nav labels stay reader-facing ("Understanding projectMM" over "Explanation"), because the type is a writer's tool. +**The folder under `docs/` is the type.** A page's path says which cell it sits in. So `how-to/building.md` is a how-to by location, and a reader never has to be told. The nav labels stay reader-facing ("Understanding MoonLight" over "Explanation"), because the type is a writer's tool. The test for any page is the cell it sits in. A tutorial that stops to explain, or a reference that starts to teach, is two pages: move the other half to where it belongs. diff --git a/docs/explanation/architecture/index.md b/docs/explanation/architecture/index.md index 9aa3c0dc..2d7e6b8e 100644 --- a/docs/explanation/architecture/index.md +++ b/docs/explanation/architecture/index.md @@ -1,6 +1,6 @@ # Architecture -The agreed-up-front **architecture contract**: what projectMM is designed to be. A design described here is committed, meaning this is the intended behavior and code is written toward it, rather than optional or undecided. +The agreed-up-front **architecture contract**: what MoonLight is designed to be. A design described here is committed, meaning this is the intended behavior and code is written toward it, rather than optional or undecided. Coding conventions live in [coding-standards.md](../../contributing/coding-standards.md); how to build and run lives in [building.md](../../how-to/building.md); what is tested lives in [testing.md](../../reference/testing.md). @@ -12,7 +12,7 @@ Driving a large LED installation sets three constraints at once. - **Variety**: strips, panels, DMX fixtures and moving heads, each with its own wire protocol and its own definition of a pixel. - **Change**: you rearrange the show while it runs, with no reboot and no recompile. -A fixed pipeline holds the frame rate but cannot be rearranged. A scriptable one rearranges but cannot hold the frame rate. projectMM meets all three with one uniform building block on a known lifecycle, a domain-neutral core that owns the hard constructs once, and a light domain that stays simple on top of it. +A fixed pipeline holds the frame rate but cannot be rearranged. A scriptable one rearranges but cannot hold the frame rate. MoonLight meets all three with one uniform building block on a known lifecycle, a domain-neutral core that owns the hard constructs once, and a light domain that stays simple on top of it. ## The parts, and how they sit diff --git a/docs/explanation/architecture/moonbase.md b/docs/explanation/architecture/moonbase.md index 842a7ff9..01180d1c 100644 --- a/docs/explanation/architecture/moonbase.md +++ b/docs/explanation/architecture/moonbase.md @@ -9,7 +9,7 @@ flowchart LR subgraph flash["one flash, two images"] direction TB base["factory slot
MoonBase, ~750 KB
small and rarely changing"] - app["app slot
projectMM
one copy, not two"] + app["app slot
MoonLight
one copy, not two"] fs["filesystem
the space the second
app slot used to hold"] end diff --git a/docs/explanation/architecture/mooncore.md b/docs/explanation/architecture/mooncore.md index 03b7a489..bb4468d3 100644 --- a/docs/explanation/architecture/mooncore.md +++ b/docs/explanation/architecture/mooncore.md @@ -77,7 +77,7 @@ A device has **one** network name, `deviceName`, and every name it presents on t - **Always a valid hostname.** Because all three uses are DNS/SSID names, `deviceName` must satisfy the RFC-1123 label rules (`[A-Za-z0-9-]`, no spaces, no leading/trailing hyphen). `SystemModule` enforces this at the source: it runs `mm::sanitizeHostname()` (in `core/Control.h`) on the value in `setup()` and every `tick1s()`, coercing whatever the user typed or persistence restored (`"My Living Room!"` → `"My-Living-Room"`) and falling back to the MAC-derived `MM-XXXX` if the result is empty. Sanitising *at the owner* means every consumer is correct for free, no per-consumer validation, no chance a raw name reaches mDNS. (`unit_sanitizeHostname` pins the rule.) - **Follows a live rename.** Renaming the device re-advertises immediately, no reboot, the [live-reconfiguration](moonmodule.md#live-reconfiguration-every-change-applies-on-the-next-frame) rule applied to identity. `NetworkModule::syncMdns()` (called each `tick1s()`) compares the current name to the last-registered one and re-registers mDNS when it changed, so `.local` resolves within a tick. -**A machine-facing identity that an external system binds to is never the editable name.** An MQTT topic prefix, a Home Assistant discovery `unique_id`, an API key path: anything a foreign system keys off must derive from an immutable hardware id such as the MAC or chip-id, as in `projectMM/`. A live `deviceName` rename would otherwise repoint every topic silently and orphan the peer's config. The human-readable name rides a *separate*, published-but-non-identifying field (WLED, Tasmota, ESPHome, and HA discovery all anchor identity this way). `deviceName` above is the network-presentation identity (mDNS / AP / DHCP, where the name *is* the address); an external-integration identity is the opposite case and stays decoupled from it. +**A machine-facing identity that an external system binds to is never the editable name.** An MQTT topic prefix, a Home Assistant discovery `unique_id`, an API key path: anything a foreign system keys off must derive from an immutable hardware id such as the MAC or chip-id, as in `MoonLight/`. A live `deviceName` rename would otherwise repoint every topic silently and orphan the peer's config. The human-readable name rides a *separate*, published-but-non-identifying field (WLED, Tasmota, ESPHome, and HA discovery all anchor identity this way). `deviceName` above is the network-presentation identity (mDNS / AP / DHCP, where the name *is* the address); an external-integration identity is the opposite case and stays decoupled from it. ## Web UI diff --git a/docs/explanation/architecture/moonlight.md b/docs/explanation/architecture/moonlight.md index e8bece07..5984a38a 100644 --- a/docs/explanation/architecture/moonlight.md +++ b/docs/explanation/architecture/moonlight.md @@ -242,7 +242,7 @@ The trade is deliberate: **a chain must be homogeneous**. Mixing fixture types o ## Multicast and IGMP snooping -Three things projectMM sends to more than one listener, and they do not all use the same transport, because the protocol's owner decides it and not us: +Three things MoonLight sends to more than one listener, and they do not all use the same transport, because the protocol's owner decides it and not us: | | transport | why | |---|---|---| diff --git a/docs/explanation/architecture/moonlive.md b/docs/explanation/architecture/moonlive.md index 1048c1b1..ea69bfcd 100644 --- a/docs/explanation/architecture/moonlive.md +++ b/docs/explanation/architecture/moonlive.md @@ -70,10 +70,10 @@ The engine stays target-agnostic because all three live behind the platform boun Compiling a small language straight to machine code, so a live-authored effect runs at the speed of a compiled one, was pioneered by Yves Bazin in [ESPLiveScript](https://github.com/hpwit/ESPLiveScript). That work drives a 12,288-light panel at around 85 fps where interpreted languages managed 3 to 10, which is what makes native codegen the right call here. -MoonLive is written fresh against projectMM's architecture, with ESPLiveScript as the reference it is checked against. +MoonLive is written fresh against MoonLight's architecture, with ESPLiveScript as the reference it is checked against. The live-scripting idea in this ecosystem descends from ARTI, the interpreted-effects runtime in WLED MoonModules, which proved the load-and-run loop end to end. -The host-binding surface follows the [MoonLight effects tutorial](https://moonmodules.org/MoonLight/moonlight/effects-tutorial/). +The host-binding surface follows the [MoonLight effects tutorial](https://moonmodules.org/projectMM/moonlight/effects-tutorial/). ## A scripted module is still a module diff --git a/docs/explanation/architecture/moonmodule.md b/docs/explanation/architecture/moonmodule.md index d478e02d..cbaa3931 100644 --- a/docs/explanation/architecture/moonmodule.md +++ b/docs/explanation/architecture/moonmodule.md @@ -183,7 +183,7 @@ This is the recognized layout/prepare-pass pattern (JUCE `prepareToPlay`, UIKit A pin, leds-per-pin, protocol or mic-rate edit flows control-write to `onControlChanged` at tier 1, and when it changes shape, on to `prepare()` at tier 3. That rebuilds the derived state that changed, and nothing else: an LED driver re-targets its RMT or DMA onto the new GPIOs, an audio module re-inits I²S, an effect re-sizes, the Layer rebuilds its LUT. The render loop reads the result on the next tick. This holds for every module type because the rebuild chain is core, and it composes with the [robustness rule](#robustness): any change, any order, keeps the device running. Only a *firmware* OTA flash needs a power cycle, the same physical boundary the robustness rule draws. -A module that must notify one specific other module of an event, rather than publish data for polling or change its own controls, makes a direct method call to a known consumer. `ImprovProvisioningModule::tick1s` calls `networkModule_->setWifiCredentials(...)` when credentials arrive over UART. No event bus; the producer holds a pointer to the consumer set at wiring time (`main.cpp`). Pub/sub becomes the right pattern only when there are multiple unknown subscribers per event; projectMM has none today. +A module that must notify one specific other module of an event, rather than publish data for polling or change its own controls, makes a direct method call to a known consumer. `ImprovProvisioningModule::tick1s` calls `networkModule_->setWifiCredentials(...)` when credentials arrive over UART. No event bus; the producer holds a pointer to the consumer set at wiring time (`main.cpp`). Pub/sub becomes the right pattern only when there are multiple unknown subscribers per event; MoonLight has none today. ## Robustness diff --git a/docs/explanation/mooncloud.md b/docs/explanation/mooncloud.md index 39b8c24e..e8fbc529 100644 --- a/docs/explanation/mooncloud.md +++ b/docs/explanation/mooncloud.md @@ -1,6 +1,6 @@ # MoonCloud -MoonCloud is everything projectMM does with a server MoonModules runs. It is off until you switch it on, and it is one self-contained part of the software: its members are the only code that talks to a server of ours. +MoonCloud is everything MoonLight does with a server MoonModules runs. It is off until you switch it on, and it is one self-contained part of the software: its members are the only code that talks to a server of ours. It is deliberately small. Each member is a separate choice with its own checkbox, because wanting one is not agreeing to the other, and each says on its own card what it exchanges before you decide. What every member sends, what never leaves your network, and how the installation id works: [privacy policy](../legal/privacy-policy.md). The controls, per member: [core system catalog](../moonmodules/core/system.md#mooncloud). @@ -29,7 +29,7 @@ The totals are shown on the same card that asks: contributing earns the answer b ## Talk -A public message board between projectMM devices. Off until you turn it on, and a message is sent only because you typed one and pressed send. +A public message board between MoonLight devices. Off until you turn it on, and a message is sent only because you typed one and pressed send. Everything posted is public and permanent: no private message, no recipient, no delete. Your device name rides along only if you separately switch that on; otherwise messages show the first 8 characters of your installation id, which groups them without naming you, the way a Meshtastic node id does. diff --git a/docs/explanation/why-we-write-our-own.md b/docs/explanation/why-we-write-our-own.md index 03abdbee..fac10f0e 100644 --- a/docs/explanation/why-we-write-our-own.md +++ b/docs/explanation/why-we-write-our-own.md @@ -4,7 +4,7 @@ title: Why we write our own code # Why we write our own code -projectMM pulls in no third-party libraries: no FastLED, no ESPAsyncWebServer, no ArduinoJson. A library that is genuinely needed lives behind the platform boundary in `src/platform/`, never in core or the light domain. The *what*, with the replacement for each, is [below](#third-party-libraries). The *why* comes first. +MoonLight pulls in no third-party libraries: no FastLED, no ESPAsyncWebServer, no ArduinoJson. A library that is genuinely needed lives behind the platform boundary in `src/platform/`, never in core or the light domain. The *what*, with the replacement for each, is [below](#third-party-libraries). The *why* comes first. ## A dependency is a hole in the test coverage @@ -26,11 +26,11 @@ What changed is the effort of writing code, and what changed it is AI agents. Th ## Why agents at all -Using agents to build open-source software is contested, and a page that credits them with making this project possible cannot reasonably skip past that. So, briefly and once: where we stand. +Using agents to build open-source software is contested, and a page that credits them with making this project possible cannot reasonably skip past that. -We use AI agents because the technology is not going away, and the only way to learn what a tool does, where it is strong and where it quietly fails, is to run a real project on it. +AI agents are a new way of working, and a genuinely different one rather than a faster version of what came before. We have worries about AI as it stands, and it is also not going away. Having followed it since the eighties, we would rather understand it than pretend it is absent. -The two objections we hear most are that agents take developers' jobs, and that the energy they burn is not worth it. On both we have a position rather than an argument: we think AI changes jobs rather than takes them, the way computers changed office work from the 1990s onward, and we think the energy cost is defensible. We are not going to argue either here, and neither is a claim that everyone should work this way. +So the position is neither enthusiasm nor resignation. A tool this consequential is better understood by using it seriously than by arguing about it from outside. Learning what one does, where it is strong and where it quietly fails, means running a real project on it. It also means holding that project to a standard that would expose the failures. What follows is that standard, and what it costs. ## The limits of this argument @@ -48,7 +48,7 @@ Writing your own implementation of a known idea can land in either column, and w **Study, not skim.** This is the row that AI agents genuinely threaten, and it is worth naming rather than glossing. An agent can reproduce a working pattern without anyone involved understanding why it works, which is skimming with better output. The countermeasure is structural: each feature is spec'd from the primary source, the datasheet, the standard, the textbook algorithm, before it is written, and every line and every spec is reviewed. If the reasoning behind a piece of code cannot be stated, it does not go in. That standard is more work, not less. -**Ideas, not code.** We are not trying to acquire anyone's implementation. What travels is the idea: an approach to a problem, a technique someone proved works on real hardware, a mistake worth not repeating. Most of what projectMM implements is publicly defined. Art-Net, E1.31/sACN, DDP, WS2812 timing, the peripheral datasheets, textbook DSP: those are industry standards, not anyone's property, and we implement them from the primary source. Textbook algorithm, textbook name, our implementation. +**Ideas, not code.** We are not trying to acquire anyone's implementation. What travels is the idea: an approach to a problem, a technique someone proved works on real hardware, a mistake worth not repeating. Most of what MoonLight implements is publicly defined. Art-Net, E1.31/sACN, DDP, WS2812 timing, the peripheral datasheets, textbook DSP: those are industry standards, not anyone's property, and we implement them from the primary source. Textbook algorithm, textbook name, our implementation. **Steal from many, not one.** A rewrite that is one library with the names changed is a rip-off, whoever or whatever typed it. What is here comes from several sources, from the standards themselves, from what this hardware forces on you, and from years of our own prior work. diff --git a/docs/friend-repos/FastLED-FastLED.md b/docs/friend-repos/FastLED-FastLED.md index b8ceba31..c9532340 100644 --- a/docs/friend-repos/FastLED-FastLED.md +++ b/docs/friend-repos/FastLED-FastLED.md @@ -1,6 +1,6 @@ # FastLED: monthly activity digest -What landed on [FastLED](https://github.com/FastLED/FastLED)'s main branch, month by month. External-context reference (like the v1/v2/MoonLight inventories), a factual log of a friend repo's releases, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these digests lives in [README.md](index.md). +What landed on [FastLED](https://github.com/FastLED/FastLED)'s main branch, month by month. External-context reference (like the v1/v2/MoonLight inventories), a factual log of a friend repo's releases, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these digests lives in [README.md](index.md). ## August 2026 diff --git a/docs/friend-repos/Funkelfetisch-projectMM.md b/docs/friend-repos/Funkelfetisch-projectMM.md index 3a33857e..4ded7979 100644 --- a/docs/friend-repos/Funkelfetisch-projectMM.md +++ b/docs/friend-repos/Funkelfetisch-projectMM.md @@ -1,8 +1,8 @@ -# Funkelfetisch/projectMM: monthly activity digest +# Funkelfetisch/MoonLight: monthly activity digest -What landed on [Funkelfetisch/projectMM](https://github.com/Funkelfetisch/projectMM), month by month. External-context reference, a factual log of a friend repo's activity, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [Funkelfetisch/MoonLight](https://github.com/Funkelfetisch/MoonLight), month by month. External-context reference, a factual log of a friend repo's activity, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). -This is a **fork of this project** building a commercial product on it: **HELIO**, a physical "infinity sphere" lamp (a warm-white 3000 K RGBW strip inside a clear acrylic shell). The fork's own plan documents describe matching a browser preview to "the transmitted LED frame, warm-white output, clear acrylic, internal reflections, and optional wall bounce", and its firmware carries a `helio1` sdkconfig variant, a curated preset player, and a branded update channel. The README is unchanged from ours, so this is projectMM plus a product layer rather than a rebrand. +This is a **fork of this project** building a commercial product on it: **HELIO**, a physical "infinity sphere" lamp (a warm-white 3000 K RGBW strip inside a clear acrylic shell). The fork's own plan documents describe matching a browser preview to "the transmitted LED frame, warm-white output, clear acrylic, internal reflections, and optional wall bounce", and its firmware carries a `helio1` sdkconfig variant, a curated preset player, and a branded update channel. The README is unchanged from ours, so this is MoonLight plus a product layer rather than a rebrand. **Branch note: the work is not on the default branch.** `main` tracks our upstream and has not moved since 2026-07-09; every change lives in named branches, so each month below carries a **Branches** line for what moved on them. The repository has no issues and publishes no releases. @@ -16,7 +16,7 @@ This is a **fork of this project** building a commercial product on it: **HELIO* - **Branches:** only `codex/helio-private-wip` moved (2026-08-20). The other seven have been dormant since July. -_Checked: commits on `main` for 2026-08-01..2026-09-01 (0; `main` last moved 2026-07-09); commits on all 9 branches for the same window, filtered to fork-authored work (1: `a649bd47` on `codex/helio-private-wip`, 2026-08-20, 139 files; the branch's other August commits are upstream `MoonModules/projectMM` carry-forward); no commit on any branch between 2026-08-21 and 2026-09-01; releases published (none); issue search `repo:Funkelfetisch/projectMM is:issue created:2026-08-01..2026-08-31` and the same with `closed:` (0 results each, the repository has no issue tracker activity)._ +_Checked: commits on `main` for 2026-08-01..2026-09-01 (0; `main` last moved 2026-07-09); commits on all 9 branches for the same window, filtered to fork-authored work (1: `a649bd47` on `codex/helio-private-wip`, 2026-08-20, 139 files; the branch's other August commits are upstream `MoonModules/projectMM` carry-forward); no commit on any branch between 2026-08-21 and 2026-09-01; releases published (none); issue search `repo:Funkelfetisch/MoonLight is:issue created:2026-08-01..2026-08-31` and the same with `closed:` (0 results each, the repository has no issue tracker activity)._ ## July 2026 @@ -31,7 +31,7 @@ Six feature branches opened, none merged to the fork's `main`, alongside a carry - **Branches:** `codex/upstream-auto-update-manifest` (07-11), `codex/upstream-network-sta-reconnect` (07-10), `codex/upstream-rmt-rgbw-performance` (07-10), `codex/performance-frame-pacing` (07-11), `codex/universal-ble-provisioning` (07-13), `codex/helio-private-wip` (07-10, initial WIP), `next-iteration` (07-10, a carry-forward of our upstream branch). `main` last moved 2026-07-09. -_Checked: commits on `main` for author-date 2026-07-01..2026-08-01 (2, both upstream carry-forward); commits on all branches vs `MoonModules/projectMM@main` for the same window (11); releases published (none); issue search `repo:Funkelfetisch/projectMM is:issue created:2026-07-01..2026-07-31` and the same with `closed:` (0 results each)._ +_Checked: commits on `main` for author-date 2026-07-01..2026-08-01 (2, both upstream carry-forward); commits on all branches vs `MoonModules/projectMM@main` for the same window (11); releases published (none); issue search `repo:Funkelfetisch/MoonLight is:issue created:2026-07-01..2026-07-31` and the same with `closed:` (0 results each)._ ## June 2026 @@ -39,4 +39,4 @@ _Checked: commits on `main` for author-date 2026-07-01..2026-08-01 (2, both upst - **Branches:** `feature/gyro_module` (06-05). No other branch activity. -_Checked: commits on all branches vs `MoonModules/projectMM@main` for author-date 2026-06-01..2026-07-01 (1); releases published (none); issue search `repo:Funkelfetisch/projectMM is:issue created:2026-06-01..2026-06-30` and the same with `closed:` (0 results each)._ +_Checked: commits on all branches vs `MoonModules/projectMM@main` for author-date 2026-06-01..2026-07-01 (1); releases published (none); issue search `repo:Funkelfetisch/MoonLight is:issue created:2026-06-01..2026-06-30` and the same with `closed:` (0 results each)._ diff --git a/docs/friend-repos/MoonModules-WLED-MM.md b/docs/friend-repos/MoonModules-WLED-MM.md index d646b689..e0ae39cb 100644 --- a/docs/friend-repos/MoonModules-WLED-MM.md +++ b/docs/friend-repos/MoonModules-WLED-MM.md @@ -1,6 +1,6 @@ # WLED-MM: monthly activity digest -What landed on [WLED-MM](https://github.com/MoonModules/WLED-MM)'s `mdev` (default) branch, month by month. External-context reference, a factual log of a friend repo's releases, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). Months are split at versioned-release boundaries (the rolling `nightly` tag is not a release). +What landed on [WLED-MM](https://github.com/MoonModules/WLED-MM)'s `mdev` (default) branch, month by month. External-context reference, a factual log of a friend repo's releases, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). Months are split at versioned-release boundaries (the rolling `nightly` tag is not a release). ## August 2026 diff --git a/docs/friend-repos/PlummersSoftwareLLC-NightDriverStrip.md b/docs/friend-repos/PlummersSoftwareLLC-NightDriverStrip.md index fadf06dc..b93b03b8 100644 --- a/docs/friend-repos/PlummersSoftwareLLC-NightDriverStrip.md +++ b/docs/friend-repos/PlummersSoftwareLLC-NightDriverStrip.md @@ -1,6 +1,6 @@ # NightDriverStrip: monthly activity digest -What landed on [NightDriverStrip](https://github.com/PlummersSoftwareLLC/NightDriverStrip)'s `main` branch, month by month. External-context reference, a factual log of a friend repo's releases, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [NightDriverStrip](https://github.com/PlummersSoftwareLLC/NightDriverStrip)'s `main` branch, month by month. External-context reference, a factual log of a friend repo's releases, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). Summarised via the GitHub commits API (no local clone), so counts are all commits on `main`, not first-parent merges, the bullets filter out dependency bumps, whitespace, and pure refactors. Releases are noted as context rather than used as month boundaries: **v1.3.0** (published 2026-01-10) was tagged from a late-November commit, and the latest are **v2.0.0** and **v2.0.1**, both published 2026-06-14. v1.3.0 is not a clean month boundary so its month is kept whole; June IS split at v2.0.0, which was cut from `main` mid-month (see the two June sections below). diff --git a/docs/friend-repos/hpwit-ESPLiveScript.md b/docs/friend-repos/hpwit-ESPLiveScript.md index 26a9d2aa..8bd6d671 100644 --- a/docs/friend-repos/hpwit-ESPLiveScript.md +++ b/docs/friend-repos/hpwit-ESPLiveScript.md @@ -1,6 +1,6 @@ # hpwit/ESPLiveScript: monthly activity digest -What landed on [hpwit/ESPLiveScript](https://github.com/hpwit/ESPLiveScript), month by month. External-context reference, a factual log of a friend repo's activity, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [hpwit/ESPLiveScript](https://github.com/hpwit/ESPLiveScript), month by month. External-context reference, a factual log of a friend repo's activity, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). The library: Yves Bazin's (hpwit) C-like compiler/interpreter for the ESP32, small scripts (e.g. LED effects) compiled and run live on-device without a full recompile-and-flash cycle. Summarised via the GitHub commits API. diff --git a/docs/friend-repos/hpwit-I2SClocklessLedDriver.md b/docs/friend-repos/hpwit-I2SClocklessLedDriver.md index 970ebcd9..6e9c9a76 100644 --- a/docs/friend-repos/hpwit-I2SClocklessLedDriver.md +++ b/docs/friend-repos/hpwit-I2SClocklessLedDriver.md @@ -1,10 +1,10 @@ # hpwit/I2SClocklessLedDriver: monthly activity digest -What landed on [hpwit/I2SClocklessLedDriver](https://github.com/hpwit/I2SClocklessLedDriver)'s `main` branch, month by month. External-context reference, a factual log of a friend repo's activity, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [hpwit/I2SClocklessLedDriver](https://github.com/hpwit/I2SClocklessLedDriver)'s `main` branch, month by month. External-context reference, a factual log of a friend repo's activity, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). -The library: Yves Bazin's (hpwit) clockless-LED driver that clocks WS2812-class strips out over the ESP32's I2S/LCD peripheral with DMA, the parallel-output technique projectMM's LED-driver analysis studies. Summarised via the GitHub commits API (all commits on `main`), filtering out merge commits, lint/format churn, and 🐰-review fixups. No versioned release is cut from `main` in this window (latest tag is 1.4), so months are kept whole. +The library: Yves Bazin's (hpwit) clockless-LED driver that clocks WS2812-class strips out over the ESP32's I2S/LCD peripheral with DMA, the parallel-output technique MoonLight's LED-driver analysis studies. Summarised via the GitHub commits API (all commits on `main`), filtering out merge commits, lint/format churn, and 🐰-review fixups. No versioned release is cut from `main` in this window (latest tag is 1.4), so months are kept whole. -> **Authorship note.** Most of the activity in this window is projectMM's own, `ewowi` authored ~53 of the in-window commits, with the rest from the maintainer (Yves Bazin / hpwit) and a couple of others. The IDF 5.5 / arduino-less ESP-IDF / RGBCCT / >65K-LED work below is largely projectMM upstreaming its driver needs into hpwit's library, then tracking the result here. +> **Authorship note.** Most of the activity in this window is MoonLight's own, `ewowi` authored ~53 of the in-window commits, with the rest from the maintainer (Yves Bazin / hpwit) and a couple of others. The IDF 5.5 / arduino-less ESP-IDF / RGBCCT / >65K-LED work below is largely MoonLight upstreaming its driver needs into hpwit's library, then tracking the result here. ## August 2026 diff --git a/docs/friend-repos/hpwit-I2SClocklessVirtualLedDriver.md b/docs/friend-repos/hpwit-I2SClocklessVirtualLedDriver.md index 89bc4b89..57045e84 100644 --- a/docs/friend-repos/hpwit-I2SClocklessVirtualLedDriver.md +++ b/docs/friend-repos/hpwit-I2SClocklessVirtualLedDriver.md @@ -1,8 +1,8 @@ # hpwit/I2SClocklessVirtualLedDriver: monthly activity digest -What landed on [hpwit/I2SClocklessVirtualLedDriver](https://github.com/hpwit/I2SClocklessVirtualLedDriver), month by month. External-context reference, a factual log of a friend repo's activity, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [hpwit/I2SClocklessVirtualLedDriver](https://github.com/hpwit/I2SClocklessVirtualLedDriver), month by month. External-context reference, a factual log of a friend repo's activity, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). -The library: Yves Bazin's (hpwit) "virtual pins" variant of the I2S clockless driver, drives far more strips than the chip has usable pins by fanning the I2S output through external shift registers. This multiplex technique is the load-bearing idea projectMM's LED-driver analysis singles out (factoring the shift-register multiplex out of the I2S/LCD peripheral code). Summarised via the GitHub commits API, read across all branches (`main`, `integration`, `int2`, `variable`, `hpwit-patch-1`, `dev`, `optomize`), not just `main`. +The library: Yves Bazin's (hpwit) "virtual pins" variant of the I2S clockless driver, drives far more strips than the chip has usable pins by fanning the I2S output through external shift registers. This multiplex technique is the load-bearing idea MoonLight's LED-driver analysis singles out (factoring the shift-register multiplex out of the I2S/LCD peripheral code). Summarised via the GitHub commits API, read across all branches (`main`, `integration`, `int2`, `variable`, `hpwit-patch-1`, `dev`, `optomize`), not just `main`. ## August 2026 diff --git a/docs/friend-repos/hpwit-new-parser.md b/docs/friend-repos/hpwit-new-parser.md index 3dc14778..d93fb3f6 100644 --- a/docs/friend-repos/hpwit-new-parser.md +++ b/docs/friend-repos/hpwit-new-parser.md @@ -1,6 +1,6 @@ # hpwit/new-parser (ESPLiveScript2): monthly activity digest -What landed on [hpwit/new-parser](https://github.com/hpwit/new-parser), month by month. External-context reference, a factual log of a friend repo's activity, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [hpwit/new-parser](https://github.com/hpwit/new-parser), month by month. External-context reference, a factual log of a friend repo's activity, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). The library: **ESPLiveScript2**, Yves Bazin's (hpwit) from-scratch C++ rewrite of [ESPLiveScript](https://github.com/hpwit/ESPLiveScript), the same idea (a small C-like language compiled on-device to real Xtensa machine code, no interpreter, so a script runs at near-native speed) reimplemented independently rather than refactored. The library ships inside the repo as `asmparser2/` (PlatformIO name `ESPLiveScript2`, at v1.3.0). Summarised via the GitHub commits API. diff --git a/docs/friend-repos/index.md b/docs/friend-repos/index.md index 37b2d963..48e502f7 100644 --- a/docs/friend-repos/index.md +++ b/docs/friend-repos/index.md @@ -1,6 +1,6 @@ # Friend repos, monthly activity digests -Monthly logs of what shipped on related open-source LED projects, the live landscape projectMM watches to sharpen its own designs under the *Industry standards, our own code* principle ([CLAUDE.md § Principles](../../CLAUDE.md#principles)): study to think, write fresh, never copy. Generated by the [digest prompt](#digest-prompt-reusable) below. +Monthly logs of what shipped on related open-source LED projects, the live landscape MoonLight watches to sharpen its own designs under the *Industry standards, our own code* principle ([CLAUDE.md § Principles](../../CLAUDE.md#principles)): study to think, write fresh, never copy. Generated by the [digest prompt](#digest-prompt-reusable) below. - [FastLED-FastLED.md](FastLED-FastLED.md): the LED-animation library; ESP32/Arduino driver + color math. - [wled-WLED.md](wled-WLED.md): upstream WLED firmware. @@ -20,21 +20,21 @@ Reading across the friend-repo digests, the themes the wider ESP32-LED ecosystem - **ESP32-P4 / S3 parallel output.** FastLED poured effort into the PARLIO and LCD_CAM drivers (P4/S3 parallel LED output, big encode speedups); NightDriverStrip added custom RMT output; hpwit's I2SClocklessLedDriver pushed IDF-5.5 + arduino-less-ESP-IDF support for its I2S/LCD DMA driver (the canonical implementation of this technique), and troyhacks ran ESP32-P4 bring-up branches. The frontier is parallel, DMA-driven output on the newer chips. - **PSRAM strategy is unsettled everywhere.** All four wrestled with PSRAM this cycle, WLED-MM moved preview buffers into PSRAM, NightDriverStrip did a full PSRAM-default reversal (then tuned the threshold), WLED added S3-no-PSRAM builds. Nobody has a clean answer; the cache-disabled-during-flash hazard recurs across repos. - **Audio-reactive maturing.** FastLED added a silence-gate + ESP-DSP FFT backend; WLED-MM and WLED both refined audio sync and auto-disable-during-realtime; NightDriverStrip modernised its SoundAnalyzer/FFT. Audio-reactive is table stakes now, and the polish is in *not* reacting to noise/silence. -- **The FastLED dependency question.** WLED merged a *full FastLED replacement* (its own color/math); projectMM already made the same call (own color math, no FastLED in core). Two independent projects concluded the dependency wasn't worth it. -- **UI as a firmware-driven consumer.** Both WLED and NightDriverStrip pushed toward "the official UI knows nothing the firmware doesn't publish over the wire", exactly projectMM's MoonModule-driven, no-hardcoded-knowledge UI principle. Convergent design. NightDriverStrip's **2.0.0** (June 2026) crystallised this: a brand-new web UI, a browser-based installer, and settings (like strip type) moved from compile-time to *runtime-selectable* on the device, the same "reconfigure live, no reflash" direction projectMM builds around. +- **The FastLED dependency question.** WLED merged a *full FastLED replacement* (its own color/math); MoonLight already made the same call (own color math, no FastLED in core). Two independent projects concluded the dependency wasn't worth it. +- **UI as a firmware-driven consumer.** Both WLED and NightDriverStrip pushed toward "the official UI knows nothing the firmware doesn't publish over the wire", exactly MoonLight's MoonModule-driven, no-hardcoded-knowledge UI principle. Convergent design. NightDriverStrip's **2.0.0** (June 2026) crystallised this: a brand-new web UI, a browser-based installer, and settings (like strip type) moved from compile-time to *runtime-selectable* on the device, the same "reconfigure live, no reflash" direction MoonLight builds around. - **Effect velocity.** WLED and WLED-MM shipped many new effects (PacMan, Color Clouds, Shimmer, the user_fx pack); new effects remain the most visible user-facing output. - **Display / HDMI output beyond LED strips.** troyhacks ran a cluster of branches probing HDMI video output and large hardware panels (WaveShare 10.1″, M5Stack, ESP32-P4 panels), driving *displays*, not just addressable strips, off the same firmware. - **On-device live scripting.** hpwit's ESPLiveScript compiles small C-like effect scripts that run live on the ESP32 with no reflash, a different answer to effect authoring than C++ recompilation or a fixed effect table. -## What these projects do that projectMM doesn't (yet) +## What these projects do that MoonLight doesn't (yet) -Observational: where the landscape is ahead of projectMM. These are *not* commitments; real adoption decisions live in the [`../backlog/`](../work/future/index.md), cross-referenced where one already exists. +Observational: where the landscape is ahead of MoonLight. These are *not* commitments; real adoption decisions live in the [`../backlog/`](../work/future/index.md), cross-referenced where one already exists. - **Parallel multi-strip output on S3/P4** (PARLIO/LCD_CAM, and hpwit's I2S/shift-register drivers), the direct parallel drivers ship (MultiPin/Moon on LCD_CAM, Parlio on P4, driving up to 16 strands and 12,288+ lights). The shift-register/'595 expander path also ships but is dormant: it works at prime-only geometries yet has a known lapping-ring sparkle at the largest configs, so it stays off by default. See the [LED-driver analysis](../work/future/leddriver-analysis-top-down.md). -- **Audio-reactive input**: none of projectMM's effects are audio- or motion-reactive yet. The Peripheral role + the Pi-sensor backlog entry are the foundation; the producer→effect wiring is backlog. -- **A guided setup/installer wizard on-device** (NightDriverStrip's Setup Wizard, WLED's installer), projectMM has the web installer + Improv, but no on-device first-run wizard. -- **A large built-in effect library**: projectMM ships a focused set (concrete-first); the WLED family ships dozens. Breadth is a deliberate non-goal until the core is proven. -- **On-device live effect scripting** (hpwit's ESPLiveScript), projectMM effects are compiled C++; there's no runtime script path. Not a goal today, noted as a landscape contrast. +- **Audio-reactive input**: none of MoonLight's effects are audio- or motion-reactive yet. The Peripheral role + the Pi-sensor backlog entry are the foundation; the producer→effect wiring is backlog. +- **A guided setup/installer wizard on-device** (NightDriverStrip's Setup Wizard, WLED's installer), MoonLight has the web installer + Improv, but no on-device first-run wizard. +- **A large built-in effect library**: MoonLight ships a focused set (concrete-first); the WLED family ships dozens. Breadth is a deliberate non-goal until the core is proven. +- **On-device live effect scripting** (hpwit's ESPLiveScript), MoonLight effects are compiled C++; there's no runtime script path. Not a goal today, noted as a landscape contrast. ## Refreshing @@ -50,7 +50,7 @@ Adding a month or a new friend repo is the [friend-repos](index.md) workflow, an > 3. Write an **end-user-readable** summary: what changed that a *user of the library* would notice or care about, new features, new hardware/platform support, notable fixes, breaking changes. Skip internal refactors, CI, test-only, and dependency bumps unless they affect users. > 4. Format as **short bullet points**, each one line, plainest language, minimal jargon. Group only if there's a natural split (e.g. "New" / "Fixed"); otherwise a flat list. > 4b. Follow [the documentation standards](../contributing/documentation-standards.md): American spelling, **no em-dashes** (use a comma, colon, parentheses or a full stop), and one line per paragraph with no hard wraps, since `check_docgen` reports all three as errors on the file you write. -> 5. Add it as a `## ` section to `docs/friend-repos/.md`, newest month on top. Don't editorialise or compare to projectMM, just report what they shipped. +> 5. Add it as a `## ` section to `docs/friend-repos/.md`, newest month on top. Don't editorialise or compare to MoonLight, just report what they shipped. > 6. State the commit range / count **and the issue query** summarised so the digest is auditable. > > When backfilling several months (e.g. since the last release), run this once per month for a consistent timeline, then optionally add a `## Since v, overview` intro at the top with 3–5 bullets naming the multi-month threads the per-month slices can't show on their own. diff --git a/docs/friend-repos/troyhacks-WLED.md b/docs/friend-repos/troyhacks-WLED.md index 67930c43..426ef6ac 100644 --- a/docs/friend-repos/troyhacks-WLED.md +++ b/docs/friend-repos/troyhacks-WLED.md @@ -1,6 +1,6 @@ # troyhacks/WLED: monthly activity digest -What landed on [troyhacks/WLED](https://github.com/troyhacks/WLED)'s `mdev` branch, month by month. External-context reference, a factual log of a friend repo's activity, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [troyhacks/WLED](https://github.com/troyhacks/WLED)'s `mdev` branch, month by month. External-context reference, a factual log of a friend repo's activity, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). This is a personal fork of [MoonModules/WLED-MM](https://github.com/MoonModules/WLED-MM) (the `mdev` branch), so much of the `mdev` traffic is merges from and alignments with the MM and upstream WLED lines; the bullets below pick out what a *user* of this fork would notice. Summarised via the GitHub commits API (all commits on `mdev`, not first-parent merges), filtering out build-number bumps, merge commits, typo/comment churn, and pure refactors. No versioned release is cut from this branch (it tracks `mdev` and ships nightly `mdev` builds), so months are kept whole. diff --git a/docs/friend-repos/wled-WLED.md b/docs/friend-repos/wled-WLED.md index 9bb43201..a084f11d 100644 --- a/docs/friend-repos/wled-WLED.md +++ b/docs/friend-repos/wled-WLED.md @@ -1,6 +1,6 @@ # WLED (upstream): monthly activity digest -What landed on [wled/WLED](https://github.com/wled/WLED)'s `main` branch, month by month. External-context reference, a factual log of a friend repo's releases, not projectMM's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). +What landed on [wled/WLED](https://github.com/wled/WLED)'s `main` branch, month by month. External-context reference, a factual log of a friend repo's releases, not MoonLight's own history or roadmap. Newest month on top. The reusable prompt that generates these lives in [README.md](index.md). Months are **not** split at release dates: upstream WLED cuts releases from separate release branches (`0_15`, `16_x`), so the version tags aren't on `main`, `main` is the development trunk that feeds future releases. Each month notes which release shipped, as context. diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 764dfffd..dc52d848 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -1,20 +1,20 @@ # Getting started -New to ESP32 or flashing firmware? You don't need to be. projectMM installs straight from your web browser: no software to download, no command line. In a few minutes you'll have lights running and the device on your network, and the device's own web interface open in your browser ready to play with. +New to ESP32 or flashing firmware? You don't need to be. MoonLight installs straight from your web browser: no software to download, no command line. In a few minutes you'll have lights running and the device on your network, and the device's own web interface open in your browser ready to play with. -This guide has two chapters. **Chapter 1** gets projectMM onto your device. +This guide has two chapters. **Chapter 1** gets MoonLight onto your device. **Chapter 2** is a tour of the interface you land in afterwards, so you know what every part does and where to start building your own light show. **You need:** an ESP32 board, a USB cable that carries data (not charge-only), and a **Chromium-based browser** on a computer (Google Chrome, Microsoft Edge, or Opera (the installer uses the Web Serial API, which Safari and Firefox don't support). -> Want the bigger picture of what projectMM is first? See the +> Want the bigger picture of what MoonLight is first? See the > [project overview](../README.md). --- -## Chapter 1: install projectMM +## Chapter 1: install MoonLight ### 1. Open the installer and plug in @@ -22,7 +22,7 @@ Open the **[web installer](https://moonmodules.org/projectMM/install/)** in Chro ![The web installer](assets/gettingstarted/01-01-installer-start.png) - + ### 2. Pick the USB port @@ -95,17 +95,21 @@ You'll see this same "Device is online!" box however your device connected, over ![Online over Ethernet](assets/gettingstarted/01-11-online-ethernet.png) ![Online on an address it already had](assets/gettingstarted/01-12-online-existing-ip.png) -That's it, projectMM is installed and on your network. The link opens the device's own web interface, served straight from the ESP32. Let's look around. +That's it, MoonLight is installed and on your network. The link opens the device's own web interface, served straight from the ESP32. Let's look around. --- -## Chapter 2: Your projectMM interface +## Chapter 2: Your MoonLight interface Everything below runs **in your browser, live from the device**. There's no app, no account, no cloud, the ESP32 itself serves this page, and every change you make takes effect on the lights immediately. Open the link from step 6 and follow along; you can't break anything by exploring. +Here is the whole tour in half a minute, on a board that was flashed a moment earlier: + + + ### The layout: list, preview, controls -![The full projectMM interface](assets/gettingstarted/02-01-UI-large.png) +![The full MoonLight interface](assets/gettingstarted/02-01-UI-large.png) Three regions, left to right: @@ -138,6 +142,14 @@ Drag to rotate, scroll to zoom. Each dot is one light at its real position, lit > More on how the preview streams from the device: > [PreviewDriver](moonmodules/light/moxygen/PreviewDriver.md). +### Every card works the same way + +Learning one card teaches you all of them. Each carries the same five buttons in its corner, and the mode selector decides how many controls you see at all. + + + +⏻ turns a module off, ✎ swaps it for another type, × deletes it, ? opens that module's page in this documentation, and { } opens the card's own JSON, which is what to paste into an issue when something misbehaves. + ### The system modules The top of the list is your device's "about" section, read-outs and connection settings. You rarely need to touch these, but they're the first place to look if something seems off. @@ -162,7 +174,7 @@ USB cable needed once it's on your network. > [FirmwareUpdateModule](moonmodules/core/system.md#firmware-update) **Network**, your connection: WiFi or Ethernet, signal strength, and the -address others reach it at. The **Devices** section underneath finds other projectMM devices on the same network, so a roomful of them can discover each other. +address others reach it at. The **Devices** section underneath finds other MoonLight devices on the same network, so a roomful of them can discover each other. ![The Network module](assets/gettingstarted/02-07-UI-Network.png) @@ -174,28 +186,28 @@ address others reach it at. The **Devices** section underneath finds other proje > about LEDs. The light show below is one *domain* built on top of it; you could build > a different one and reuse all the same machinery. [FastLED-MM](https://github.com/MoonModules/FastLED-MM) > is an example, driving its LEDs with [FastLED](https://github.com/FastLED/FastLED) (on -> hold until projectMM ships as a reusable library). +> hold until MoonLight ships as a reusable library). ### Control it from your phone with WLED Native -The device's own web UI works on a phone, but for quick on/off and brightness from your pocket there's a nicer option: **WLED Native**, the open-source mobile app for the WLED ecosystem. projectMM speaks the WLED JSON API and announces itself over the network the same way a WLED device does, so the app finds your projectMM controllers automatically, no setup, no pairing. Each one shows up as a card with a power toggle and a brightness slider, so a roomful of controllers is a scroll and a tap away. +The device's own web UI works on a phone, but for quick on/off and brightness from your pocket there's a nicer option: **WLED Native**, the open-source mobile app for the WLED ecosystem. MoonLight speaks the WLED JSON API and announces itself over the network the same way a WLED device does, so the app finds your MoonLight controllers automatically, no setup, no pairing. Each one shows up as a card with a power toggle and a brightness slider, so a roomful of controllers is a scroll and a tap away. -![projectMM devices discovered in WLED Native](assets/core/WLED%20Native%20discovers%20projectMM.jpeg){ width="300" } +![MoonLight devices discovered in WLED Native](assets/core/WLED%20Native%20discovers%20projectMM.jpeg){ width="300" } Get it free for your phone: - **iPhone / iPad:** [WLED Native on the App Store](https://apps.apple.com/us/app/wled-native/id6446207239) - **Android:** [WLED Native on Google Play](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) -WLED Native is by **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**, who wrote both the [Android](https://github.com/Moustachauve/WLED-Android) and [iOS](https://github.com/Moustachauve/WLED-iOS) apps. Their open source is what let us work out exactly what those apps read, so a projectMM device appears in them without either side needing to know about the other. +WLED Native is by **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**, who wrote both the [Android](https://github.com/Moustachauve/WLED-Android) and [iOS](https://github.com/Moustachauve/WLED-iOS) apps. Their open source is what let us work out exactly what those apps read, so a MoonLight device appears in them without either side needing to know about the other. For the full picture and controls, the device's web interface is always there at `http://.local`, WLED Native is the fast everyday remote alongside it. ### Bring it into your smart home with Home Assistant -Want your lights in the same dashboard as the rest of your house, and in automations, voice assistants, and Apple Home? projectMM adopts into **Home Assistant** like any other light: point the device at your HA setup and it appears as a light entity with on/off and brightness, alongside a floor of other devices. +Want your lights in the same dashboard as the rest of your house, and in automations, voice assistants, and Apple Home? MoonLight adopts into **Home Assistant** like any other light: point the device at your HA setup and it appears as a light entity with on/off and brightness, alongside a floor of other devices. -![projectMM devices as lights in a Home Assistant dashboard](assets/core/ha-integration.png){ width="600" } +![MoonLight devices as lights in a Home Assistant dashboard](assets/core/ha-integration.png){ width="600" } There are two ways in, zeroconf (HA finds the device on its own) or MQTT auto-discovery (for a broker-only or cross-subnet setup), and from there you can bridge the entity into Apple Home too. The step-by-step, including installing HA and the MQTT broker if you don't have them, is in the [home automation guide](how-to/home-automation.md). @@ -256,12 +268,22 @@ That update runs from the app, because only the running app can write the partit --- +### The same thing on your computer + +MoonLight runs on macOS, Windows and Linux as well, with no board attached. The installer offers it from the same page: pick **This computer** instead of a USB port, and the Install button becomes a Download. + + + +What you get is the same interface, the same modules and the same effects. A computer has no LED pins, so it previews and sends the frame over the network instead of driving a strip, and it has the memory for a far larger grid than a board does. + + + +Useful for trying an idea before you wire anything, for driving a board over the network from a machine with real processing power, or for developing effects without a flash cycle. + ### Where to go next - **Understand the pipeline**: how layouts, layers, effects, modifiers and drivers fit together: [architecture overview](explanation/architecture/moonlight.md#the-pipeline). -- **Run it on your computer** instead of (or alongside) an ESP32, macOS, Windows, - Linux: [project overview → Getting started](../README.md#getting-started). - **Manage several devices, build, and flash from one console** with MoonDeck, our developer tool: [MoonDeck guide](../moondeck/MoonDeck.md). - **Build from source** or target Teensy / Raspberry Pi: [building.md](how-to/building.md). diff --git a/docs/how-to/building.md b/docs/how-to/building.md index 73f9162c..24b94b35 100644 --- a/docs/how-to/building.md +++ b/docs/how-to/building.md @@ -13,7 +13,7 @@ The scripts have two front ends with the same code and arguments: Use whichever fits. Neither path is "more official" than the other; the scripts are the source of truth and the front ends are interfaces. New work adds a script first; both interfaces follow. -**Why our own scripts, not PlatformIO:** the ESP32 build is ESP-IDF-native — projectMM tracks IDF pre-releases against a pinned commit for chips like the P4 and S31, a level of version control PlatformIO's packaged platforms don't offer, and the hot-path drivers (LCD_CAM, Parlio, GDMA) use the vendor APIs first-class rather than through an Arduino-core abstraction. The tooling surface is also far wider than compile-upload-monitor: desktop builds, unit and scenario runs, spec and boundary checks, KPI collection, the web installer, provisioning, multi-board bench orchestration. A wrapper toolchain would cover one of those tasks and still need all the scripts around it; one script per task, two front ends, keeps humans, agents, and CI on the identical path. +**Why our own scripts, not PlatformIO:** the ESP32 build is ESP-IDF-native — MoonLight tracks IDF pre-releases against a pinned commit for chips like the P4 and S31, a level of version control PlatformIO's packaged platforms don't offer, and the hot-path drivers (LCD_CAM, Parlio, GDMA) use the vendor APIs first-class rather than through an Arduino-core abstraction. The tooling surface is also far wider than compile-upload-monitor: desktop builds, unit and scenario runs, spec and boundary checks, KPI collection, the web installer, provisioning, multi-board bench orchestration. A wrapper toolchain would cover one of those tasks and still need all the scripts around it; one script per task, two front ends, keeps humans, agents, and CI on the identical path. MoonDeck has three tabs: @@ -68,7 +68,7 @@ Two things worth knowing: ### Packaging -`uv run moondeck/ci/package_desktop.py` builds and packages for the host it runs on: a `.dmg` with a `.app` on macOS, a `.tar.gz` plus a `.deb` on Linux, and a `.zip` plus an NSIS `-setup.exe` on Windows. The Windows installer puts the program in `%LOCALAPPDATA%\Programs\projectMM` with a Start-menu shortcut and an uninstaller; it needs no elevation, and it never touches the settings directory, so an upgrade keeps the user's configuration. +`uv run moondeck/ci/package_desktop.py` builds and packages for the host it runs on: a `.dmg` with a `.app` on macOS, a `.tar.gz` plus a `.deb` on Linux, and a `.zip` plus an NSIS `-setup.exe` on Windows. The Windows installer puts the program in `%LOCALAPPDATA%\Programs\MoonLight` with a Start-menu shortcut and an uninstaller; it needs no elevation, and it never touches the settings directory, so an upgrade keeps the user's configuration. Both the Windows icon and the macOS `.icns` derive from `mooninstaller/favicon.png`, so the mark has one source. The `.ico` is generated during the CMake build (`moondeck/ci/make_ico.py`, which pulls Pillow on demand through uv) and embedded in the executable, so the binary carries its icon whether it was installed or just unzipped. @@ -115,8 +115,8 @@ volume, so an upgrade keeps settings, presets, scripts and the device's identity | | | |---|---| -| **Config** | `/data/projectMM/.config/` in the volume, `XDG_DATA_HOME=/data` | -| **Identity** | `/data/projectMM/.config/identity`, generated on first run | +| **Config** | `/data/MoonLight/.config/` in the volume, `XDG_DATA_HOME=/data` | +| **Identity** | `/data/MoonLight/.config/identity`, generated on first run | | **Logs** | stdout, so `docker logs` | | **UI** | container port 8080; the compose file publishes it on 8081 so it never fights a native install | | **Output** | Art-Net UDP 6454, DDP 4048, E1.31 5568, all outbound | @@ -187,7 +187,7 @@ The ESP32 tab in MoonDeck wraps the same steps as cards (Setup → Firmware → ### Windows: USB-serial drivers -Windows ships no drivers for the two USB-serial chips almost every ESP32 dev board uses (WCH CH340/CH341, Silicon Labs CP2102/CP2102N). macOS and Linux do — so a board that Just Works on your Mac may show up on Windows as an `Unknown` device with no `COM*` port allocated at all, in which case both MoonDeck's port dropdown and the web installer's Chrome Web Serial picker come up **empty**. This isn't a projectMM bug; it's the OS. +Windows ships no drivers for the two USB-serial chips almost every ESP32 dev board uses (WCH CH340/CH341, Silicon Labs CP2102/CP2102N). macOS and Linux do — so a board that Just Works on your Mac may show up on Windows as an `Unknown` device with no `COM*` port allocated at all, in which case both MoonDeck's port dropdown and the web installer's Chrome Web Serial picker come up **empty**. This isn't a MoonLight bug; it's the OS. **How to tell what you're dealing with:** diff --git a/docs/how-to/control-surface.md b/docs/how-to/control-surface.md index 944efd94..c27e7bb7 100644 --- a/docs/how-to/control-surface.md +++ b/docs/how-to/control-surface.md @@ -1,28 +1,28 @@ -# Driving projectMM from a phone or tablet +# Driving MoonLight from a phone or tablet Eight switches, eight knobs and eight faders on a touchscreen, moving the device in real time and following it when something else moves it. It takes about five minutes to go from nothing to a working surface, using a free app and one file. -> New here? Start with **[Install & first light](../gettingstarted.md)**. What follows assumes projectMM is running and you can find it in a browser. +> New here? Start with **[Install & first light](../gettingstarted.md)**. What follows assumes MoonLight is running and you can find it in a browser. --- ## The short version 1. Install **[Open Stage Control](https://openstagecontrol.ammd.net/)** (free; macOS, Windows, Linux) -2. Download **[projectMM-control-surface.json](https://github.com/MoonModules/projectMM/releases/download/latest/projectMM-control-surface.json)** +2. Download **[MoonLight-control-surface.json](https://github.com/MoonModules/projectMM/releases/download/latest/MoonLight-control-surface.json)** 3. In its launcher set `send` to `:9000`, `osc-port` to `9001`, and `load` to the file 4. On the device: **Services → OSC**, turn on `listen` and `feedback` 5. Press start -The faders move the device; moving something in the projectMM UI moves the faders back. +The faders move the device; moving something in the MoonLight UI moves the faders back. --- ## 1. What this gives you -projectMM's Control card is a surface: a row of switches, a row of encoders, a row of faders, each of which can drive something on the device. The web UI shows it, but a mouse can only touch one control at a time. +MoonLight's Control card is a surface: a row of switches, a row of encoders, a row of faders, each of which can drive something on the device. The web UI shows it, but a mouse can only touch one control at a time. -A **control surface** is that same row of controls on something you can put your hands on. Open Stage Control is a free app that draws one on any screen, including a phone or tablet browser, and speaks **OSC**, the protocol projectMM listens for. +A **control surface** is that same row of controls on something you can put your hands on. Open Stage Control is a free app that draws one on any screen, including a phone or tablet browser, and speaks **OSC**, the protocol MoonLight listens for. Two things make this worth the five minutes: @@ -37,7 +37,7 @@ Today `switch1` drives the master on/off and `fader1` drives the global brightne The surface sends to an address, so you need the one your device is on. -It is in the projectMM UI on the **System** card, and it is the same address you typed into the browser to get there. On a desktop install talking to itself, it is `127.0.0.1`. +It is in the MoonLight UI on the **System** card, and it is the same address you typed into the browser to get there. On a desktop install talking to itself, it is `127.0.0.1`. Write it down; it goes in step 5. @@ -55,7 +55,7 @@ Download it from **[openstagecontrol.ammd.net](https://openstagecontrol.ammd.net A **session** is the layout: which knobs exist, what they look like, and what each one sends. You do not have to build one. -**[Download projectMM-control-surface.json](https://github.com/MoonModules/projectMM/releases/download/latest/projectMM-control-surface.json)** +**[Download MoonLight-control-surface.json](https://github.com/MoonModules/projectMM/releases/download/latest/MoonLight-control-surface.json)** That link always serves the newest session, built from the latest code, and it sits beside the firmware on the [releases page](https://github.com/MoonModules/projectMM/releases) if you would rather find it there. @@ -69,7 +69,7 @@ Open Stage Control opens a **launcher** first, a settings window, before it draw | Field | Value | What it means | |---|---|---| -| `send` | `:9000` | where the surface sends. `9000` is the port projectMM listens on | +| `send` | `:9000` | where the surface sends. `9000` is the port MoonLight listens on | | `osc-port` | `9001` | where the surface LISTENS, so the device can answer | | `load` | the file from step 4 | the layout to draw | @@ -85,7 +85,7 @@ Press the start button. The surface appears. ## 6. Turn the device's side on -In projectMM: **Services → OSC**. +In MoonLight: **Services → OSC**. | Control | Set to | Why | |---|---|---| @@ -112,7 +112,7 @@ http://:8080 Same surface, on a touchscreen, with ten fingers instead of one pointer. The computer running Open Stage Control stays the middleman; the phone talks to it, and it talks to the device. -> If projectMM's own UI is on port 8080 on that same machine, give Open Stage Control a different port in its launcher, or the two collide. +> If MoonLight's own UI is on port 8080 on that same machine, give Open Stage Control a different port in its launcher, or the two collide. --- @@ -180,4 +180,4 @@ The session also draws a **pad grid**. Those pads are inert for now: `/mm/pad/N` - **[OSC module reference](../moonmodules/core/services.md)**: every control, the feedback rules, `/mm/hello` - **[Control card](../moonmodules/core/system.md#control)**: the surface the device owns, and what each control drives -- **[Control surfaces](../reference/hardware/control-surfaces.md)**: what it would take to drive projectMM from a Mackie desk or a MIDI controller +- **[Control surfaces](../reference/hardware/control-surfaces.md)**: what it would take to drive MoonLight from a Mackie desk or a MIDI controller diff --git a/docs/how-to/home-automation.md b/docs/how-to/home-automation.md index 0081d123..7b07116a 100644 --- a/docs/how-to/home-automation.md +++ b/docs/how-to/home-automation.md @@ -1,11 +1,11 @@ # Home automation -Bring a projectMM device into your smart home — controlled alongside your lights, scenes, and automations — using the [MQTT module](../moonmodules/core/system.md#mqtt) (or, for some hubs, the built-in WLED compatibility). The device exposes on/off, brightness, and color; a home-automation platform adopts it and drives those from its own app, voice assistant, and automations. +Bring a MoonLight device into your smart home — controlled alongside your lights, scenes, and automations — using the [MQTT module](../moonmodules/core/system.md#mqtt) (or, for some hubs, the built-in WLED compatibility). The device exposes on/off, brightness, and color; a home-automation platform adopts it and drives those from its own app, voice assistant, and automations. Integration goes **both directions**, and this page covers both: -- **Your smart home controlling the device** — a hub (Home Assistant, Homebridge, …) adopts the projectMM device and drives its on/off, brightness, and color from its own app, voice assistant, and automations. -- **The device controlling smart-home lights** — projectMM drives **Philips Hue** bulbs as effect pixels, so your existing smart bulbs become part of a show. +- **Your smart home controlling the device** — a hub (Home Assistant, Homebridge, …) adopts the MoonLight device and drives its on/off, brightness, and color from its own app, voice assistant, and automations. +- **The device controlling smart-home lights** — MoonLight drives **Philips Hue** bulbs as effect pixels, so your existing smart bulbs become part of a show. The device-side recipes below assume the hub and (where relevant) an MQTT broker are already running. If any of that infrastructure isn't there yet, jump to [Set up the infrastructure](#set-up-the-infrastructure) at the end for HA-with-Mosquitto and standalone-Homebridge walkthroughs, then come back. @@ -18,8 +18,8 @@ The control surface these integrations drive — the MQTT controls, topics, and ## Prerequisites -- A projectMM device on your WiFi/Ethernet. For the MQTT-based paths (HA MQTT-discovery, Homebridge) the [MQTT module](../moonmodules/core/system.md#mqtt) must be present (it ships on boards whose catalog entry includes it; the Shelly model does); for the WLED-only HA path it isn't needed — HA reaches the device over its always-present `/json` endpoint. -- The device's **MAC suffix** — the last 6 hex of its MAC, which is its stable topic id on MQTT (`projectMM/`) and identifies the device to HA on the WLED path too. Read it from the MQTT module's `mqtt_status`, or once the device is talking to the broker, `mosquitto_sub -t 'projectMM/#'`. The examples below use `563cfe`; substitute yours. +- A MoonLight device on your WiFi/Ethernet. For the MQTT-based paths (HA MQTT-discovery, Homebridge) the [MQTT module](../moonmodules/core/system.md#mqtt) must be present (it ships on boards whose catalog entry includes it; the Shelly model does); for the WLED-only HA path it isn't needed — HA reaches the device over its always-present `/json` endpoint. +- The device's **MAC suffix** — the last 6 hex of its MAC, which is its stable topic id on MQTT (`MoonLight/`) and identifies the device to HA on the WLED path too. Read it from the MQTT module's `mqtt_status`, or once the device is talking to the broker, `mosquitto_sub -t 'MoonLight/#'`. The examples below use `563cfe`; substitute yours. - A running hub — with per-path infra: - **HA + Mosquitto broker add-on** — for MQTT auto-discovery (richer state + HomeKit Bridge). [Set up HA + Mosquitto](#set-up-home-assistant-mosquitto). - **HA alone (no broker)** — for the WLED-integration path. Same HA install, just skip the Mosquitto add-on. @@ -27,14 +27,14 @@ The control surface these integrations drive — the MQTT controls, topics, and ## Adopt in Home Assistant -projectMM devices as 💫-marked lights in a Home Assistant dashboard +MoonLight devices as 💫-marked lights in a Home Assistant dashboard -HA discovers a projectMM device automatically over the WLED path — no broker, nothing to configure — and lists it as a light with color, palette, and brightness. The 💫 in each name marks it as projectMM among any plain WLED devices. An optional step brings that entity into Apple Home via HA's own HomeKit Bridge — no Homebridge needed. Turn the `haDiscovery` control on only for the MQTT path (broker-only or cross-subnet setups); see [Let HA auto-create the entity](#2-let-ha-auto-create-the-entity) below. +HA discovers a MoonLight device automatically over the WLED path — no broker, nothing to configure — and lists it as a light with color, palette, and brightness. The 💫 in each name marks it as MoonLight among any plain WLED devices. An optional step brings that entity into Apple Home via HA's own HomeKit Bridge — no Homebridge needed. Turn the `haDiscovery` control on only for the MQTT path (broker-only or cross-subnet setups); see [Let HA auto-create the entity](#2-let-ha-auto-create-the-entity) below. The chain: ``` -Apple Home ──HAP──▶ HA (HomeKit Bridge) ──MQTT──▶ Mosquitto ◀──MQTT── projectMM device +Apple Home ──HAP──▶ HA (HomeKit Bridge) ──MQTT──▶ Mosquitto ◀──MQTT── MoonLight device └── HA app / voice / automations ``` @@ -47,16 +47,16 @@ The default WLED path needs none of this — skip to step 2. Do step 1 **only if - `username` / `password` — the HA user you created for MQTT (a plain HA user with local-network access; see the [broker add-on setup](#install-the-mosquitto-broker-add-on) if you haven't). - `haDiscovery` — turn **on** (it's off by default). This is what makes HA auto-create the MQTT entity in the next step; leave it off if you're on the WLED path. -`mqtt_status` turns to `connected` when it reaches the broker. Confirm from HA with the built-in **MQTT integration → Configure → Listen to topic → `projectMM/#`** — the device's `.../on/get`, `.../brightness/get`, `.../hsv/get`, and retained `.../name` appear. +`mqtt_status` turns to `connected` when it reaches the broker. Confirm from HA with the built-in **MQTT integration → Configure → Listen to topic → `MoonLight/#`** — the device's `.../on/get`, `.../brightness/get`, `.../hsv/get`, and retained `.../name` appear. ### 2. Let HA auto-create the entity Nothing to configure — HA discovers the device automatically over the WLED path. Two paths exist; the WLED one is the default and the richer of the two, so a device appears in HA **once** out of the box: -- **WLED integration** (the default, no MQTT at all): HA's built-in WLED integration discovers the device over zeroconf and reads the WLED-compatible `/json` API projectMM already serves — **color, palette, brightness, and diagnostic sensors**, no broker. This is the recommended path. +- **WLED integration** (the default, no MQTT at all): HA's built-in WLED integration discovers the device over zeroconf and reads the WLED-compatible `/json` API MoonLight already serves — **color, palette, brightness, and diagnostic sensors**, no broker. This is the recommended path. - **MQTT auto-discovery** (opt-in, uses the Mosquitto add-on): turn `haDiscovery` on for a broker-only or cross-subnet setup where zeroconf can't reach HA. The device then also announces itself on the retained `homeassistant/light/…/config` topic, and HA **auto-creates a light entity** named after the device with **on/off + brightness** (the discovery config declares `brightness` only, so this entity has no color control); it greys out when the device drops offline. Color/palette control stays on the separate `hsv/set` topic — not the auto-created entity. -**Why is MQTT discovery off by default?** Because the WLED path alone already gives HA a richer light (color, palette, sensors) with zero infrastructure, and running both lists the device **twice** (one WLED entity, one MQTT entity). So WLED is the default and MQTT discovery is opt-in, reserved for what WLED's zeroconf can't do: reach across VLANs/guest nets or a broker-only network, sit next to other MQTT devices (Tasmota, ESPHome, Zigbee2MQTT) under one convention, or push state with retained-across-offline latency. On a flat LAN with only projectMM devices, WLED alone is the whole story; a segmented network or an existing MQTT estate is where turning `haDiscovery` on earns its slot. The two never announce the same device to the same hub by default, so HA never double-lists it, and toggling `haDiscovery` re-announces or retracts live: an empty retained config removes the MQTT entity, with no reconnect. A device that wants both cards can still have them, deliberately. +**Why is MQTT discovery off by default?** Because the WLED path alone already gives HA a richer light (color, palette, sensors) with zero infrastructure, and running both lists the device **twice** (one WLED entity, one MQTT entity). So WLED is the default and MQTT discovery is opt-in, reserved for what WLED's zeroconf can't do: reach across VLANs/guest nets or a broker-only network, sit next to other MQTT devices (Tasmota, ESPHome, Zigbee2MQTT) under one convention, or push state with retained-across-offline latency. On a flat LAN with only MoonLight devices, WLED alone is the whole story; a segmented network or an existing MQTT estate is where turning `haDiscovery` on earns its slot. The two never announce the same device to the same hub by default, so HA never double-lists it, and toggling `haDiscovery` re-announces or retracts live: an empty retained config removes the MQTT entity, with no reconnect. A device that wants both cards can still have them, deliberately. The MQTT control surface (topics, HSV → palette mapping, retained state) is in [Core › System › MQTT](../moonmodules/core/system.md#mqtt); nothing here restates it. @@ -64,14 +64,14 @@ The MQTT control surface (topics, HSV → palette mapping, retained state) is in ### 3. Optional: expose to Apple Home via HomeKit Bridge -HA can act as its own HomeKit bridge. **Settings → Devices & Services → Add Integration → HomeKit Bridge**, pick **Include selected entities**, and tick the projectMM light entity created above. HA prints a QR code and PIN; in the iOS **Home** app, **Add Accessory → More options → HA HomeKit Bridge** and scan / enter the PIN. The light shows up as a HomeKit accessory driven end-to-end through HA — same outcome as the standalone Homebridge recipe, one fewer service to run. +HA can act as its own HomeKit bridge. **Settings → Devices & Services → Add Integration → HomeKit Bridge**, pick **Include selected entities**, and tick the MoonLight light entity created above. HA prints a QR code and PIN; in the iOS **Home** app, **Add Accessory → More options → HA HomeKit Bridge** and scan / enter the PIN. The light shows up as a HomeKit accessory driven end-to-end through HA — same outcome as the standalone Homebridge recipe, one fewer service to run. ## Adopt in Homebridge Standalone Apple-Home-only path, for when HA isn't wanted. Assumes Homebridge + a Mosquitto broker are already running (both together on a Pi is the usual permanent setup; a Mac or PC works for a quick test — see [Set up standalone Homebridge + Mosquitto](#set-up-standalone-homebridge-mosquitto) below if not). Two steps: **point the device at the broker**, then **add one accessory to Homebridge**. ``` -Apple Home ──HAP──▶ Homebridge ──MQTT──▶ Mosquitto ◀──MQTT── projectMM device +Apple Home ──HAP──▶ Homebridge ──MQTT──▶ Mosquitto ◀──MQTT── MoonLight device ``` ### 1. Point the device at the broker @@ -82,7 +82,7 @@ In the device's web UI, open the **MQTT** module and set: - `port` — usually `1883`. - `username` / `password` — only if your broker requires auth. -`mqtt_status` turns to `connected` once it reaches the broker, and `mosquitto_sub -t 'projectMM/#' -v` shows the device's `.../on/get`, `.../brightness/get`, `.../hsv/get`, and retained `.../name`. +`mqtt_status` turns to `connected` once it reaches the broker, and `mosquitto_sub -t 'MoonLight/#' -v` shows the device's `.../on/get`, `.../brightness/get`, `.../hsv/get`, and retained `.../name`. ### 2. Add the accessory to Homebridge @@ -100,7 +100,7 @@ Once the broker, device, and Homebridge are all talking (the `mosquitto_sub` win ## Drive Hue lights -The other direction: instead of a hub controlling the device, the **device controls your Philips Hue bulbs**, treating each color bulb as a pixel of an effect. Your existing smart lights join the show — an effect's colors glide across them alongside (or instead of) an LED strip. This is a projectMM **output driver**, not a hub integration, so there's no broker and no Homebridge — the device talks straight to the Hue bridge over its LAN HTTP API. +The other direction: instead of a hub controlling the device, the **device controls your Philips Hue bulbs**, treating each color bulb as a pixel of an effect. Your existing smart lights join the show — an effect's colors glide across them alongside (or instead of) an LED strip. This is a MoonLight **output driver**, not a hub integration, so there's no broker and no Homebridge — the device talks straight to the Hue bridge over its LAN HTTP API. Because Hue is a rate-limited HTTP hub (~10 commands/s), this is **smooth ambient color**, not fast strobing: the driver paces itself to the bridge and lets it fade between colors. Full behavior, controls, and the wire contract are in the driver reference: [Drivers › Hue](../moonmodules/light/drivers.md#hue). @@ -147,7 +147,7 @@ Windows 10/11 **Pro** or **Enterprise** ships Hyper-V; the Home editions don't. 4. **Create the VM.** **Hyper-V Manager → New → Virtual Machine**. Name it `HomeAssistant`; **Generation 2**; assign at least **2 GB RAM** (2 GB is fine for a home LAN, 4 GB is comfortable); attach the network to the external switch you just made; **Use an existing virtual hard disk** and point at the unzipped `.vhdx`; finish. 5. **Disable Secure Boot.** Right-click the new VM → **Settings → Security** → untick **Enable Secure Boot** (HAOS's shim isn't in the Microsoft template). 6. **Start the VM.** It boots to a text console — wait ~2 minutes for the first-run "Preparing Home Assistant" to finish. When the console shows `homeassistant login:`, open a browser on the host at **[http://homeassistant.local:8123](http://homeassistant.local:8123)** (or the LAN IP the console prints) and complete the onboarding wizard. -7. **Keep the host awake and the VM auto-managed.** An always-on HA host must not sleep — an S3 or hibernate suspend pauses Hyper-V, and every projectMM device / HA entity flips to "unavailable" until it wakes. In an admin PowerShell: +7. **Keep the host awake and the VM auto-managed.** An always-on HA host must not sleep — an S3 or hibernate suspend pauses Hyper-V, and every MoonLight device / HA entity flips to "unavailable" until it wakes. In an admin PowerShell: ```powershell # what sleep states this host actually supports (tells you what to disable) @@ -230,7 +230,7 @@ For an Apple-Home-only setup without HA. A **Raspberry Pi** is the natural perma Wherever the broker runs, the device reaches it over the LAN, so the broker must listen on all interfaces, not just loopback — noted per platform below. Confirm the broker sees the device at any point with: ``` -mosquitto_sub -t 'projectMM/#' -v +mosquitto_sub -t 'MoonLight/#' -v ``` #### Raspberry Pi @@ -297,7 +297,7 @@ Now jump back to [Adopt in Homebridge](#adopt-in-homebridge). - **HA entities flap "unavailable" every few minutes** — the VM's network is dropping. Almost always the WiFi-bridge symptom noted in the HA install section: the HA VM is on a WiFi-bridged NIC that re-associates periodically. Move the HA VM to a wired external switch (Hyper-V / ProxMox / ESXi all support this), or run HAOS on a wired Pi. This is also what a battery of Zigbee / Wi-Fi routers with a bad channel plan looks like — worth checking the HA host's own connectivity first. - **Every Zigbee entity is `unavailable` and Zigbee2MQTT will not start**: work down the chain instead of restarting things. If `binary_sensor.zigbee2mqtt_bridge_connection_state` reads **`off`** rather than `unavailable`, MQTT is healthy and Home Assistant is receiving Zigbee2MQTT's own offline message, so neither the broker nor the integration is at fault. On a network coordinator (see [reach a USB Zigbee coordinator from the VM](#optional-reach-a-usb-zigbee-coordinator-from-the-vm)), read the bridge's log next: a connection logged as `to-serial 16 B / from-serial 0 B` means the bridge faithfully wrote Zigbee2MQTT's init and the coordinator answered nothing. That is a hung coordinator, not a software fault, and restarting the add-on, the bridge, or Windows will not clear it. **Unplug the coordinator, wait ten seconds, plug it back in, then restart the bridge service** with `Restart-Service ZigbeeBridge -Force` from an elevated PowerShell. That last step is not optional: a bridge that opens its serial port once at startup is still holding a handle to the device that was unplugged, and it will keep failing every connection until it is restarted. **Then restart the Zigbee2MQTT add-on as well.** While the bridge was unreachable the add-on will have failed its own adapter initialization and dropped into an error state, and its watchdog does not reliably bring it back, so it sits there stopped even once the bridge is healthy again. Turning off USB selective suspend is what stops the whole sequence recurring. - **Devices report briefly after each Zigbee2MQTT restart, then go quiet, and commands never arrive**: a different fault from the one above, and the one most often misread as a software problem. The coordinator powers up, starts its network, and then loses power again a short while later, typically once its radio begins transmitting. It stays enumerated as a serial port the whole time and still answers simple queries, so nothing in the operating system, the bridge or Zigbee2MQTT reports an error. The only visible symptom is that incoming reports stop and every outgoing command fails. **Move the coordinator to a different USB socket, or onto a powered hub that supplies its own current, before considering a re-flash.** Moving it counts as an unplug, so restart the bridge service and then the add-on afterwards, exactly as above, or the bridge keeps serving a handle to the socket the coordinator just left and nothing appears to improve. A socket whose power delivery has degraded can no longer hold a transmitting radio up, and that is far more common than a corrupted coordinator. If the devices then keep their pairings and the mesh reforms on its own, nothing was ever corrupted, which is also the sign that re-flashing would have been wasted effort. -- **HA created two light entities with the same slug (`light._`)** — a stale MQTT-discovery config is still retained on the broker. Delete the retained topic (`mosquitto_pub -h -t 'homeassistant/light/projectMM_/config' -r -n`) and let the device republish; HA cleans up the entity within a few seconds. +- **HA created two light entities with the same slug (`light._`)** — a stale MQTT-discovery config is still retained on the broker. Delete the retained topic (`mosquitto_pub -h -t 'homeassistant/light/MoonLight_/config' -r -n`) and let the device republish; HA cleans up the entity within a few seconds. - **HA WLED integration fails with "Cannot connect", or the entity shows partial state** — HA reads two endpoints, both on the device's HTTP port (default **80** on ESP32, **8080** on desktop builds), so probe both: ```bash @@ -306,5 +306,5 @@ Now jump back to [Adopt in Homebridge](#adopt-in-homebridge). ``` A **404 on `/json`** means an old firmware that predates the WLED-compatibility shim → reflash. A **404 on `/presets.json`** with `/json` working means the presets route is missing → HA's coordinator retry-storms trying to fetch presets and the entity ends up stuck on "unavailable" even though the light responds; also a firmware reflash. A `/json` response that parses but is missing `info.fs`, `state.nl`, `state.udpn`, or `state.lor` fails python-wled's dataclass parse and HA reports HTTP-500 on `light.turn_on` — again a firmware version older than the current WLED shim. -- **Homebridge shows "No Response"** — the accessory's topics don't match the device's MAC suffix, or the `url` points at the wrong broker. Confirm the suffix with `mosquitto_sub -t 'projectMM/#'` and that the same broker appears in both the device's `broker` control and the accessory `url`. +- **Homebridge shows "No Response"** — the accessory's topics don't match the device's MAC suffix, or the `url` points at the wrong broker. Confirm the suffix with `mosquitto_sub -t 'MoonLight/#'` and that the same broker appears in both the device's `broker` control and the accessory `url`. - **HomeKit color wheel doesn't match a specific color.** Expected: HomeKit sends a full-precision hue, and the device snaps it to the *nearest* built-in palette (there's no arbitrary-color mode). Same behavior whether the bridge is HA's HomeKit Bridge or standalone Homebridge. See the palette note in the [MQTT reference](../moonmodules/core/system.md#mqtt). diff --git a/docs/how-to/installing-on-linux.md b/docs/how-to/installing-on-linux.md index e2ef89dc..00f2c23d 100644 --- a/docs/how-to/installing-on-linux.md +++ b/docs/how-to/installing-on-linux.md @@ -1,10 +1,10 @@ -# Running projectMM on a Linux machine +# Running MoonLight on a Linux machine -projectMM runs as an ordinary Linux application: the same effect pipeline, web UI and network drivers as on a board, with a real CPU behind them. A small always-on machine makes a good installation controller, whether a server, a Raspberry Pi or a NanoPi. +MoonLight runs as an ordinary Linux application: the same effect pipeline, web UI and network drivers as on a board, with a real CPU behind them. A small always-on machine makes a good installation controller, whether a server, a Raspberry Pi or a NanoPi. Deploying is covered here. Building and developing on Linux is in [building.md](../how-to/building.md). -> Windows, with screenshots: [Installing projectMM on a desktop](installing-to-desktop.md). Flashing a board: [Install & first light](../gettingstarted.md). +> Windows, with screenshots: [Installing MoonLight on a desktop](installing-to-desktop.md). Flashing a board: [Install & first light](../gettingstarted.md). ## Which route applies to your machine @@ -19,7 +19,7 @@ uname -m | `x86_64` | Intel or AMD PC, server or VM | Install the package | | `aarch64` | arm64 board: Pi, NanoPi, most SBCs | Install the package | -Both architectures get a released binary, so the route below is the same one and only the filename differs. Building from source is still there for a distribution the package does not suit, and for developing. A Pi 4 or 5 has ample headroom. A NanoPi R28S has two Gigabit ports, so it can sit between the house network and the lighting network, and 1 GB of RAM, which runs projectMM comfortably. Everything here assumes a Debian-based system (Debian, Ubuntu, Raspberry Pi OS, Armbian); on another distribution, translate the package names. +Both architectures get a released binary, so the route below is the same one and only the filename differs. Building from source is still there for a distribution the package does not suit, and for developing. A Pi 4 or 5 has ample headroom. A NanoPi R28S has two Gigabit ports, so it can sit between the house network and the lighting network, and 1 GB of RAM, which runs MoonLight comfortably. Everything here assumes a Debian-based system (Debian, Ubuntu, Raspberry Pi OS, Armbian); on another distribution, translate the package names. > `x64` and `amd64` are two names for the same thing. `arm64` is different machine code. @@ -31,7 +31,7 @@ On a machine with a browser, download it and install: ```sh sudo apt install ./projectmm_X.Y.Z_arm64.deb -projectMM +MoonLight ``` On a headless board, fetch it over ssh instead. This picks the right file for the architecture it runs on, so the same two lines work on a Pi, a NanoPi and a server: @@ -41,7 +41,7 @@ arch=$(dpkg --print-architecture) url=$(curl -fsSL https://api.github.com/repos/MoonModules/projectMM/releases/tags/latest \ | grep -o "https://[^\"]*_${arch}\.deb" | head -1) curl -fsSL -o projectmm.deb "$url" && sudo apt install -y ./projectmm.deb -projectMM +MoonLight ``` `latest` is the rolling build from `main`, which is what the web installer offers too. For the newest tagged release, replace `tags/latest` with `latest` in that URL. @@ -131,7 +131,7 @@ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc ```sh git clone https://github.com/MoonModules/projectMM.git -cd projectMM +cd MoonLight uv run moondeck/build/build_desktop.py uv run moondeck/run/run_desktop.py ``` @@ -150,12 +150,12 @@ Give it a systemd unit at `/etc/systemd/system/projectmm.service`: ```ini [Unit] -Description=projectMM +Description=MoonLight After=network-online.target Wants=network-online.target [Service] -ExecStart=/home/pi/projectMM/build/projectMM +ExecStart=/home/pi/MoonLight/build/MoonLight Restart=always RestartSec=5 User=pi @@ -169,7 +169,7 @@ sudo systemctl enable --now projectmm systemctl status projectmm ``` -`Restart=always` covers a crash as well as a reboot. Adjust `User` and the path: `/usr/bin/projectMM` for the package, or where you built for a source build. +`Restart=always` covers a crash as well as a reboot. Adjust `User` and the path: `/usr/bin/MoonLight` for the package, or where you built for a source build. ## Shutting down @@ -179,7 +179,7 @@ Shut down cleanly; an SD card interrupted mid-write can corrupt the filesystem: sudo shutdown now # or: sudo reboot ``` -projectMM writes to disk only when settings change, so the card is a fine home for it. The risk is the operating system's own writes. +MoonLight writes to disk only when settings change, so the card is a fine home for it. The risk is the operating system's own writes. ## Docker @@ -244,5 +244,5 @@ The realistic limits are architecture and memory, not the kind of device. It nee ## Where to go next - [Install & first light](../gettingstarted.md): the same program on an ESP32. -- [How projectMM works](../tutorials/how-projectmm-works.md): layouts, layers, effects and drivers. +- [How MoonLight works](../tutorials/how-projectmm-works.md): layouts, layers, effects and drivers. - [building.md](../how-to/building.md): building, testing and packaging in depth. diff --git a/docs/how-to/installing-to-desktop.md b/docs/how-to/installing-to-desktop.md index a2a53e4b..ea16fc94 100644 --- a/docs/how-to/installing-to-desktop.md +++ b/docs/how-to/installing-to-desktop.md @@ -1,15 +1,15 @@ -# Installing projectMM on a desktop +# Installing MoonLight on a desktop -projectMM does not need an ESP32. The same code runs as an ordinary application on your computer, rendering effects, serving the web UI, and driving Art-Net, DMX and LED panel cards over the network. It is the quickest way to see projectMM working, and on a real PC the effects have far more compute behind them than any microcontroller can offer. +MoonLight does not need an ESP32. The same code runs as an ordinary application on your computer, rendering effects, serving the web UI, and driving Art-Net, DMX and LED panel cards over the network. It is the quickest way to see MoonLight working, and on a real PC the effects have far more compute behind them than any microcontroller can offer. The steps below cover **Windows**. macOS and Linux differ only in the download and the first run: -- **macOS**: open the `.dmg` and drag projectMM to Applications. The build is ad-hoc signed rather than notarized, so Gatekeeper says it cannot verify the developer: right-click the app and choose **Open** to accept it once, or clear the flag with `xattr -dr com.apple.quarantine /Applications/projectMM.app`. +- **macOS**: open the `.dmg` and drag MoonLight to Applications. The build is ad-hoc signed rather than notarized, so Gatekeeper says it cannot verify the developer: right-click the app and choose **Open** to accept it once, or clear the flag with `xattr -dr com.apple.quarantine /Applications/MoonLight.app`. - **Linux**: unpack the `.tar.gz` and run the binary, or install the `.deb` on Debian, Ubuntu and Raspberry Pi OS with `sudo apt install ./projectmm_X.Y.Z_amd64.deb`, which puts it on your PATH. Both then open `http://localhost:8080/`, and §5 onward applies unchanged. -> Looking to flash a device instead? That is [Install & first light](../gettingstarted.md). This page is about running projectMM *on the computer itself*. +> Looking to flash a device instead? That is [Install & first light](../gettingstarted.md). This page is about running MoonLight *on the computer itself*. Five steps, one of which is Windows asking whether you trust an unsigned application. That is not a fault, and it is covered below. @@ -27,7 +27,7 @@ Your browser will most likely flag it straight away: *"isn't commonly downloaded ## 2. Tell the browser to keep it -Microsoft Defender SmartScreen judges a download by its **reputation**, built from how many people have downloaded that exact file from a publisher it recognizes. projectMM is not code-signed, and every build produces a brand-new file, so its reputation is always zero. The warning is about the certificate, not about the contents. +Microsoft Defender SmartScreen judges a download by its **reputation**, built from how many people have downloaded that exact file from a publisher it recognizes. MoonLight is not code-signed, and every build produces a brand-new file, so its reputation is always zero. The warning is about the certificate, not about the contents. In the Downloads panel, click the **`⋯`** next to the file, then open the **Delete** dropdown and choose **Keep anyway**: @@ -41,7 +41,7 @@ Worth knowing so it does not surprise you later: **this happens for every new bu Occasionally Defender goes a step further and removes the file outright, naming something like `Trojan:Win32/Wacatac.C!ml`. The `!ml` suffix means a **machine-learning guess**, not a match against known malware: an unsigned, zero-reputation executable that opens audio capture devices (the Audio module records from your microphone or a loopback device) fits a pattern the model weighs, and every new build is a brand-new fingerprint for it to judge. The contents are checkable rather than a matter of trust: the binary is compiled from this repository's source in public CI, and the one vendored file behind the audio support (`miniaudio.h`) is byte-identical to its upstream release. -**If the download itself fails**, which shows as *"Couldn't download - Download error"*, Defender is stopping it mid-transfer and there is no file to rescue. Re-downloading only repeats it. **Use the zip instead** (§8): it is a different file with a different fingerprint, so the verdict on the setup does not apply to it, and it carries a script that installs projectMM exactly as the setup would. +**If the download itself fails**, which shows as *"Couldn't download - Download error"*, Defender is stopping it mid-transfer and there is no file to rescue. Re-downloading only repeats it. **Use the zip instead** (§8): it is a different file with a different fingerprint, so the verdict on the setup does not apply to it, and it carries a script that installs MoonLight exactly as the setup would. **If the file did land and was then quarantined**, delete it (**Windows Security → Virus & threat protection → Protection history**) and download again from the [releases page](https://github.com/MoonModules/projectMM/releases) over HTTPS, which re-establishes what you are running rather than trusting a file out of quarantine. Restoring from Protection history works too, but only for a file you downloaded yourself moments before. An exclusion on the install directory would not help, since the flagged file is in **Downloads**, and a broad Downloads exclusion costs more protection than it is worth. @@ -51,31 +51,31 @@ Either way the durable route is reporting it as a false positive at [microsoft.c Double-click the file you just kept, and the installer opens straight away. Keeping it in step 2 was the trust decision, so Windows does not ask a second time: -![The projectMM setup dialog, showing the install location under AppData Local Programs](../assets/tutorials/windows-03-setup.png) +![The MoonLight setup dialog, showing the install location under AppData Local Programs](../assets/tutorials/windows-03-setup.png) -There is nothing to decide here. It installs **for your user only**, into `%LOCALAPPDATA%\Programs\projectMM`, which is why it never asks for an administrator password. It needs about 1.3 MB. Click **Install**. +There is nothing to decide here. It installs **for your user only**, into `%LOCALAPPDATA%\Programs\MoonLight`, which is why it never asks for an administrator password. It needs about 1.3 MB. Click **Install**. -If a copy of projectMM is already running, the installer stops it before replacing the files. That is deliberate: a running copy holds a lock on its own executable, and the install would otherwise fail part-way. +If a copy of MoonLight is already running, the installer stops it before replacing the files. That is deliberate: a running copy holds a lock on its own executable, and the install would otherwise fail part-way. ## 4. Start it -projectMM is now in the Start menu with its own icon. Type `projectMM` and open it: +MoonLight is now in the Start menu with its own icon. Type `MoonLight` and open it: -![The Windows Start menu showing projectMM with its icon, and an Uninstall projectMM entry](../assets/tutorials/windows-04-start-menu.png) +![The Windows Start menu showing MoonLight with its icon, and an Uninstall MoonLight entry](../assets/tutorials/windows-04-start-menu.png) -The **Uninstall projectMM** entry beside it is the clean way to remove it later, and it leaves your settings in place (§6). +The **Uninstall MoonLight** entry beside it is the clean way to remove it later, and it leaves your settings in place (§6). ## 5. That is it -A console window opens showing what projectMM is doing, and your browser opens the interface at `http://localhost:8080/`. +A console window opens showing what MoonLight is doing, and your browser opens the interface at `http://localhost:8080/`. -![projectMM running: the web interface with a live 3D preview and the Effects panel](../assets/tutorials/windows-05-running.png) +![MoonLight running: the web interface with a live 3D preview and the Effects panel](../assets/tutorials/windows-05-running.png) -The console window **is** the application. It shows the log, and closing it stops projectMM. The line that matters on a first run is `projectMM is running: http://localhost:8080/`; the address printed just below it is the same interface, reachable from your phone or another machine on your network. +The console window **is** the application. It shows the log, and closing it stops MoonLight. The line that matters on a first run is `MoonLight is running: http://localhost:8080/`; the address printed just below it is the same interface, reachable from your phone or another machine on your network. -On a first install you get a default grid and a running effect, enough to confirm everything works. The screenshot above is not a first install: that machine already had projectMM configured with a Game of Life layer, and the setup left it exactly as it was. That is §6. +On a first install you get a default grid and a running effect, enough to confirm everything works. The screenshot above is not a first install: that machine already had MoonLight configured with a Game of Life layer, and the setup left it exactly as it was. That is §6. -From here, [How projectMM works](../tutorials/how-projectmm-works.md) explains the Layouts, Effects and Drivers down the left-hand side. +From here, [How MoonLight works](../tutorials/how-projectmm-works.md) explains the Layouts, Effects and Drivers down the left-hand side. Two options worth knowing: `--no-browser` stops it opening a browser (for a headless machine), and `--port ` serves somewhere other than 8080. @@ -87,7 +87,7 @@ Everything you change is saved automatically, in a folder that belongs to **your %LOCALAPPDATA%\projectMM ``` -Note that this is *not* where the program went. The program sits under `Programs\projectMM`; your settings live beside it in a separate folder, and that separation is what makes upgrades safe: +Note that this is *not* where the program went. The program sits under `Programs\MoonLight`; your settings live beside it in a separate folder, and that separation is what makes upgrades safe: - **Installing a new version keeps your settings.** The installer replaces the program and never touches the settings folder. - **Uninstalling keeps them too.** Delete `%LOCALAPPDATA%\projectMM` by hand if you want a genuinely clean slate. @@ -96,7 +96,7 @@ Paste `%LOCALAPPDATA%\projectMM` into the Explorer address bar to open it. ## 7. Updating -You do not have to watch the releases page. When a newer release exists, projectMM lights an **⬆ badge** in its top bar, next to the device name. You can see it in the screenshot in §5. +You do not have to watch the releases page. When a newer release exists, MoonLight lights an **⬆ badge** in its top bar, next to the device name. You can see it in the screenshot in §5. On a desktop the badge opens the **release page**, not the Firmware card. A device flashed over the network can install a new firmware in place; a desktop cannot replace its own running executable, so updating means downloading the new setup and running it. That is steps 1 to 4 of this page again, and it takes about a minute. @@ -112,13 +112,13 @@ It holds the same application plus three files, so it serves two purposes: **Run it in place.** Double-click `projectMM.exe` wherever you extracted it. No Start-menu entry, no uninstaller, nothing written outside your settings folder. This is the one to take on a USB stick. -**Or install it properly.** Double-click **`Install-projectMM.cmd`**. It does exactly what the setup does: copies the program to `%LOCALAPPDATA%\Programs\projectMM`, adds the Start-menu entry with its icon, registers an uninstaller in Add/Remove Programs, and stops a running copy first so it can replace a locked executable. No administrator rights, because everything stays under your own user profile. +**Or install it properly.** Double-click **`Install-MoonLight.cmd`**. It does exactly what the setup does: copies the program to `%LOCALAPPDATA%\Programs\MoonLight`, adds the Start-menu entry with its icon, registers an uninstaller in Add/Remove Programs, and stops a running copy first so it can replace a locked executable. No administrator rights, because everything stays under your own user profile. -The `.cmd` is a three-line wrapper around `Install-projectMM.ps1`, which is where the work happens and which you can read first. **Right-clicking that `.ps1` and choosing "Run with PowerShell" does not work**, and it is worth knowing why rather than being surprised by it: Windows marks every file extracted from a downloaded zip as internet-sourced, and PowerShell's default policy refuses to run an unsigned script carrying that mark. The wrapper exists solely to get past that, and the only thing it adds is permission for its own single invocation. +The `.cmd` is a three-line wrapper around `Install-MoonLight.ps1`, which is where the work happens and which you can read first. **Right-clicking that `.ps1` and choosing "Run with PowerShell" does not work**, and it is worth knowing why rather than being surprised by it: Windows marks every file extracted from a downloaded zip as internet-sourced, and PowerShell's default policy refuses to run an unsigned script carrying that mark. The wrapper exists solely to get past that, and the only thing it adds is permission for its own single invocation. Two reasons this route exists rather than being redundant with the setup. The script is **plain text you can read before you run it**, which an installer cannot offer. And when Defender blocks the setup download outright (§2), it is the way through: the flagged thing is the compressed installer, not the application, so the zip is unaffected. -Settings live in the same per-user folder whichever route you take, so the three are interchangeable and share one configuration. `Uninstall-projectMM.ps1` reverses the install and leaves your settings alone. +Settings live in the same per-user folder whichever route you take, so the three are interchangeable and share one configuration. `Uninstall-MoonLight.ps1` reverses the install and leaves your settings alone. --- @@ -129,15 +129,15 @@ Settings live in the same per-user folder whichever route you take, so the three | The download is flagged and will not open | §2. The choice hides behind the **`⋯`** and the **Delete** dropdown, which is easy to miss. | | A blue **"Windows protected your PC"** appears when you run it | Only happens if the file reached you without the step-2 prompt, so the trust question waits until you run it instead. Click **More info**, then **Run anyway**; the button is hidden until you click **More info**. | | It opened and closed immediately | Run it from a terminal so the error stays on screen instead of vanishing with the window. | -| The browser shows nothing at `localhost:8080` | Check the console window is still open. Closing it stops projectMM. If another program already uses port 8080, start with `--port 8081`. | +| The browser shows nothing at `localhost:8080` | Check the console window is still open. Closing it stops MoonLight. If another program already uses port 8080, start with `--port 8081`. | | Another machine cannot reach it | Use the `HTTP server ->` address from the log, not `localhost`. Windows Firewall prompts on first run; it needs to be allowed on your private network. | | Settings do not survive a restart | The log will say `cannot use ... persistence disabled` and name the directory it tried. That is the fault, not the saving itself. | -| The installer fails saying a file is in use | A copy of projectMM is running that it could not stop. Close the console window and run the setup again. | +| The installer fails saying a file is in use | A copy of MoonLight is running that it could not stop. Close the console window and run the setup again. | --- ## Where to go next -- **[How projectMM works](../tutorials/how-projectmm-works.md)**: the interface, and the Layouts / Effects / Drivers model. +- **[How MoonLight works](../tutorials/how-projectmm-works.md)**: the interface, and the Layouts / Effects / Drivers model. - **[Driving LED panels with a receiving card](panel-cards.md)**: turn this desktop into the sending card for an LED wall. - **[Install & first light](../gettingstarted.md)**: flashing an ESP32, if you want the same thing on a device. diff --git a/docs/how-to/panel-cards.md b/docs/how-to/panel-cards.md index fbc02209..e63708e8 100644 --- a/docs/how-to/panel-cards.md +++ b/docs/how-to/panel-cards.md @@ -1,14 +1,14 @@ # Driving LED panels with a receiving card -You bought a panel receiving card, most likely a **ColorLight** one, which is the family projectMM supports today. This page takes you from a box of parts to a lit wall, on an ESP32 or from a desktop. +You bought a panel receiving card, most likely a **ColorLight** one, which is the family MoonLight supports today. This page takes you from a box of parts to a lit wall, on an ESP32 or from a desktop. -> New here? Start with **[Install & first light](../gettingstarted.md)**, then **[How projectMM works](../tutorials/how-projectmm-works.md)**. What follows assumes you can find a card and change a control. +> New here? Start with **[Install & first light](../gettingstarted.md)**, then **[How MoonLight works](../tutorials/how-projectmm-works.md)**. What follows assumes you can find a card and change a control. --- ## The short version -If you have the card wired and projectMM running, this is the whole sequence. Every step has its own section below. +If you have the card wired and MoonLight running, this is the whole sequence. Every step has its own section below. 1. **Layouts** → describe your wall ([§5.4](#54-describe-the-wall)) 2. **Effects** → pick an effect, so there is something to send @@ -27,7 +27,7 @@ The status line on the PanelCard card tells you where you are: it should read a An LED wall is normally driven by two boxes. A **sending card** (a PCI-E board in a PC, or a standalone unit) takes video in and puts a specialised signal on Ethernet. A **receiving card** sits in each cabinet, decodes that signal, and drives the panels over HUB75 ribbon cables. The card you bought is a receiving card. -**projectMM takes the sending card's place.** The board renders the effect and emits the frames itself, so there is no PC in the installation and no sending card to buy. +**MoonLight takes the sending card's place.** The board renders the effect and emits the frames itself, so there is no PC in the installation and no sending card to buy. That has one consequence worth understanding before you wire anything: these frames are **raw Ethernet**, below IP. No address, no port, no DHCP. The cable between your controller and the card is not a network in the usual sense; it is a private link carrying pixel data, and nothing else should be on it. @@ -40,12 +40,12 @@ Supported: **ColorLight 5A-75** (5A-75B and 5A-75E use the same wire format). | Part | Notes | |---|---| | ColorLight 5A-75 receiving card | The one this guide is about | -| HUB75 panels | Any size; you tell projectMM the geometry later | +| HUB75 panels | Any size; you tell MoonLight the geometry later | | 5 V power supply | Sized from the panels' own rating: see the note on power in [§5.3](#53-wire-it) | | Cat5e or Cat6 cable | Controller → the card's **input** port | | A controller | An ESP32 board ([§5](#5-on-an-esp32)) or a desktop/Pi ([§6](#6-from-a-desktop)) | | A gigabit switch | Only if your controller is a **P4 or an S3**, whose Ethernet is 100 Mbit: the switch lets the card negotiate a gigabit link on its own side and re-times the frames toward it. It does **not** make the controller faster: the 100 Mbit leg and its wire time ([§2](#the-one-hardware-fact-that-decides-everything)) remain, so a large wall still wants an S31. An **S31 is gigabit already** and connects straight to the card. | -| A USB gigabit Ethernet dongle | Recommended on Windows. Not for projectMM, which drives these cards fine from a built-in port, but for **LEDUpgrade**: if it cannot find the card through your built-in adapter, a dongle is the known way through. See [§7](#7-card-firmware-and-the-flicker). | +| A USB gigabit Ethernet dongle | Recommended on Windows. Not for MoonLight, which drives these cards fine from a built-in port, but for **LEDUpgrade**: if it cannot find the card through your built-in adapter, a dongle is the known way through. See [§7](#7-card-firmware-and-the-flicker). | ### The one hardware fact that decides everything @@ -53,7 +53,7 @@ Supported: **ColorLight 5A-75** (5A-75B and 5A-75E use the same wire format). The cards have no buffering and no flow control. They latch the image when the sync frame arrives, so an entire frame has to land inside the gap between frames. At gigabit a 256×256 frame is ~1.6 ms on the wire; at 100 Mbit the identical bytes take ~16 ms, which overruns the budget and breaks the timing the latch depends on. -The failure mode is the confusing part: **nothing errors**. The link is up, frames go out, and the panels tear, show wrong rows, or never latch. That is why projectMM reads the *negotiated* speed and warns you, rather than letting a slow link look like a format bug. It still sends, since a small wall on 100 Mbit is often fine, but if your picture is unstable, check this first. +The failure mode is the confusing part: **nothing errors**. The link is up, frames go out, and the panels tear, show wrong rows, or never latch. That is why MoonLight reads the *negotiated* speed and warns you, rather than letting a slow link look like a format bug. It still sends, since a small wall on 100 Mbit is often fine, but if your picture is unstable, check this first. The cards can also be picky about negotiating a gigabit link with a 100 Mbit controller. A gigabit switch in between is the remedy: the card negotiates gigabit with the switch, the switch buffers, and the controller's slower link stops being the card's problem. This applies to the **P4 and the S3**. An **S31 is gigabit on its own** and connects directly. @@ -76,14 +76,14 @@ The two halves are independent: an ESP32 and a desktop drive the same card the s ## 4. Set the panels up in LED Vision -The receiving card has to know what it is driving before projectMM sends it anything: how big each panel is, how many there are, and which driver IC they use. That configuration lives **on the card**, written once with ColorLight's own **[LEDVision](https://en.colorlightinside.com/product/download/380)**, and it is why projectMM itself needs no panel wiring settings at all (see [§5.4](#54-describe-the-wall)). +The receiving card has to know what it is driving before MoonLight sends it anything: how big each panel is, how many there are, and which driver IC they use. That configuration lives **on the card**, written once with ColorLight's own **[LEDVision](https://en.colorlightinside.com/product/download/380)**, and it is why MoonLight itself needs no panel wiring settings at all (see [§5.4](#54-describe-the-wall)). **Which version.** An **8.x** build is what people running these cards in this scene actually use: this project's own wall is set up with **8.8**, and the walkthrough linked below uses **8.5**. Newer releases exist, and whether they are equally suitable here has not been established, so the safe advice is to take an 8.x build and only move if you have a reason to. > **Worth watching first:** [Setting up a Colorlight Card with FPP v6.3 and LED Vision 8.5](https://www.youtube.com/watch?v=L4lHbwUszAs) > walks through the whole card-and-panel setup on video. It drives the card from FPP rather than -> projectMM, but everything up to the sender is the same job, and seeing it done is worth more than +> MoonLight, but everything up to the sender is the same job, and seeing it done is worth more than > any written step list. The steps below cover the same ground in short form. > The steps below are written from how LEDVision generally works, not from a verified run on this @@ -97,11 +97,11 @@ this project's own wall is set up with **8.8**, and the walkthrough linked below `.rcfgx` / `.rcvx` file the panel supplier provided, which is the reliable route for a panel that is not a well-known model. 4. Set the **cabinet** size: how many pixels one card drives, across and down. 5. Set the **panel arrangement**: how the HUB75 ribbons chain, and which physical panel is first. - This is the step that makes the card, not projectMM, responsible for panel order. + This is the step that makes the card, not MoonLight, responsible for panel order. 6. **Send to the receiving card**, then **Save** so the configuration survives a power cycle. Saving is a separate action from sending in most versions, and skipping it is the usual reason a wall comes back wrong after being unplugged. -When this is right, a test pattern from LEDVision fills the wall correctly. Get to that point before introducing projectMM: it separates "the panels are wired and configured" from "the sender works". +When this is right, a test pattern from LEDVision fills the wall correctly. Get to that point before introducing MoonLight: it separates "the panels are wired and configured" from "the sender works". --- @@ -152,11 +152,11 @@ WiFi is what serves the web interface. Leave WiFi configured as normal; it is un The driver has **no geometry controls**. The wall's shape lives in the Layout, once, so that everything else (effects, modifiers, the preview) sees the same picture. **A plain Grid is usually all you need.** Two 128x64 panels stacked is a 128x128 grid, and that is -the whole configuration. The reason it is that simple is worth knowing: the driver reads only the wall's width and height and sends the image row by row. Which physical panel a row lands on, and in what order the HUB75 ribbons chain, was already settled on the card in [§4](#4-set-the-panels-up-in-led-vision). The card owns panel arrangement; projectMM owns the picture. +the whole configuration. The reason it is that simple is worth knowing: the driver reads only the wall's width and height and sends the image row by row. Which physical panel a row lands on, and in what order the HUB75 ribbons chain, was already settled on the card in [§4](#4-set-the-panels-up-in-led-vision). The card owns panel arrangement; MoonLight owns the picture. -That is also why this needs none of the physical detail you may have filled in elsewhere. An output page that asks for scan rate, address lines and chain order is describing panels driven *directly*, where the software has to generate the HUB75 timing itself. Through a receiving card, none of that is the sender's business: the card generates the timing, and the sender hands it an image. That holds for any sender, [FPP](https://github.com/FalconChristmas/fpp) included, which reaches these cards over Ethernet exactly as projectMM does. +That is also why this needs none of the physical detail you may have filled in elsewhere. An output page that asks for scan rate, address lines and chain order is describing panels driven *directly*, where the software has to generate the HUB75 timing itself. Through a receiving card, none of that is the sender's business: the card generates the timing, and the sender hands it an image. That holds for any sender, [FPP](https://github.com/FalconChristmas/fpp) included, which reaches these cards over Ethernet exactly as MoonLight does. -**When you need the Panels layout instead.** It exists for walls where projectMM, not a card, owns +**When you need the Panels layout instead.** It exists for walls where MoonLight, not a card, owns the ordering: addressable panels wired as one long pixel strip, where the strip snakes from panel to panel and the layout has to undo that. Its controls are about **wiring order**, which a HUB75 ribbon does not have. | Control | Meaning | @@ -201,21 +201,21 @@ A PC, a Mac or a Raspberry Pi can drive the same card. Reasons to want this: far The steps are the same as [§5](#5-on-an-esp32), same Layout and same driver, with **two differences**: - **`interface` matters.** A desktop has several NICs and the frames must leave the right one. Which spelling to use is per-OS, below. -- **Raw Ethernet needs permission.** Sending below IP is privileged on every desktop OS. Without it, projectMM does not fail silently: the driver warns and *records* frames instead of sending them, which is also how the tests run with no hardware. +- **Raw Ethernet needs permission.** Sending below IP is privileged on every desktop OS. Without it, MoonLight does not fail silently: the driver warns and *records* frames instead of sending them, which is also how the tests run with no hardware. Pick your OS. ### 6.1 Windows: needs Npcap -Windows has **no** built-in way for an application to put a raw Ethernet frame on the wire. That is an OS restriction, not a projectMM limitation, and it is why Wireshark bundles a driver and why ColorLight's own LEDVision needs one. +Windows has **no** built-in way for an application to put a raw Ethernet frame on the wire. That is an OS restriction, not a MoonLight limitation, and it is why Wireshark bundles a driver and why ColorLight's own LEDVision needs one. **Install [Npcap](https://npcap.com/)** (free; it is also installed if you already have Wireshark). Legacy WinPcap 4.1.3 also works, and is what this driver was developed and measured against; Npcap offers the same API and is the maintained choice on a new machine. -projectMM loads it *at run time*, so the application installs and runs fine without either; you simply cannot bind an interface until one is present, and the driver says so. +MoonLight loads it *at run time*, so the application installs and runs fine without either; you simply cannot bind an interface until one is present, and the driver says so. -For `interface`, type **any distinctive part of the adapter's name**, case-insensitive: `Realtek`, `Intel`, `Ethernet`. Windows names its capture devices `\Device\NPF_{…GUID…}`, which is neither memorable nor short enough for the field, so projectMM matches your text against the adapter description instead. The full device name also works if you have it. +For `interface`, type **any distinctive part of the adapter's name**, case-insensitive: `Realtek`, `Intel`, `Ethernet`. Windows names its capture devices `\Device\NPF_{…GUID…}`, which is neither memorable nor short enough for the field, so MoonLight matches your text against the adapter description instead. The full device name also works if you have it. -> If binding fails with Npcap installed, re-run its installer and check whether *"Restrict Npcap driver's access to Administrators only"* was selected. If so, run projectMM as Administrator. +> If binding fails with Npcap installed, re-run its installer and check whether *"Restrict Npcap driver's access to Administrators only"* was selected. If so, run MoonLight as Administrator. ### 6.2 Linux, including Raspberry Pi @@ -224,7 +224,7 @@ Raw frames go out over `AF_PACKET`, which needs `CAP_NET_RAW`. Either run as root, or grant the capability once so it does not need root again: ```sh -sudo setcap cap_net_raw+ep ./projectMM +sudo setcap cap_net_raw+ep ./MoonLight ``` For `interface`, use the kernel's name exactly: `eth0`, `enp3s0`. `ip link` lists them. @@ -233,7 +233,7 @@ For `interface`, use the kernel's name exactly: `eth0`, `enp3s0`. `ip link` list Raw frames go out over BPF (`/dev/bpf*`), which is root-only by default. -Run projectMM with `sudo`, or install Wireshark's **ChmodBPF** helper, which grants your user access to the BPF devices at boot and is the tidier option if you do this regularly. +Run MoonLight with `sudo`, or install Wireshark's **ChmodBPF** helper, which grants your user access to the BPF devices at boot and is the tidier option if you do this regularly. For `interface`, use the BSD name exactly: `en0`, `en7`. `ifconfig` lists them. @@ -247,13 +247,13 @@ Set up the layout and the **Panel Card** driver exactly as in [§5.4](#54-descri ## 7. Card firmware, and the flicker -A card's firmware has a version of its own, separate from the hardware revision printed on the board. It matters twice: once because one generation is defective, and once because projectMM has to know which generation it is talking to. +A card's firmware has a version of its own, separate from the hardware revision printed on the board. It matters twice: once because one generation is defective, and once because MoonLight has to know which generation it is talking to. ### The v13 flicker -Cards running **firmware v13** on **v8.x hardware** flicker in time with network activity. This is a defect in the card, not in the sender: it shows up identically under projectMM, [FPP](https://github.com/FalconChristmas/fpp) and ColorLight's own LEDVision, and nothing about how the frames are sent avoids it. The fix is to put older firmware on the card. +Cards running **firmware v13** on **v8.x hardware** flicker in time with network activity. This is a defect in the card, not in the sender: it shows up identically under MoonLight, [FPP](https://github.com/FalconChristmas/fpp) and ColorLight's own LEDVision, and nothing about how the frames are sent avoids it. The fix is to put older firmware on the card. -To be clear about what is and is not wrong: projectMM drives a v13 card perfectly well. It binds, sends at the full frame rate, and the picture is correct. The flicker is the *only* reason to move off v13, and it is the card doing it. Since the trigger is network activity and driving a wall means constant network activity, there is no sending-side setting that avoids it. Batching, frame rate and packet count have all been tried; the defect is downstream of all of them. +To be clear about what is and is not wrong: MoonLight drives a v13 card perfectly well. It binds, sends at the full frame rate, and the picture is correct. The flicker is the *only* reason to move off v13, and it is the card doing it. Since the trigger is network activity and driving a wall means constant network activity, there is no sending-side setting that avoids it. Batching, frame rate and packet count have all been tried; the defect is downstream of all of them. Two different faults look like "flicker", and only one of them is this. Tell them apart before spending an evening on the wrong one: @@ -262,7 +262,7 @@ Two different faults look like "flicker", and only one of them is this. Tell the | Flicker follows the Ethernet activity LED, and is there even on a still, dim image | The v13 defect. Downgrade the card. | | Flicker grows with brightness and with how much of the wall is lit | Power. The panels draw more than the supply holds. A downgrade changes nothing. | -The second row is worth taking seriously, because projectMM sends a full frame every tick no matter what the effect is doing. The packet rate is identical for a black wall and a busy one, so flicker that tracks *content* is not coming from the network. +The second row is worth taking seriously, because MoonLight sends a full frame every tick no matter what the effect is doing. The packet rate is identical for a black wall and a busy one, so flicker that tracks *content* is not coming from the network. ### Reading and changing the version @@ -279,18 +279,18 @@ automatically safer: cards on 11.08 were reported strobing white, which 11.09 fi > something else in the stack, and Hyper-V's virtual switch is the usual culprit: it binds the > adapter, so a tool that needs raw layer-2 access reaches nothing even though the port looks > ordinary and your normal networking works. The reliable way through is a **USB gigabit Ethernet -> dongle**, which Hyper-V is not bridging. This is about the card-flashing step: projectMM's own +> dongle**, which Hyper-V is not bridging. This is about the card-flashing step: MoonLight's own > sending works from a built-in port. 1. Connect the card **directly** to the machine, no switch in between. -2. **Close everything else that talks to the card**, projectMM included. A card being streamed at will not answer, and two ColorLight tools at once (LEDVision and LEDUpgrade) interfere. +2. **Close everything else that talks to the card**, MoonLight included. A card being streamed at will not answer, and two ColorLight tools at once (LEDVision and LEDUpgrade) interfere. 3. `Send Mode` set to the network-card mode, then choose your adapter. Restart LEDUpgrade afterwards: it binds the adapter at startup. 4. **Detect Receiver Cards.** It reports something like `5A 13.17 (v8.0)`, meaning firmware 13.17 on v8.0 hardware. 5. **Readback Firmware** to back up what is on the card before you replace it. 6. `Upgrade Firmware`, preset, `4in1`, `normal`, then **`normal-11.09`**. Stay in the `normal` series: `PWM` and `shixin` are for different panel driver ICs. 7. **Power-cycle the card.** It goes on running the old firmware until you do, which is the step most often missed. -### Then tell projectMM what it is talking to +### Then tell MoonLight what it is talking to Set the driver's `firmware` control to match the card: diff --git a/docs/how-to/updating-firmware.md b/docs/how-to/updating-firmware.md index 72da9e31..c91f11b1 100644 --- a/docs/how-to/updating-firmware.md +++ b/docs/how-to/updating-firmware.md @@ -1,8 +1,8 @@ # Update the firmware -Install a newer projectMM on a device that is already running one. Over the network, from the device's own interface, with no cable. +Install a newer MoonLight on a device that is already running one. Over the network, from the device's own interface, with no cable. -Your settings survive: a firmware update replaces the program, not the configuration. The one thing it costs is a reboot, which is what makes it different from every other change in projectMM. +Your settings survive: a firmware update replaces the program, not the configuration. The one thing it costs is a reboot, which is what makes it different from every other change in MoonLight. ## The normal route diff --git a/docs/index.md b/docs/index.md index 46aede99..4d4fdd3a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,15 @@ -# projectMM +# MoonLight High-performance LED & DMX lighting control for ESP32 and beyond. -[:material-flash: Flash an ESP32 from your browser](/projectMM/install/){ .md-button .md-button--primary }   [:material-github: GitHub](https://github.com/MoonModules/projectMM){ .md-button } +[:material-flash: Flash an ESP32 from your browser](/MoonLight/install/){ .md-button .md-button--primary }   [:material-github: GitHub](https://github.com/MoonModules/projectMM){ .md-button } !!! tip "New here?" The [Getting started](gettingstarted.md) guide walks you from a blank ESP32 to your first running light show, step by step, with no build tools required. ## What it is -projectMM drives large LED installations and DMX fixtures. You build a light show by stacking simple blocks: a **layout** (how the LEDs are arranged), one or more **effects** (what they animate), **modifiers** (mirror, rotate, mask…), and a **driver** (how the pixels reach the hardware). Every setting takes effect live; there is no reboot to apply a change. +MoonLight drives large LED installations and DMX fixtures. You build a light show by stacking simple blocks: a **layout** (how the LEDs are arranged), one or more **effects** (what they animate), **modifiers** (mirror, rotate, mask…), and a **driver** (how the pixels reach the hardware). Every setting takes effect live; there is no reboot to apply a change. One source tree drives ESP32, Teensy, Raspberry Pi, macOS, Windows and Linux. @@ -21,7 +21,7 @@ One source tree drives ESP32, Teensy, Raspberry Pi, macOS, Windows and Linux. Flash a board from your browser and light your first pixels. - [Getting started](gettingstarted.md) · [Web installer](/projectMM/install/) + [Getting started](gettingstarted.md) · [Web installer](/MoonLight/install/) - :material-palette: **Build a show** diff --git a/docs/legal/privacy-policy.md b/docs/legal/privacy-policy.md index b097a97c..b0e550f6 100644 --- a/docs/legal/privacy-policy.md +++ b/docs/legal/privacy-policy.md @@ -2,15 +2,15 @@ **Last updated: 2026-09-10** -Covers the projectMM software (firmware, desktop application and the web interface they serve), the [web installer](https://moonmodules.org/projectMM/install/), and this documentation site. +Covers the MoonLight software (firmware, desktop application and the web interface they serve), the [web installer](https://moonmodules.org/projectMM/install/), and this documentation site. ## The rule -**projectMM sends nothing to us unless you switch it on.** Every feature that transmits anything is opt-in, off by default, and asks in plain words before its first transmission. Declining is one click, is remembered, and sends nothing at all: not even a record that you declined. Some things do reach the internet without you switching anything on, and they are listed below: they go to GitHub, never to a server of ours. +**MoonLight sends nothing to us unless you switch it on.** Every feature that transmits anything is opt-in, off by default, and asks in plain words before its first transmission. Declining is one click, is remembered, and sends nothing at all: not even a record that you declined. Some things do reach the internet without you switching anything on, and they are listed below: they go to GitHub, never to a server of ours. -Everything else stays on the machine you run it on: your layouts, effects, drivers, pin assignments, device name, and any credentials you entered. `%LOCALAPPDATA%\projectMM` on Windows, `~/Library/Application Support/projectMM` on macOS, `$XDG_DATA_HOME/projectMM` on Linux, the device's own flash on a board. Nothing there is uploaded, synchronized or backed up by us. +Everything else stays on the machine you run it on: your layouts, effects, drivers, pin assignments, device name, and any credentials you entered. `%LOCALAPPDATA%\MoonLight` on Windows, `~/Library/Application Support/MoonLight` on macOS, `$XDG_DATA_HOME/MoonLight` on Linux, the device's own flash on a board. Nothing there is uploaded, synchronized or backed up by us. -Whatever you switch on, projectMM does not transmit **your name, email or postal address, your Wi-Fi or MQTT credentials, your IP or MAC address, or the contents of files you made**. +Whatever you switch on, MoonLight does not transmit **your name, email or postal address, your Wi-Fi or MQTT credentials, your IP or MAC address, or the contents of files you made**. ## What reaches the internet before you opt in @@ -43,7 +43,7 @@ Whatever the feature, now or later, these rules hold. **You can turn it off again at any time.** Nothing further is sent, and what was already published stays published: a message on a public board cannot be recalled, and a report already counted cannot be withdrawn. So the honest advice is the simple one: leave it off if you are unsure, turn it on when you want to take part, and turn it off if you change your mind. -**MoonCloud** is the name for anything projectMM does with a server we run, and it is one self-contained part of the software rather than something woven through it. It sits on its own card, its members are the only code that talks to a server of ours, and nothing else in projectMM goes through it. So this is not a promise about scattered behavior you would have to take on trust: it is one place, and you can switch off what is in it. +**MoonCloud** is the name for anything MoonLight does with a server we run, and it is one self-contained part of the software rather than something woven through it. It sits on its own card, its members are the only code that talks to a server of ours, and nothing else in MoonLight goes through it. So this is not a promise about scattered behavior you would have to take on trust: it is one place, and you can switch off what is in it. Each member is a separate setting on that card, off by default, and each says on the card what it exchanges. Members are added over time; the rules above apply to every one of them, including any added after you read this. @@ -66,9 +66,9 @@ What follows from it being stable: Any server receiving a request sees the address it came from. Ours derives a country from it at the network edge and **never writes the address to storage**. What is stored is a country, never an address: beside the figures a report contributes, or beside the text of a message and the time it was sent. -## Systems you connect projectMM to +## Systems you connect MoonLight to -An MQTT broker, Home Assistant, Art-Net or E1.31 consoles: projectMM speaks to these when you configure it to, and those connections go where you point them. **Whatever you connect it to is governed by that service's own privacy policy**, which is worth knowing if you point it at a cloud-hosted broker. +An MQTT broker, Home Assistant, Art-Net or E1.31 consoles: MoonLight speaks to these when you configure it to, and those connections go where you point them. **Whatever you connect it to is governed by that service's own privacy policy**, which is worth knowing if you point it at a cloud-hosted broker. ## Changes to this policy @@ -78,4 +78,4 @@ Revisions are made in the open: the policy lives in the [project repository](htt ## Contact -Questions, or anything here you would like to verify: an [issue on the repository](https://github.com/MoonModules/projectMM/issues) or the [Discord](https://discord.gg/TC8NSUSCdV). projectMM is free and open-source software, and the network calls described above are the only ones in it. +Questions, or anything here you would like to verify: an [issue on the repository](https://github.com/MoonModules/projectMM/issues) or the [Discord](https://discord.gg/TC8NSUSCdV). MoonLight is free and open-source software, and the network calls described above are the only ones in it. diff --git a/docs/moonmodules/core/services.md b/docs/moonmodules/core/services.md index 46261672..da87670f 100644 --- a/docs/moonmodules/core/services.md +++ b/docs/moonmodules/core/services.md @@ -18,7 +18,7 @@ A user-added Service: the audio source the audio-reactive effects consume. `mode Audio module controls - + - `mode` — Local audio, Receive network or Simulate, each showing only its own controls below. - `micMode`: (Local, I²S targets) `I2S` for a three-wire part, `PDM` for a two-wire one. @@ -44,7 +44,7 @@ Detail: [technical](moxygen/AudioService.md) · [the sync packet](../light/moxyg OSC module controls: listen, port, status -Receives [OSC](https://opensoundcontrol.stanford.edu/) over UDP and writes it onto this device's controls, so a fader in Resolume, TouchDesigner or TouchOSC drives projectMM directly. It owns no surface of its own: everything lands in the same control writes the HTTP API and the UI use, so every validator still runs. Addresses, feedback and setup: ⌄ details. +Receives [OSC](https://opensoundcontrol.stanford.edu/) over UDP and writes it onto this device's controls, so a fader in Resolume, TouchDesigner or TouchOSC drives MoonLight directly. It owns no surface of its own: everything lands in the same control writes the HTTP API and the UI use, so every validator still runs. Addresses, feedback and setup: ⌄ details. - `listen` — receive OSC (default **off**). This opens an unauthenticated UDP port that writes controls, on the same LAN-trust basis as the Art-Net and audio-sync receivers, so it is a capability you turn on rather than one every device carries. @@ -160,13 +160,13 @@ A desktop device is picked by list position, so re-pick if the OS reorders them; Sending and receiving both use the **multicast address 239.0.0.1**, which is what WLED's own usermod does (`beginMulticast` on both ends). It never uses broadcast, so a broadcast sender is inaudible to WLED and a receiver that only binds the port never hears WLED. This is a network-layer address, unrelated to any device grouping. **Port 11988 is the WLED contract**, and `syncPort` defaults to it. The port is configurable for -projectMM peers that want a private stream, but a custom port is no longer WLED-compatible: the endpoint WLED speaks is 239.0.0.1:11988 specifically. +MoonLight peers that want a private stream, but a custom port is no longer WLED-compatible: the endpoint WLED speaks is 239.0.0.1:11988 specifically. Multicast is also the better neighbor, with a caveat worth knowing: a switch or access point that does **IGMP snooping** forwards the group only to the ports that joined it, so the other hosts never see the traffic at all. Without snooping the switch floods it exactly like broadcast, and on WiFi it goes out at the lowest basic rate to every station. So multicast can reduce how many hosts have to process ~40 packets a second, but it does not guarantee it. See [multicast and IGMP snooping](../../explanation/architecture/moonlight.md#multicast-and-igmp-snooping). The 44-byte v2 packet is byte-compatible with WLED, with one field that is not yet equivalent: -| field | projectMM | WLED | status | +| field | MoonLight | WLED | status | |---|---|---|---| | `sampleRaw` / `sampleSmth` | level / smoothed level | same | compatible | | `samplePeak` | latched beat, 80 ms refractory | same rule | compatible | @@ -176,9 +176,9 @@ The 44-byte v2 packet is byte-compatible with WLED, with one field that is not y | `FFT_Magnitude` | 0..255 internally, x16 on the wire | ~0..4096 | compatible | **The magnitude scale differs, so it is converted at the wire.** WLED sends the raw magnitude of -its FFT's dominant bin, scaled so that "the end result is linear and ~4096 max" (its own comment where it divides the input samples by 16). Its effects then divide that by 4, 8 or 16 depending on the effect and treat the result as a byte, which is why their thresholds read `< 48` squelch and `> 144` full brightness. projectMM byte-scales the peak magnitude to 0..255 instead, through the same noise floor and gain conditioning as the 16 bands, so one pair of knobs governs the whole spectrum. +its FFT's dominant bin, scaled so that "the end result is linear and ~4096 max" (its own comment where it divides the input samples by 16). Its effects then divide that by 4, 8 or 16 depending on the effect and treat the result as a byte, which is why their thresholds read `< 48` squelch and `> 144` full brightness. MoonLight byte-scales the peak magnitude to 0..255 instead, through the same noise floor and gain conditioning as the 16 bands, so one pair of knobs governs the whole spectrum. -projectMM keeps its own units internally and multiplies by 16 on send, dividing by 16 on receive. +MoonLight keeps its own units internally and multiplies by 16 on send, dividing by 16 on receive. The factor is exact rather than a fudge: it is the divisor WLED's effects apply, so our full-scale 255 arrives as 4080, right on WLED's own ~4096 design target, and every effect's thresholds land where they were tuned to. Adopting WLED's range internally was the alternative, and was rejected because that range is an artifact of FFT size and input scaling rather than a specification (WLED's own fallback path admits "no idea if 10000 is a good value"), and importing it would cost the property that one floor/gain pair conditions every value the service publishes, in exchange for resolution the receiving effects discard anyway when they divide back down to a byte. A received magnitude is clamped to 255, since a real WLED source reaches ~9500 and an unclamped value would drive effects harder than locally analyzed audio ever could. @@ -193,7 +193,7 @@ A client learns the current state three ways: when it first writes to us from a The shipped session has a `sync from device` button for exactly this. **Setting one up**, from installing the app to using it from a phone, is its own page: -[Driving projectMM from a phone or tablet](../../how-to/control-surface.md). It needs no checkout and no tooling, just the app and the session file from the latest release. +[Driving MoonLight from a phone or tablet](../../how-to/control-surface.md). It needs no checkout and no tooling, just the app and the session file from the latest release. **Addresses.** These are a public contract: a TouchOSC layout built against them keeps working, so they stay small and boring. @@ -210,7 +210,7 @@ Both argument forms are accepted because controllers disagree: apps send a float Send one from the bench with `uv run moondeck/check/send_osc.py /mm/fader/1 0.75`. -Origin: projectMM original +Origin: MoonLight original **One command to a working surface** (with the repo checked out). Install [Open Stage Control](https://openstagecontrol.ammd.net/) (free, macOS / Windows / Linux), then: @@ -228,17 +228,17 @@ It runs **headless**: a web server rather than a desktop window. That is deliber |---|---| | `--host` / `--port` | the device and its OSC `port` (default `127.0.0.1:9000`) | | `--listen` | where we receive feedback, the device's `feedbackPort` (default 9001) | -| `--ui-port` | the surface's web UI (default 8088; 8080 is projectMM's own) | +| `--ui-port` | the surface's web UI (default 8088; 8080 is MoonLight's own) | | `--app` | the Open Stage Control binary, when it is not on PATH or in the usual place | | `--gui` | also open the desktop window; by default it is the server alone | The launcher looks on PATH first, then in each platform's default install location. **Windows and Linux are untested**: the paths are the ones those installers use, but only macOS has been run. If it cannot find the app, `--app` takes the full path and that always works. **A ready-made control surface.** A session of the switches, encoders and faders ships as a release -asset (`projectMM-control-surface.json`) and lives in the repo at [`docs/reference/examples/open-stage-control.json`](../../reference/examples/open-stage-control.json). +asset (`MoonLight-control-surface.json`) and lives in the repo at [`docs/reference/examples/open-stage-control.json`](../../reference/examples/open-stage-control.json). Editing the layout needs `read-only` off in the launcher. -The shipped Open Stage Control session beside projectMM's own Control card: eight switches, eight encoders and eight faders in both +The shipped Open Stage Control session beside MoonLight's own Control card: eight switches, eight encoders and eight faders in both Driving the device from that session, beside the Control card it mirrors. diff --git a/docs/moonmodules/core/system.md b/docs/moonmodules/core/system.md index b804e01a..e9de1bb5 100644 --- a/docs/moonmodules/core/system.md +++ b/docs/moonmodules/core/system.md @@ -47,7 +47,7 @@ Detail: [technical](moxygen/NetworkModule.md) ### Improv provisioning -Serial/BLE Improv Wi-Fi provisioning: the web installer hands credentials to a fresh device over this protocol during the flash-and-connect flow. [Improv Wi-Fi](https://github.com/improv-wifi) is an open standard, and its [sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js) are the specification this implements, so any Improv-capable installer can provision a projectMM device. +Serial/BLE Improv Wi-Fi provisioning: the web installer hands credentials to a fresh device over this protocol during the flash-and-connect flow. [Improv Wi-Fi](https://github.com/improv-wifi) is an open standard, and its [sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js) are the specification this implements, so any Improv-capable installer can provision a MoonLight device. Improv provisioning module controls @@ -59,14 +59,14 @@ Detail: [technical](moxygen/ImprovProvisioningModule.md) · [frame format](moxyg ### Devices -Discovers other projectMM devices on the LAN and lists them, persisting the last-known list across a reboot. A wired-by-code child of Network. +Discovers other MoonLight devices on the LAN and lists them, persisting the last-known list across a reboot. A wired-by-code child of Network. Devices module, discovered LAN devices - `devices`: a List of discovered devices; each row expands to a detail panel. Persistable. - `wledCompatible`: also announce on WLED's broadcast address, off by default. -WLED apps browse on broadcast, so a device appears in them only with this on. Off is the better neighbour, since a broadcast wakes every device on the LAN to parse a packet none of them want. Presence always goes to the projectMM group regardless, so peers find each other either way. See [multicast and IGMP snooping](../../explanation/architecture/moonlight.md#multicast-and-igmp-snooping). +WLED apps browse on broadcast, so a device appears in them only with this on. Off is the better neighbour, since a broadcast wakes every device on the LAN to parse a packet none of them want. Presence always goes to the MoonLight group regardless, so peers find each other either way. See [multicast and IGMP snooping](../../explanation/architecture/moonlight.md#multicast-and-igmp-snooping). Detail: [technical](moxygen/DevicesModule.md) @@ -114,7 +114,7 @@ Detail: [technical](moxygen/FirmwareUpdateModule.md) · [image vetting](moxygen/ ### MoonCloud -The container for everything projectMM does with a server MoonModules runs. It holds no settings of its own: each thing it does is a child with its own consent, because a user who wants one has not thereby agreed to the other. +The container for everything MoonLight does with a server MoonModules runs. It holds no settings of its own: each thing it does is a child with its own consent, because a user who wants one has not thereby agreed to the other. MoonCloud module card @@ -148,7 +148,7 @@ Detail: [technical](moxygen/MoonStatsModule.md) ### Talk -A public message board between projectMM devices, in the shape Meshtastic's channel chat has. Off until you turn it on, and a message is sent because you typed one: nothing posts on its own. +A public message board between MoonLight devices, in the shape Meshtastic's channel chat has. Off until you turn it on, and a message is sent because you typed one: nothing posts on its own. Talk module controls @@ -347,41 +347,41 @@ sudo uv run moondeck/run/run_desktop.py --port 80 The discovery buffers are sized to the looks this device actually has, and grow or shrink as presets are added and removed. There is no cap on the number: a fixed one would either reserve memory a small setup never uses, or silently publish nothing once the list outgrew it. ## MQTT, details -The topic prefix is `projectMM/`, a **stable** identifier (the last 6 hex of the device's MAC), fixed for the device's life. Renaming the device does **not** change its topics, so a hub's config never breaks on a rename (the WLED/Tasmota/Home-Assistant convention). It's derived, not a stored control. +The topic prefix is `MoonLight/`, a **stable** identifier (the last 6 hex of the device's MAC), fixed for the device's life. Renaming the device does **not** change its topics, so a hub's config never breaks on a rename (the WLED/Tasmota/Home-Assistant convention). It's derived, not a stored control. **Topics** (for a device whose MAC ends `563cfe`): the device SUBSCRIBEs to the `set` topics and PUBLISHes the `get` topics on change (and on connect, so a controller never reads "No Response"). It also publishes its friendly `deviceName` on the retained `name` topic, so a hub can show the human name while the topics stay MAC-stable: | direction | topic | payload | |---|---|---| -| set → device | `projectMM/563cfe/on/set` | `true` / `false` | -| device → get | `projectMM/563cfe/on/get` | `true` / `false` | -| set → device | `projectMM/563cfe/brightness/set` | `0`–`100` | -| device → get | `projectMM/563cfe/brightness/get` | `0`–`100` | -| set → device | `projectMM/563cfe/hsv/set` | `h,s,v` (hue `0`–`359`, sat/val `0`–`100`) | -| device → get | `projectMM/563cfe/hsv/get` | `h,s,v` | -| device → get | `projectMM/563cfe/name` | the friendly `deviceName` (retained) | -| device → get | `projectMM/563cfe/update/state` | `{"installed_version":…,"latest_version":…,"release_url":…,"title":…}` (retained; HA update entity) | -| set → device | `projectMM/563cfe/update/set` | target version string (empty = install latest); triggers OTA against the matching GitHub release asset | +| set → device | `MoonLight/563cfe/on/set` | `true` / `false` | +| device → get | `MoonLight/563cfe/on/get` | `true` / `false` | +| set → device | `MoonLight/563cfe/brightness/set` | `0`–`100` | +| device → get | `MoonLight/563cfe/brightness/get` | `0`–`100` | +| set → device | `MoonLight/563cfe/hsv/set` | `h,s,v` (hue `0`–`359`, sat/val `0`–`100`) | +| device → get | `MoonLight/563cfe/hsv/get` | `h,s,v` | +| device → get | `MoonLight/563cfe/name` | the friendly `deviceName` (retained) | +| device → get | `MoonLight/563cfe/update/state` | `{"installed_version":…,"latest_version":…,"release_url":…,"title":…}` (retained; HA update entity) | +| set → device | `MoonLight/563cfe/update/set` | target version string (empty = install latest); triggers OTA against the matching GitHub release asset | The HomeKit color wheel has no "palette" concept, so `hsv/set`'s hue+saturation pick the **nearest palette** (each built-in palette has a representative color; the closest one is selected) and the value drives brightness, the color wheel becomes a natural palette selector. -**Homebridge**, install [`homebridge-mqttthing`](https://github.com/arachnetech/homebridge-mqttthing) and add a `lightbulb` accessory. Use the device's own MAC suffix (read it from the `mqtt_status`/topics, or `mosquitto_sub -t 'projectMM/#'`) in place of `563cfe`: +**Homebridge**, install [`homebridge-mqttthing`](https://github.com/arachnetech/homebridge-mqttthing) and add a `lightbulb` accessory. Use the device's own MAC suffix (read it from the `mqtt_status`/topics, or `mosquitto_sub -t 'MoonLight/#'`) in place of `563cfe`: ```json { "accessory": "mqttthing", "type": "lightbulb", - "name": "projectMM", + "name": "MoonLight", "url": "mqtt://:1883", "username": "", "password": "", "topics": { - "getOn": "projectMM/563cfe/on/get", - "setOn": "projectMM/563cfe/on/set", - "getBrightness": "projectMM/563cfe/brightness/get", - "setBrightness": "projectMM/563cfe/brightness/set", - "getHSV": "projectMM/563cfe/hsv/get", - "setHSV": "projectMM/563cfe/hsv/set" + "getOn": "MoonLight/563cfe/on/get", + "setOn": "MoonLight/563cfe/on/set", + "getBrightness": "MoonLight/563cfe/brightness/get", + "setBrightness": "MoonLight/563cfe/brightness/set", + "getHSV": "MoonLight/563cfe/hsv/get", + "setHSV": "MoonLight/563cfe/hsv/set" }, "onValue": "true", "offValue": "false" @@ -389,8 +389,8 @@ The HomeKit color wheel has no "palette" concept, so `hsv/set`'s hue+saturation ``` Home Assistant adopts the device two ways, both zero-config: -- **MQTT auto-discovery**: with `haDiscovery` on (opt-in; off by default) and a broker set, the device announces itself on `homeassistant/light/projectMM_/config` and HA auto-creates a wired entity with **on/off + brightness** (the config declares `brightness` only; color isn't in it, so the entity has no color control). Retained across reboots. Color/palette stays on the separate `hsv/set` topic above, not this entity. Off by default because the WLED `/json` shim already gives HA a richer light (color + palette + sensors) over mDNS with no broker, leaving both on lists the device twice; enable this only for broker-only / cross-subnet setups. -- **WLED integration**: HA's built-in WLED integration discovers the device over the WLED `/json` API projectMM already serves; on/off + brightness work with no broker. +- **MQTT auto-discovery**: with `haDiscovery` on (opt-in; off by default) and a broker set, the device announces itself on `homeassistant/light/MoonLight_/config` and HA auto-creates a wired entity with **on/off + brightness** (the config declares `brightness` only; color isn't in it, so the entity has no color control). Retained across reboots. Color/palette stays on the separate `hsv/set` topic above, not this entity. Off by default because the WLED `/json` shim already gives HA a richer light (color + palette + sensors) over mDNS with no broker, leaving both on lists the device twice; enable this only for broker-only / cross-subnet setups. +- **WLED integration**: HA's built-in WLED integration discovers the device over the WLED `/json` API MoonLight already serves; on/off + brightness work with no broker. Both can be on at once. Setup walkthrough (including exposing HA to Apple Home via HA's HomeKit Bridge, no Homebridge needed) in the [Home Assistant recipe](../../how-to/home-automation.md#adopt-in-home-assistant). diff --git a/docs/moonmodules/core/ui.md b/docs/moonmodules/core/ui.md index cbafa7fb..141c1c04 100644 --- a/docs/moonmodules/core/ui.md +++ b/docs/moonmodules/core/ui.md @@ -1,6 +1,6 @@ # Web UI -The projectMM web UI as shipped, the render layer over the MoonModule tree. This page is the UI's own implementation spec (status bar, cards, control rendering, styling, the no-rebuild update contract). The **high-level architecture**, hand-maintained files, MoonModule-driven rendering, the light-domain plug-in points, lives in [MoonCore, the web UI](../../explanation/architecture/mooncore.md#web-ui); the **backend contract** it consumes (every `/api/*` endpoint, the `/ws` frame shape, the control descriptors) is owned by [HttpServerModule](moxygen/HttpServerModule.md); the **emoji legend** the cards and picker render is [the tag emoji legend](../../explanation/architecture/index.md#tag-emoji-legend). +The MoonLight web UI as shipped, the render layer over the MoonModule tree. This page is the UI's own implementation spec (status bar, cards, control rendering, styling, the no-rebuild update contract). The **high-level architecture**, hand-maintained files, MoonModule-driven rendering, the light-domain plug-in points, lives in [MoonCore, the web UI](../../explanation/architecture/mooncore.md#web-ui); the **backend contract** it consumes (every `/api/*` endpoint, the `/ws` frame shape, the control descriptors) is owned by [HttpServerModule](moxygen/HttpServerModule.md); the **emoji legend** the cards and picker render is [the tag emoji legend](../../explanation/architecture/index.md#tag-emoji-legend). This page covers only what those don't: the browser-side rendering behavior. ## Interaction principles @@ -43,7 +43,7 @@ Fixed top, 44 px, left-to-right: 1. **Hamburger (☰)**, toggles the side nav (`body.nav-open`). See [§ Side navigation](#side-navigation). 2. **MoonLight logo**, 28 px PNG from `/moonlight-logo.png`; the same image is the page favicon. -3. **Brand wordmark**, "projectMM". +3. **Brand wordmark**, "MoonLight". 4. **Device name**, from the `System.deviceName` control. 5. **System stats**, `uptime · NN K free` (uptime `Xd Yh Zm Ws`, free heap KB), read from the SystemModule controls in the WS state push; no separate polling endpoint. diff --git a/docs/moonmodules/light/drivers.md b/docs/moonmodules/light/drivers.md index acb33cc0..84445b55 100644 --- a/docs/moonmodules/light/drivers.md +++ b/docs/moonmodules/light/drivers.md @@ -175,7 +175,7 @@ Detail: [technical](moxygen/PreviewDriver.md) NDI driver controls -Publishes the layer as an **NDI video source**, so OBS, Resolume, TouchDesigner or any other NDI receiver picks projectMM up by name, on this machine or another on the network. Where the Preview driver draws the lights for a person, this hands the same frame to a production tool as video. +Publishes the layer as an **NDI video source**, so OBS, Resolume, TouchDesigner or any other NDI receiver picks MoonLight up by name, on this machine or another on the network. Where the Preview driver draws the lights for a person, this hands the same frame to a production tool as video. The grid becomes the frame, one light per pixel, output correction applied, so a receiver sees what the wall sees. **Desktop only, and you install the NDI runtime yourself**; without it the driver says so and nothing else changes. See [the details below](#ndi-details). @@ -303,7 +303,7 @@ Read the write-up with its comments: a reader supplied the controller-number fie ## HLS, details -**On desktop you install ffmpeg yourself** (any 5.x+, on PATH), because projectMM never ships or links an encoder. Install it with `brew` on macOS, `winget` on Windows, or `apt` on Debian, Ubuntu and Raspberry Pi OS. Without it the driver reports `ffmpeg not found` and nothing else changes. +**On desktop you install ffmpeg yourself** (any 5.x+, on PATH), because MoonLight never ships or links an encoder. Install it with `brew` on macOS, `winget` on Windows, or `apt` on Debian, Ubuntu and Raspberry Pi OS. Without it the driver reports `ffmpeg not found` and nothing else changes. The `encoder` control picks which one ffmpeg uses: `libx264` (the default, in practically every build) is software. @@ -311,7 +311,7 @@ Three hardware encoders offload it instead, and are worth picking on large grids An encoder your ffmpeg lacks starts and exits immediately; the status then reads `encoder exited - check ffmpeg`. -**On the ESP32-P4** there is no ffmpeg and no filesystem in the path: the chip's own H.264 block encodes and projectMM packages the MPEG-TS itself. Segments are served from a RAM ring rather than written to flash, which at one segment per second would wear it for nothing. The `encoder` control is absent, since the hardware offers only one. +**On the ESP32-P4** there is no ffmpeg and no filesystem in the path: the chip's own H.264 block encodes and MoonLight packages the MPEG-TS itself. Segments are served from a RAM ring rather than written to flash, which at one segment per second would wear it for nothing. The `encoder` control is absent, since the hardware offers only one. **Sizing the picture.** The P4's encoder takes only EVEN dimensions between 80x80 and 1920x2032, so an odd wall has its scale doubled so both axes come out even. A wall whose scaled size exceeds the maximum is refused with a status rather than streaming something the hardware cannot encode. Desktop ffmpeg has none of these limits. The floor is what the auto scale exists for: the P4 will not accept a frame smaller than 80x80, and a small wall streamed 1:1 arrives as a postage stamp in the player. `scale` at 0 (the default) therefore picks the smallest whole factor that lifts *both* axes to 80: a 20x10 wall streams as 160x80 rather than being refused, and a wall already past 80 stays 1:1. One factor serves both axes, so the aspect ratio is preserved and each light stays a square block. Raising `scale` by hand on an already-large wall costs real time (a 128x128 wall at scale 4 measures about 60 ms per frame against 1 ms at 1:1) and buys nothing a player's own zoom does not. @@ -361,15 +361,15 @@ ffplay -fflags nobuffer -flags low_delay -probesize 32 -analyzeduration 0 \ ## NDI, details -**You install the NDI runtime yourself**, projectMM cannot ship it. Until you do, the driver reports `NDI runtime not installed` and everything else works normally. +**You install the NDI runtime yourself**, MoonLight cannot ship it. Until you do, the driver reports `NDI runtime not installed` and everything else works normally. | OS | Where it comes from | |---|---| -| macOS | [NDI Tools](https://ndi.video/tools/) (free). It puts the runtime inside its app bundles rather than system-wide, which projectMM knows to look for; a Resolume install also carries one. | +| macOS | [NDI Tools](https://ndi.video/tools/) (free). It puts the runtime inside its app bundles rather than system-wide, which MoonLight knows to look for; a Resolume install also carries one. | | Windows | The [NDI Tools](https://ndi.video/tools/) or SDK installer puts `Processing.NDI.Lib.x64.dll` on the PATH. | | Linux | The NDI SDK. | -**To see the output** you need a receiver. **NDI Video Monitor** (part of NDI Tools) is the simplest; OBS gains an "NDI Source" via the [DistroAV](https://github.com/DistroAV/DistroAV) plugin. projectMM appears by the name in `sourceName`, or the device's own name when that is blank. +**To see the output** you need a receiver. **NDI Video Monitor** (part of NDI Tools) is the simplest; OBS gains an "NDI Source" via the [DistroAV](https://github.com/DistroAV/DistroAV) plugin. MoonLight appears by the name in `sourceName`, or the device's own name when that is blank. **Desktop only.** No NDI runtime exists for the ESP32 chips, so the driver is not offered there. An ESP32 reaches the same tools over Art-Net, sACN or DDP instead, send with the [Network Send](#networksend) driver, receive with the NetworkReceive effect. diff --git a/docs/moonmodules/light/effects.md b/docs/moonmodules/light/effects.md index 1491f9a8..ede303f7 100644 --- a/docs/moonmodules/light/effects.md +++ b/docs/moonmodules/light/effects.md @@ -4,7 +4,7 @@ Every effect, one block each: its preview, what it does, and what each control m Effects are built from the shared [power functions](power-functions.md): the drawing, field and motion routines every effect composes; that page lists each one with its callers. -**Jump to:** [MoonLight](#moonlight-effects) · [MoonModules](#moonmodules-effects) · [WLED](#wled-effects) · [FastLED](#fastled-effects) · [projectMM-native](#projectmm-native-effects) +**Jump to:** [MoonLight](#moonlight-effects) · [MoonModules](#moonmodules-effects) · [WLED](#wled-effects) · [FastLED](#fastled-effects) · [MoonLight-native](#moonlight-native-effects) > Some WLED-origin effects show a preview gif from [WLED-Utils](https://github.com/scottrbailey/WLED-Utils) by scottrbailey (the canonical WLED effect gif set, cross-linked with credit); these show WLED's rendering. Effects with a local `../../assets/…` gif show our own output. @@ -30,7 +30,7 @@ Emitters pouring color into a flow that carries and folds it. There is no veloci Compare with [Fluid](#fluid): the solver when the medium is the subject, this when the color is. -Origin: MoonLight · concept by [Stefan Petrick](https://github.com/StefanPetrick), composition by Jeff (mindful_stone / [4wheeljive](https://github.com/4wheeljive)) in [FlowFields](https://github.com/4wheeljive/FlowFields/blob/main/src/flows/flow_noise.h) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_FastLED.h) +Origin: MoonLight · concept by [Stefan Petrick](https://github.com/StefanPetrick), composition by Jeff (mindful_stone / [4wheeljive](https://github.com/4wheeljive)) in [FlowFields](https://github.com/4wheeljive/FlowFields/blob/main/src/flows/flow_noise.h) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_FastLED.h) Detail: [technical](moxygen/ColorTrailsEffect.md) @@ -45,7 +45,7 @@ Two interfering sine waves beat against each other into a moiré color field. - `freq_x` / `freq_y`: horizontal/vertical wave frequency (1–8). - `speed`: animation rate (0 = frozen). -Origin: WLED · by ldirko & blazoncek (WLED port) · [gallery](https://editor.soulmatelights.com/gallery/1089-distorsion-waves) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED · by ldirko & blazoncek (WLED port) · [gallery](https://editor.soulmatelights.com/gallery/1089-distorsion-waves) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/DistortionWavesEffect.md) @@ -64,7 +64,7 @@ A solid color filling a positioned box within the grid, with an optional alterna - `Rectangle width` / `height` / `depth`: the box extent on each axis. - `alternateWhite`: alternate box pixels to white in a checker pattern. -Origin: MoonLight · by [limpkin](https://github.com/limpkin) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by [limpkin](https://github.com/limpkin) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/FixedRectangleEffect.md) @@ -86,7 +86,7 @@ Audio-reactive sawtooth waves: each column maps to a frequency band whose magnit - `keepOn`: keep oscillating even when a band is silent. - `method`: phase model (`Chaos`, `Chaos fix`, `BandPhases`). -Origin: MoonLight (audio) · by [@TroyHacks](https://github.com/troyhacks) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight (audio) · by [@TroyHacks](https://github.com/troyhacks) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/FreqSawsEffect.md) @@ -104,7 +104,7 @@ Three slow blobs through a black→red→orange→yellow→white ramp: atmospher - `radius`: blob influence radius. - `intensity`: field gain into the black→red→orange→yellow→white ramp. -Origin: projectMM original (metaball lava lamp) +Origin: MoonLight original (metaball lava lamp) Detail: [technical](moxygen/LavaLampEffect.md) @@ -123,7 +123,7 @@ Sweeps axis-aligned planes in sync; red/green/blue name the X/Y/Z axis: a previe - `axis`: which plane sweeps: `all`, `x`, `y` or `z`, in `lines`. - `panelW` / `panelH`: the panel the dot walks, in `panel dots`. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/LinesEffect.md) @@ -140,7 +140,7 @@ Detail: [technical](moxygen/LinesEffect.md) - `count`: number of orbiting balls (1–8). - `hue_shift`: rotate the palette index. -Origin: projectMM original (metaballs) +Origin: MoonLight original (metaballs) Detail: [technical](moxygen/MetaballsEffect.md) @@ -159,7 +159,7 @@ A swarm of drifting particles with persistent fading trails. - `fade`: trail persistence (higher = longer tails). - `hue_shift`: rotate every particle's hue. -Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/ParticlesEffect.md) @@ -195,7 +195,7 @@ An algorithmic palette pattern driven by two beat oscillators (a macro and a mic - `macroMutatorFreq` / `Min` / `Max`: the coarse mutator's beat rate and range. - `microMutatorFreq` / `Min` / `Max`: the fine mutator's beat rate and range. -Origin: MoonLight · by MONSOONO / @Flavourdynamics · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by MONSOONO / @Flavourdynamics · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/PraxisEffect.md) @@ -211,7 +211,7 @@ Diagonal animated rainbow: always-visible default/test effect. - `speed`: animation BPM (one full hue cycle per beat). -Origin: FastLED · Mark Kriegsman (rainbow) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_FastLED.h) +Origin: FastLED · Mark Kriegsman (rainbow) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_FastLED.h) Detail: [technical](moxygen/RainbowEffect.md) @@ -227,7 +227,7 @@ Lights one random light per frame in a random palette color over a fading backgr - `fade`: how fast prior sparkles fade to black. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/RandomEffect.md) @@ -246,7 +246,7 @@ Expanding concentric rings from random centers, additive overlap (calm defaults) - `thickness`: ring band width. - `hue_shift`: rotate every ring's hue. -Origin: projectMM original (concentric rings) +Origin: MoonLight original (concentric rings) Detail: [technical](moxygen/RingsEffect.md) @@ -263,7 +263,7 @@ Distance-from-center sets a per-column wave phase; the lit surface ripples like - `speed`: wave animation rate (0 = frozen, 99 = fast). - `interval`: wavefront spacing (low = tight rings, high = wide). -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/RipplesEffect.md) @@ -282,7 +282,7 @@ A 3D Rubik's Cube projected onto the volume: it scrambles, then plays its soluti - `randomTurning`: turn endlessly at random instead of scramble-then-solve. - `usePalette`: color the faces from the palette, not the classic colors. -Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/RubiksCubeEffect.md) @@ -306,7 +306,7 @@ Shells rise, stall, and burst into sparks that arc over and fall. Nothing schedu Physics runs on elapsed time, so the same settings behave identically at any frame rate. -Origin: projectMM original, on the WLED Particle System's firework family by Damian Schneider / [@DedeHai](https://github.com/DedeHai) +Origin: MoonLight original, on the WLED Particle System's firework family by Damian Schneider / [@DedeHai](https://github.com/DedeHai) Detail: [technical](moxygen/FireworksEffect.md) @@ -327,7 +327,7 @@ An aquarium on a light wall: fish of three shapes swim across a dark tank, each Uses the global palette, each fish's band a paler version of its own body color. -Origin: projectMM original; inspired by the aquarium screensavers of the After Dark era, the pixel art drawn fresh for this effect +Origin: MoonLight original; inspired by the aquarium screensavers of the After Dark era, the pixel art drawn fresh for this effect Detail: [technical](moxygen/FishTankEffect.md) @@ -347,7 +347,7 @@ The classic screensaver on a light wall: chrome toasters with flapping wings and The sprites carry their own colors (chrome, wing, crust), so the global palette does not apply. Needs a grid at least the toaster's size (12×9). -Origin: projectMM original; inspired by After Dark's Flying Toasters (Berkeley Systems, 1989), suggested by Frank ([softhack007](https://github.com/softhack007)): the pixel art here is drawn fresh for this effect +Origin: MoonLight original; inspired by After Dark's Flying Toasters (Berkeley Systems, 1989), suggested by Frank ([softhack007](https://github.com/softhack007)): the pixel art here is drawn fresh for this effect Detail: [technical](moxygen/FlyingToastersEffect.md) @@ -395,7 +395,7 @@ The first effect that aims a fixture rather than only coloring it. A fixture chain is one-dimensional, so lay the rig out as a **1 x N** grid. -Origin: projectMM original +Origin: MoonLight original Detail: [technical](moxygen/MovingHeadEffect.md) @@ -417,7 +417,7 @@ The characters travel independently, each on its own path. Pacman keeps his yellow; the ghosts take their colors from the active palette. -Origin: projectMM original; inspired by Namco's Pac-Man (1980), the pixel art drawn fresh for this effect +Origin: MoonLight original; inspired by Namco's Pac-Man (1980), the pixel art drawn fresh for this effect Detail: [technical](moxygen/PacmanEffect.md) @@ -437,7 +437,7 @@ The 1978 formation marching down the wall: five ranks stepping sideways in the t The invaders take their body color from the active palette. -Origin: projectMM original, after Taito's Space Invaders (1978) +Origin: MoonLight original, after Taito's Space Invaders (1978) Detail: [technical](moxygen/SpaceInvadersEffect.md) @@ -458,7 +458,7 @@ A fountain that throws the project's whole sprite cast: fish, Pacman and his gho Colors come from the active palette, one per sprite, held for its whole flight. -Origin: projectMM original +Origin: MoonLight original Detail: [technical](moxygen/SpriteFountainEffect.md) @@ -479,7 +479,7 @@ Two paddles rallying a ball, the attract mode of the 1972 original where both pl Uses the global palette. -Origin: projectMM original, after Atari's Pong (1972) +Origin: MoonLight original, after Atari's Pong (1972) Detail: [technical](moxygen/PongEffect.md) @@ -502,7 +502,7 @@ Several noise fields, each on its own clock, read in polar coordinates and compo - `polarTable`, `polarTable16`: as PolarNoise above. -Origin: projectMM original, in the shader vocabulary Stefan Petrick made recognizable in the LED world +Origin: MoonLight original, in the shader vocabulary Stefan Petrick made recognizable in the LED world Detail: [technical](moxygen/AuroraEffect.md) @@ -523,7 +523,7 @@ Falling balls that pile up and shove each other aside. The heap is emergent: gra Collisions are the one non-linear part of the particle kernel, so the pool is small. -Origin: projectMM original, on the WLED Particle System's ballpit family by Damian Schneider / [@DedeHai](https://github.com/DedeHai) +Origin: MoonLight original, on the WLED Particle System's ballpit family by Damian Schneider / [@DedeHai](https://github.com/DedeHai) Detail: [technical](moxygen/BallpitEffect.md) @@ -540,7 +540,7 @@ Two color fields trade places pixel by pixel in an order that looks random but i - `eased`: ease the progress instead of sweeping linearly. - `scatter`: random order; off gives a positional wipe from the same code. -Origin: projectMM original, on the classic dissolve transition in its position-addressed (shader) form +Origin: MoonLight original, on the classic dissolve transition in its position-addressed (shader) form Detail: [technical](moxygen/DissolveEffect.md) @@ -560,7 +560,7 @@ The previous frame fed back through a zoom and rotation, dimmed, with a bright s Shows that feedback is not a primitive: once the grid can be read as a texture (`sampleWrap`), the whole family of trails, zoom blur and smear is a few lines. -Origin: projectMM original, on video feedback and the standard texture-feedback shader shape +Origin: MoonLight original, on video feedback and the standard texture-feedback shader shape Detail: [technical](moxygen/EchoEffect.md) @@ -580,7 +580,7 @@ An audio analyser with real meter ballistics: bars rise fast enough to catch a t The asymmetry is the whole point; a symmetric follower either misses the hit or flickers. -Origin: projectMM original, on standard VU/PPM meter ballistics and WLED's GEQ band mapping +Origin: MoonLight original, on standard VU/PPM meter ballistics and WLED's GEQ band mapping Detail: [technical](moxygen/SpectrumEffect.md) @@ -601,7 +601,7 @@ A maze of interlocking arcs that never repeats, drawn without storing a single t **The representative 2D shader**: no 3D, no rays, no float, and cheap on any target. -Origin: projectMM original, on Sébastien Truchet's 1704 tiling and the standard shader fract/hash/smoothstep idiom +Origin: MoonLight original, on Sébastien Truchet's 1704 tiling and the standard shader fract/hash/smoothstep idiom Detail: [technical](moxygen/TruchetEffect.md) @@ -622,7 +622,7 @@ Light poured into a simulated medium and carried by it. Every other flow here is On a cube every depth slice is its own medium, so the slices differ. Sized for the desktop and the P4. -Origin: projectMM original, after Stam 1999 "Stable Fluids" +Origin: MoonLight original, after Stam 1999 "Stable Fluids" Detail: [technical](moxygen/FluidEffect.md) @@ -644,7 +644,7 @@ A noise field decides where light is born, a curl flow decides where it goes, an Held at 16 bits and dithered on the way out, which keeps a slow fade smooth. -Origin: projectMM original, composing the noise-field and curl-flow kernels: the contrast window is Aurora's, in the shader vocabulary Stefan Petrick made recognizable in the LED world, and the flow is Bridson's curl noise (SIGGRAPH 2007) +Origin: MoonLight original, composing the noise-field and curl-flow kernels: the contrast window is Aurora's, in the shader vocabulary Stefan Petrick made recognizable in the LED world, and the flow is Bridson's curl noise (SIGGRAPH 2007) Detail: [technical](moxygen/NebulaEffect.md) @@ -664,7 +664,7 @@ Dots thrown into a moving medium, leaving tails the flow carries and bends. Noth The trail plane is 16-bit, which is what lets a tail fade smoothly rather than stepping. -Origin: projectMM original, in the flow-field idiom (4wheeljive's FlowFields, from a Stefan Petrick concept), with Stam's backward advection for the transport +Origin: MoonLight original, in the flow-field idiom (4wheeljive's FlowFields, from a Stefan Petrick concept), with Stam's backward advection for the transport Detail: [technical](moxygen/TrailsEffect.md) @@ -683,7 +683,7 @@ A texture mapped onto the inside of an infinite tube, so the viewer appears to f - `octaves`: wall texture detail, and the cost knob. - `vignette`: darken toward the vanishing point so it reads as receding. -Origin: projectMM original, on the standard demoscene tunnel +Origin: MoonLight original, on the standard demoscene tunnel Detail: [technical](moxygen/TunnelEffect.md) @@ -703,7 +703,7 @@ A rotating 3D object drawn as shaded spheres, the demoscene classic that named t Without painter's ordering a far ball paints over a near one and the object turns inside out. -Origin: projectMM original, on the Amiga-era demoscene vector-ball effect +Origin: MoonLight original, on the Amiga-era demoscene vector-ball effect Detail: [technical](moxygen/VectorBallsEffect.md) @@ -724,7 +724,7 @@ A propagating wave simulation: drops land, their rings spread outward, reflect o Distinct from [Ripples](#ripples), which draws clean concentric circles; this behaves like water. -Origin: projectMM original, on Hugo Elias's water surface algorithm +Origin: MoonLight original, on Hugo Elias's water surface algorithm Detail: [technical](moxygen/WaterRippleEffect.md) @@ -744,7 +744,7 @@ A lit 3D scene rendered by marching a ray through a distance field, one ray per Compiled only where the chip has a hardware FPU. Cost is per pixel, so `steps` trades quality against it. -Origin: projectMM original, on Iñigo Quilez's raymarching and distance-function articles +Origin: MoonLight original, on Iñigo Quilez's raymarching and distance-function articles Detail: [technical](moxygen/RaymarchEffect.md) @@ -767,7 +767,7 @@ A warped noise field addressed by angle and radius, folded into a kaleidoscope. Cost scales with `octaves` and `warp`: at `warp` > 0 and `octaves` 2 it is roughly 4 noise samples per pixel. On a large wall set `octaves` to 1 or `warp` to 0, which degrades to a plain polar noise that still reads well. -Origin: projectMM original, after Stefan Petrick's polar/noise vocabulary and Iñigo Quilez's domain warping +Origin: MoonLight original, after Stefan Petrick's polar/noise vocabulary and Iñigo Quilez's domain warping Detail: [technical](moxygen/PolarNoiseEffect.md) @@ -788,7 +788,7 @@ A circle and a box orbit and melt into each other, drawn as signed distance fiel Measured on an ESP32-S3 at 128×128: 20 fps, 728 cycles/pixel using the true-distance form, alongside StarSky (692) and Metaballs (647) at the same size. -Origin: projectMM original, after Iñigo Quilez's distance-function catalogue and polynomial smooth-minimum (iquilezles.org) +Origin: MoonLight original, after Iñigo Quilez's distance-function catalogue and polynomial smooth-minimum (iquilezles.org) Detail: [technical](moxygen/SdfShapesEffect.md) @@ -806,7 +806,7 @@ A flat fill with five color modes: a plain RGB(W) color, the active palette spre - `minRGB`: in the band modes, drop palette entries darker than this floor. - `randomColors`: in the band modes, shuffle the surviving palette entries. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/SolidEffect.md) @@ -822,7 +822,7 @@ A hollow spherical shell that bounces through the 3D volume, its surface colored - `speed`: how fast the sphere moves through the volume. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/SphereMoveEffect.md) @@ -840,7 +840,7 @@ Rotating spiral from angle + distance (`atan2_8`/`dist8`). - `twist`: how tightly the arm winds (hue gain per unit of distance). - `hue_shift`: rotate the palette index. -Origin: projectMM original (rotating spiral) +Origin: MoonLight original (rotating spiral) Detail: [technical](moxygen/SpiralEffect.md) @@ -859,7 +859,7 @@ A perspective starfield: stars approach the viewer from a vanishing point, brigh - `blur`: motion-trail fade per frame. - `usePalette`: color the stars from the palette instead of white. -Origin: MoonLight · by [@Brandon502](https://github.com/Brandon502), inspired by Daniel Shiffman / [Coding Train](https://www.youtube.com/watch?v=17WoOqgXsRM) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by [@Brandon502](https://github.com/Brandon502), inspired by Daniel Shiffman / [Coding Train](https://www.youtube.com/watch?v=17WoOqgXsRM) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/StarFieldEffect.md) @@ -877,7 +877,7 @@ Twinkling stars at random light positions, each fading in and out independently - `star_fill_ratio`: how many stars (as a fraction of the light count). - `usePalette`: color the stars from the active palette instead of white. -Origin: MoonLight · by [limpkin](https://github.com/limpkin) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by [limpkin](https://github.com/limpkin) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/StarSkyEffect.md) @@ -897,7 +897,7 @@ Renders a multi-line string in a bitmap font. Static by default (laid out top-le - `speed`: marquee speed (only used when `scroll` is on). - `hue`: palette index for the text color. -Origin: projectMM original, on MoonLight's Scrolling Text · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight original, on MoonLight's Scrolling Text · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/TextEffect.md) @@ -925,7 +925,7 @@ Conway's cellular automaton generalized to 2D and 3D, with selectable rulesets a - `infinite`: respawn on stasis (R-pentomino/glider) instead of resetting. - `blur`: dead-cell fade strength toward the background color. -Origin: MoonModules · by Ewoud Wijma (2022), mods by Brandon Butler / [@Brandon502](https://github.com/Brandon502) · [natureofcode](https://natureofcode.com/book/chapter-7-cellular-automata/) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonModules.h) +Origin: MoonModules · by Ewoud Wijma (2022), mods by Brandon Butler / [@Brandon502](https://github.com/Brandon502) · [natureofcode](https://natureofcode.com/book/chapter-7-cellular-automata/) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonModules.h) Detail: [technical](moxygen/GameOfLifeEffect.md) @@ -946,7 +946,7 @@ A flat graphic equaliser: the 16 audio bands rise as vertical bars from the bott - `colorBars`: color each bar from the palette by band instead of by row. - `smoothBars`: blend neighboring bands for smoother bar heights. -Origin: WLED (audio) · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED (audio) · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/GEQEffect.md) @@ -967,7 +967,7 @@ A 3D-perspective graphic equaliser: audio bands rise as bars with faked depth, t - `numBands`: bands shown (2–16, fewer = wider bars). - `borders`: outline each bar. -Origin: MoonModules (audio) · by [@TroyHacks](https://github.com/troyhacks) (GPLv3) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonModules.h) +Origin: MoonModules (audio) · by [@TroyHacks](https://github.com/troyhacks) (GPLv3) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonModules.h) Detail: [technical](moxygen/GEQ3DEffect.md) @@ -988,7 +988,7 @@ Audio-reactive brush strokes: lines whose 3D endpoints oscillate on the beat (`b - `color_chaos`: per-line random hue vs a per-band gradient. - `phase_chaos`: random per-frame phase jitter. -Origin: MoonModules (audio) · by [@TroyHacks](https://github.com/troyhacks) (GPLv3) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonModules.h) +Origin: MoonModules (audio) · by [@TroyHacks](https://github.com/troyhacks) (GPLv3) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonModules.h) Detail: [technical](moxygen/PaintBrushEffect.md) @@ -1006,7 +1006,7 @@ Falling Tetris-style blocks: each column drops a brick that lands on the growing - `width`: brick height (0 = randomised). - `oneColor`: one advancing palette color for every brick, not one each. -Origin: WLED · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/TetrixEffect.md) @@ -1029,7 +1029,7 @@ Audio-reactive blurred dots: one frequency band per frame lights a dot whose pos - `freqMap`: place the dot by the major-peak frequency, not by scanning. - `geqScanner`: scan the dot across the strip in a GEQ-like sweep. -Origin: WLED (audio) · by Andrew Tuline (WLED-SR), enhancements by [@softhack007](https://github.com/softhack007) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED (audio) · by Andrew Tuline (WLED-SR), enhancements by [@softhack007](https://github.com/softhack007) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/BlurzEffect.md) @@ -1048,7 +1048,7 @@ A row of balls per column bounce under gravity, each losing energy on impact and - `grav`: gravity strength (higher = faster fall, snappier bounce). - `numBalls`: balls per column (1–16). -Origin: WLED · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/BouncingBallsEffect.md) @@ -1070,7 +1070,7 @@ A 1D scrolling frequency display: each frame shifts the strip and injects a new - `sensitivity`: input gain (10–100). - `audioSpeed`: let the volume modulate the scroll speed. -Origin: WLED (audio) · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED (audio) · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/FreqMatrixEffect.md) @@ -1090,7 +1090,7 @@ A Lissajous curve traced across the grid from two phase-shifted `sin8`/`cos8` sw - `fadeRate`: trail fade per frame. - `speed`: how fast the curve's phase advances. -Origin: WLED · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/LissajousEffect.md) @@ -1109,7 +1109,7 @@ An audio VU meter rendered as a noise bar: the volume sets how many rows light f - `fadeRate`: trail decay per frame (200–254). - `width`: how strongly the volume drives the bar height. -Origin: WLED (audio) · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: WLED (audio) · by Andrew Tuline (WLED-SR) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/NoiseMeterEffect.md) @@ -1127,7 +1127,7 @@ An oscilloscope waveform scrolls across the grid with a fading trail; six select - `fade`: trail fade per frame (0 = instant clear, 255 = long tail). - `type`: waveform shape: sawtooth, triangle, sine, square, sin3 or noise. -Origin: MoonLight · by Ewoud Wijma · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight · by Ewoud Wijma · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/WaveEffect.md) @@ -1166,13 +1166,13 @@ A gradient-noise field indexed straight into the palette: the plainest way to tu - `scale`: spatial frequency: low is broad blobs, high is fine detail. - `bpm`: how fast it moves. -Origin: FastLED · inoise field (Mark Kriegsman); the `morph` form from WLED via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: FastLED · inoise field (Mark Kriegsman); the `morph` form from WLED via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/NoiseEffect.md) [Tests](../../reference/tests/unit-tests.md#noiseeffect) -## projectMM-native effects +## MoonLight-native effects @@ -1185,7 +1185,7 @@ An effect you write as text on the running device, compiled to native code on th - `script`: which `.mle` file runs, picked from the library and edited here. - Every control the script declares, editable live without a recompile. -Origin: projectMM original, on the native-codegen approach of [ESPLiveScript](https://github.com/hpwit/ESPLiveScript) by Yves Bazin +Origin: MoonLight original, on the native-codegen approach of [ESPLiveScript](https://github.com/hpwit/ESPLiveScript) by Yves Bazin Detail: [technical](moxygen/MoonLiveEffect.md) @@ -1201,7 +1201,7 @@ The 16 mic frequency bands spread across X, each column lit bottom-up by its mag - `colorMode`: bars colored by `height`, the VU look, or `per-band`, a rainbow. -Origin: projectMM original, on the WLED-SR GEQ / spectrum concept (Andrew Tuline) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) +Origin: MoonLight original, on the WLED-SR GEQ / spectrum concept (Andrew Tuline) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_WLED.h) Detail: [technical](moxygen/AudioSpectrumEffect.md) @@ -1220,10 +1220,30 @@ Every beat is a stone dropped in water. A real wave simulation, which gives what - `rain`: idle drops when there is no music, so the surface is alive in silence. - `shine`: how strongly the slope lights the surface. -Origin: projectMM original, the two-buffer water simulation (Gomez 2000) driven by the onset detector +Origin: MoonLight original, the two-buffer water simulation (Gomez 2000) driven by the onset detector Detail: [technical](moxygen/BeatRipplesEffect.md) + + +### Pulse 💫🎶 · 3D + +Pulse effect preview + +Shells expanding from a drifting origin, one per beat: a pair of fronts on a strip, a ring on a panel, a sphere in a volume. Sound fires one and its pitch colors it, and an idle clock keeps them arriving in a silent room. The effect a device boots with. + +- `bpm`: pulses a minute while nothing is heard, where 0 waits for sound alone. +- `speed`: how fast a shell crosses the layout, at any layout size. +- `thickness`: the shell's width, as a share of the layout rather than in lights. +- `audioGain`: how strongly sound fires a pulse, where 0 ignores it. +- `drift`: how far the origin wanders from the center, where 0 pins it there. + +Compare with [BeatRipples](#beatripples), which simulates water on a plane. This one draws a moving front, which carries it into 1D and 3D. + +Origin: MoonLight original + +Detail: [technical](moxygen/PulseEffect.md) + ### VuMeters 💫🎶🖌️ · 3D @@ -1239,7 +1259,7 @@ The bass needles are heavier than the treble ones, so the low end swings and the - `peakHold`: how long the peak marker stays up, as a half-life. - `smooth`: drive from the meter ballistic rather than the raw band. -Origin: projectMM original, on the VU ballistics of IEC 60268-17 +Origin: MoonLight original, on the VU ballistics of IEC 60268-17 Detail: [technical](moxygen/VuMetersEffect.md) @@ -1257,7 +1277,7 @@ The spectrum as ripples. Each band owns a sector around the center, mirrored lef - `beat`: a white shockwave born at the center on every onset. - `polarTable`, `polarTable16`, `mapping`: the polar address, and its shape. -Origin: projectMM original, the radial spectrogram on `PolarLut` and the onset detector +Origin: MoonLight original, the radial spectrogram on `PolarLut` and the onset detector Detail: [technical](moxygen/RadialSpectrumEffect.md) @@ -1274,7 +1294,7 @@ Plays every other registered effect in turn, auto-advancing on a timer, so one L - `randomPalette`: pick a random palette on each cycle; on by default. - `showName`: overlay the playing effect's name in a small font; default on. -Origin: FastLED · Mark Kriegsman's [DemoReel100](https://github.com/FastLED/FastLED/blob/master/examples/DemoReel100/DemoReel100.ino); projectMM reel +Origin: FastLED · Mark Kriegsman's [DemoReel100](https://github.com/FastLED/FastLED/blob/master/examples/DemoReel100/DemoReel100.ino); MoonLight reel Detail: [technical](moxygen/DemoReelEffect.md) @@ -1291,7 +1311,7 @@ Receives lights over UDP and writes them into the layer: the receive side for Re - `universe_start`: the first incoming universe to map, mirroring the sender. - `channels_per_universe`: bytes each universe maps to; 510 or 512. -Origin: projectMM original (E1.31 / Art-Net receive) +Origin: MoonLight original (E1.31 / Art-Net receive) Detail: [technical](moxygen/NetworkReceiveEffect.md) @@ -1311,7 +1331,7 @@ R/G/B each follow a sine along one axis at 120° phase offset: a glowing, scroll - `amplitude`: peak brightness (0–255, 255 = full). - `bpm`: scroll speed. -Origin: MoonLight (Sinus, AI-generated) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) +Origin: MoonLight (Sinus, AI-generated) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Effects/E_MoonLight.h) Detail: [technical](moxygen/SineEffect.md) diff --git a/docs/moonmodules/light/layouts.md b/docs/moonmodules/light/layouts.md index 2c3edf84..e2b45049 100644 --- a/docs/moonmodules/light/layouts.md +++ b/docs/moonmodules/light/layouts.md @@ -33,7 +33,7 @@ A 3D cube volume, `width`×`height`×`depth`, wired in a configurable axis order - `X++` / `Y++` / `Z++` — count up (vs down) along that axis. - `snakeX` / `snakeY` / `snakeZ` — serpentine on that axis. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/CubeLayout.md) @@ -47,7 +47,7 @@ A hollow walk-in cube built from five LED-curtain faces (front, back, top, left, - `width` / `height` / `depth` — cube extent per axis (1–20). -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/HumanSizedCubeLayout.md) @@ -64,7 +64,7 @@ A 2D matrix panel with full wiring control: choose the axis order, per-axis dire - `X++` / `Y++` — count up vs down along that axis. - `snake` — serpentine wiring (alternate lines reverse). -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/PanelLayout.md) @@ -81,7 +81,7 @@ Tiles an M×N grid of full matrix panels into one large display: an outer walk o - `panelWidth` / `panelHeight` — each panel's size (1–512). - `wiringOrder` / `X++` / `Y++` / `snake` — the per-panel light wiring. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/PanelsLayout.md) @@ -99,7 +99,7 @@ A single ring of LEDs evenly spaced around a circle — `nrOfLEDs` points, start - `clockwise` — direction of travel. - `scale` — spacing/radius scale. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/RingLayout.md) @@ -115,7 +115,7 @@ The classic 241-LED concentric-ring disc: nested rings of 1, 8, 12, 16, 24, 32, - `outside in` — light 0 on the outer ring, wired inward rather than outward. - `angleFirst` — where light 0 of each ring sits, in degrees. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/Rings241Layout.md) @@ -132,7 +132,7 @@ A vertical line of LEDs at a fixed X — the 1D column primitive. - `X position` — the column's x. - `reversed order` — wire top-to-bottom instead of bottom-to-top. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/SingleColumnLayout.md) @@ -149,7 +149,7 @@ A horizontal line of LEDs at a fixed Y — the 1D row primitive. - `Y position` — the row's y. - `reversed order` — wire right-to-left instead of left-to-right. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/SingleRowLayout.md) @@ -165,7 +165,7 @@ A conical spiral: `ledCount` LEDs winding up a cone from `bottomRadius` to a poi - `bottomRadius` — radius at the base. - `height` — spiral height. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/SpiralLayout.md) @@ -197,11 +197,11 @@ Parallel vertical tubes: `nrOfTubes` columns of `ledsPerTube` LEDs, spaced `tube - `tubeDistance` — spacing between tubes. - `reversed` — reverse the wiring order. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/TubesLayout.md) -## projectMM-native layouts +## MoonLight-native layouts @@ -214,7 +214,7 @@ Where the lights physically are, written as text on the running device. A layout - `script`: which `.mll` file runs, picked from the library and edited here. - Everything the script declares appears as a real control. -Origin: projectMM original +Origin: MoonLight original Detail: [technical](moxygen/MoonLiveLayout.md) · [how the count is known](#moonlive-details) @@ -231,7 +231,7 @@ A dense 3D grid, row-major (x fastest, then y, then z); every position maps to a - `width` / `height` / `depth`: lights per axis (to 3840, 2160 and 512). - `serpentine` — every other row runs in reverse, matching a snaked strip. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/GridLayout.md) @@ -250,7 +250,7 @@ A [Grid](#grid) with **mid-strand dark columns**, held black in every row, for a - `blackCount` — how many dark columns; `0` renders exactly like a Grid. - `blackStart` — first dark column (shown only once `blackCount` is set). -Origin: projectMM +Origin: MoonLight Detail: [technical](moxygen/GridBlacksLayout.md) @@ -266,7 +266,7 @@ Lights on the surface of a hollow sphere — a one-light-thick shell inside a `( - `radius` — the shell's radius in light-units (1–64). -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/SphereLayout.md) @@ -283,7 +283,7 @@ A bicycle-wheel: `spokes` straight rows radiate from a center hub, each carrying - `spokes` — number of spokes radiating from the hub (2–64). - `ledsPerSpoke` — LEDs along each spoke, one unit apart. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Layouts/L_MoonLight.h) Detail: [technical](moxygen/WheelLayout.md) diff --git a/docs/moonmodules/light/modifiers.md b/docs/moonmodules/light/modifiers.md index 0e24b5ed..ffe7b839 100644 --- a/docs/moonmodules/light/modifiers.md +++ b/docs/moonmodules/light/modifiers.md @@ -14,7 +14,7 @@ A modifier folds coordinates rather than drawing, so it reaches for very little Expands a 1D effect into concentric **square rings** (Chebyshev distance from the center): the effect's linear position becomes the ring index, so a gradient effect draws nested squares. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/BlockModifier.md) @@ -31,7 +31,7 @@ Masks the layer in a checkerboard: "off" squares are dropped, "on" squares pass - `size` — checker square edge in lights (1–64). - `invert` — flip which squares pass through vs are masked. -Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/CheckerboardModifier.md) @@ -45,7 +45,7 @@ Detail: [technical](moxygen/CheckerboardModifier.md) Expands a 1D effect into concentric **circular rings** (Euclidean distance from the center): the effect's linear position becomes the radius, so a gradient effect draws nested circles. The circular counterpart to [Block](#block). -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/CircleModifier.md) @@ -61,7 +61,7 @@ Folds the far half of the box back onto the near half per axis, mirroring the im - `mirrorX` / `mirrorY` / `mirrorZ` — mirror across the center on that axis. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/MirrorModifier.md) @@ -78,7 +78,7 @@ Tiles the logical image across the box `multiply` times per axis, optionally mir - `multiplyX` / `multiplyY` / `multiplyZ` — tiles per axis, `1` meaning none. - `mirrorX` / `mirrorY` / `mirrorZ` — reflect alternate tiles on that axis. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/MultiplyModifier.md) @@ -98,7 +98,7 @@ Remaps the grid into radial **petals** around the center — the angle to each p - `symmetry` — fold the petals into a factor-of-360 symmetry. - `zTwist` — twist the petals along z (3D). -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/PinwheelModifier.md) @@ -115,7 +115,7 @@ Collapses an axis to a single plane so a higher-dimensional effect ripples along - `shrink` — collapse the selected axis (on = collapse). - `towardsX` / `towardsZ` — which axis collapses to a single line. -Origin: MoonLight · by @Troy (WLEDMM Art-Net) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · by @Troy (WLEDMM Art-Net) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/RippleXZModifier.md) @@ -132,13 +132,13 @@ Swaps a pair of box axes (and every coordinate through them), then optionally in - `XY` / `XZ` / `YZ` — swap that pair of axes. - `inverse X` / `inverse Y` / `inverse Z` — flip that axis after the swap. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/TransposeModifier.md) [Tests](../../reference/tests/unit-tests.md#transposemodifier) -## projectMM-native modifiers +## MoonLight-native modifiers @@ -151,7 +151,7 @@ The coordinate transform written as text on the running device: mirror the patte - `script`: which `.mlm` file runs, picked from the library and edited here. - Everything the script declares appears as a real control. -Origin: projectMM original +Origin: MoonLight original Detail: [technical](moxygen/MoonLiveModifier.md) · [what a script transforms](#moonlive-details) @@ -167,7 +167,7 @@ Remaps every light to another via a true 1:1 permutation, reshuffling to a fresh - `bpm` — reshuffles per minute, `0` freezing the permutation. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/RandomMapModifier.md) @@ -183,7 +183,7 @@ Carves the layer to a sub-rectangle given as percentages of the physical extent - `startX` … `endZ` — the bounds as **percentages** of each axis. -Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/RegionModifier.md) @@ -199,7 +199,7 @@ Rotates the 2D image around its center, turning continuously over time (the code - `speed` — rotation speed (1–255; turns faster as it rises). -Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/MoonLight/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) +Origin: MoonLight · by WildCats08 / [@Brandon502](https://github.com/Brandon502) · via [MoonLight](https://github.com/MoonModules/projectMM/blob/main/src/MoonLight/Nodes/Modifiers/M_MoonLight.h) Detail: [technical](moxygen/RotateModifier.md) diff --git a/docs/moonmodules/light/moonlive.md b/docs/moonmodules/light/moonlive.md index 7c1deeb3..becaaece 100644 --- a/docs/moonmodules/light/moonlive.md +++ b/docs/moonmodules/light/moonlive.md @@ -2,7 +2,7 @@ The MoonLive language: what a script may declare, what the engine hands it, and what it can call. A script compiles to native code on the device, so it runs at the speed of a compiled module. -The library that ships with projectMM is [moonlive/](https://github.com/MoonModules/projectMM/tree/main/moonlive), and the shortest way in is [Write your first script](../../tutorials/first-script.md). +The library that ships with MoonLight is [moonlive/](https://github.com/MoonModules/projectMM/tree/main/moonlive), and the shortest way in is [Write your first script](../../tutorials/first-script.md). ## A script is a class diff --git a/docs/moonmodules/light/power-functions.md b/docs/moonmodules/light/power-functions.md index 7e29990c..0ca4ce1c 100644 --- a/docs/moonmodules/light/power-functions.md +++ b/docs/moonmodules/light/power-functions.md @@ -14,7 +14,7 @@ Sources: [draw.h](moxygen/draw.md) (drawing), `core/util/math16.h` (16-bit math) The caller lists below are generated by reading the call sites, so they record what the code does rather than what it intends. Many of the names here are **[FastLED](https://github.com/FastLED/FastLED)'s**, deliberately: -`scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), and the `beatsin8` / `inoise8` / `qadd8` family are the vocabulary the LED-effect world already shares, so a contributor recognises them on sight. projectMM links no part of FastLED: the implementations are ours, integer-only and tuned for this render loop, with FastLED credited as the prior art behind the convention here and in each primitive's own notes (`core/util/math8.h` names Mark Kriegsman's lib8tion directly). +`scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), and the `beatsin8` / `inoise8` / `qadd8` family are the vocabulary the LED-effect world already shares, so a contributor recognises them on sight. MoonLight links no part of FastLED: the implementations are ours, integer-only and tuned for this render loop, with FastLED credited as the prior art behind the convention here and in each primitive's own notes (`core/util/math8.h` names Mark Kriegsman's lib8tion directly). ## Migrating an effect: two steps, in this order diff --git a/docs/reference/MIGRATING.md b/docs/reference/MIGRATING.md index 18fcb445..378cba43 100644 --- a/docs/reference/MIGRATING.md +++ b/docs/reference/MIGRATING.md @@ -2,7 +2,7 @@ The log of **breaking changes**, what changed between versions, and the action to take. -projectMM ships **no migration code**: the persistence layer is robust by default (an absent key keeps the control's default, a stale value clamps to the new bounds, an unknown key is ignored), which absorbs almost all schema drift with zero migration-specific code. The rare change that a robust reader *cannot* absorb is **documented here instead of migrated**. A patching framework is deferred rather than rejected: it becomes the right tool if breaking format changes get frequent enough that ad-hoc losses pile up (a rough bar: more than five across a few releases) and users hold persisted state too valuable to re-derive. At that point build the recognizable version-stamp plus ordered-patch-chain pattern, not a bespoke one and its rationale. +MoonLight ships **no migration code**: the persistence layer is robust by default (an absent key keeps the control's default, a stale value clamps to the new bounds, an unknown key is ignored), which absorbs almost all schema drift with zero migration-specific code. The rare change that a robust reader *cannot* absorb is **documented here instead of migrated**. A patching framework is deferred rather than rejected: it becomes the right tool if breaking format changes get frequent enough that ad-hoc losses pile up (a rough bar: more than five across a few releases) and users hold persisted state too valuable to re-derive. At that point build the recognizable version-stamp plus ordered-patch-chain pattern, not a bespoke one and its rationale. **The File Manager's Backup (⤓) / Restore (⟲) carries config across these breaks.** [src/ui/migrate.js](https://github.com/MoonModules/projectMM/blob/main/src/ui/migrate.js) is the **authoritative, dated log of every machine-mappable break** (file, type, control, and value renames): Restore applies it in the browser and reports what did not carry over, so entries below describe only what a map cannot express, behavior changes, semantics to re-check, and erase-flash moves. It works even on a freshly erased device: join its `MM-XXXX` SoftAP, open `http://4.3.2.1`, restore there, and take the offered restart; the bundle carries the WiFi credentials, so the device comes back on your network. For a device still on old firmware (no Backup button yet), the [installer page](https://moonmodules.org/projectMM/install/) offers the same backup as a bookmarklet. @@ -24,69 +24,68 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul ## Unreleased -Nothing yet. Entries land here as breaking changes are made, and the heading takes the version at its release. +### MQTT topics and the Home Assistant entity carry the product's new name -## v5.0.0 +**Action: *update an automation*, on a device you drive over MQTT or through Home Assistant.** -The last release under the projectMM name. Its [release notes](https://github.com/MoonModules/projectMM/releases) summarise what these entries ask of you. +The topic root is now `MoonLight/` where it was `projectMM/`, and the Home Assistant discovery object, its unique id and the client id follow the same root. +A broker subscription or an automation written against the old root stops matching, and Home Assistant keeps the old retained config, so the previous entity goes unavailable while a new one appears alongside it. -### System: `expertMode` became `mode`, with three levels +Delete the stale entity in Home Assistant and repoint any automation or dashboard at the new one, then rewrite subscriptions and publishes to the new root. +Nothing on the device needs changing: the root is derived from a single constant, so every topic moves together. -**Action: re-set a control, and only if you had expert mode on.** +### Audio arrives on every device, and its modes are reordered -The switch that revealed advanced controls is now a three-way select: `user`, `expert` (🎚️) and `developer` (🔧), each level showing what the one below it shows. A saved `expertMode` no longer matches a control and is dropped, so a device comes up in `user` mode whatever it held before. Pick the level you want again on the System card. +**Action: *re-set a control*, on a device whose Audio you had configured.** Restore maps the value for you, so this asks something only of a device upgraded in place. -The old flag could only say "show more" or "show less", which left diagnostics that mean nothing without the source sitting beside the controls a light show is built from. +Audio is now wired at boot rather than added by hand, because the default effect reacts to sound. A device without the module showed none of that: the lights moved without answering whether anything was heard. It defaults to **simulate**, a synthesized signal, so a device demonstrates the behavior before a microphone is wired to it. A board that has a microphone selects `local audio` in its catalog entry, the way it already names its pins. + +The mode options are reordered to run simple to advanced: **simulate, receive network, local audio**, where the order was local, receive, simulate. The default is now the first entry rather than an index that depended on whether the platform had a network. The selection is persisted as that index, so every saved value moves: what read 0 for local audio now reads 2, and what read 2 for simulate now reads 0. [Restore](../how-to/backup-and-restore.md) carries both. A device with no network has two options rather than three, and its old `1` is ambiguous, so re-pick that one by hand. + +## v5.0.0 -### Documentation: every page moved to its Diátaxis folder +The last release under the projectMM name. Its [release notes](https://github.com/MoonModules/projectMM/releases) summarise what these entries ask of you. -**Action: nothing on a device. Update a bookmark to a documentation page.** -Affects readers of [moonmodules.org/projectMM](https://moonmodules.org/projectMM/), not devices. +### Renames Restore carries for you -The published pages were flat at the site root and the folders that did exist carried mixed meanings: `reference/` held hardware pages while the nav's "Reference" section held two unrelated ones. Each page now sits in the folder naming its [Diátaxis](https://diataxis.fr/) type, so a path says what kind of page it reaches and the source tree matches the menu. +[migrate.js](https://github.com/MoonModules/projectMM/blob/main/src/ui/migrate.js) maps each of these, so a Backup taken on an older firmware restores onto this one with the value intact. Restore reports what it could not carry. They are listed rather than described, because the map is the description. -| Was | Now | +| Was | Is now | |---|---| -| `architecture/*.html` | `explanation/architecture/*.html` | -| `usecases/*.html` | `how-to/*.html` | -| `building.html` | `how-to/building.html` | -| `logging-an-issue.html` | `how-to/logging-an-issue.html` | -| `mooncloud.html` · `why-we-write-our-own.html` | `explanation/*.html` | -| `performance.html` · `testing.html` · `MIGRATING.html` | `reference/*.html` | -| `reference/.html` | `reference/hardware/.html` | -| `tests/*.html` · `metrics/*.html` | `reference/tests/*.html` · `reference/metrics/*.html` | -| `coding-standards.html` · `documentation-standards.html` · `principles-and-process.html` | `contributing/*.html` | -| `privacy-policy.html` | `legal/privacy-policy.html` | +| `Layers` container, `Layers.json` | `Effects`, `Effects.json` | +| `Noise2DEffect` | `NoiseEffect`, which renders the same field | +| `IrService` | `InfraredService` | +| `MultiPinLedDriver`, `MoonLedDriver`, `ParlioLedDriver`, `I80LedDriver`, `MoonI80LedDriver` | `ParallelLedDriver` with a `peripheral` select | +| a driver's `preset` | `lightPreset` | +| `soundReactive` | `audioReactive` | +| `forceRing` | `useRing` | +| `sync` on AudioService | `mode`, beside a new `send audio` | +| `fps` on PreviewDriver | `targetFps` | +| peripheral values `i80`, `MoonI80` | `LCD-IDF`, `LCD-MM` | -`index.html` and `gettingstarted.html` stay at the root, and `moonmodules/` is unchanged. No redirects are published, so an old bookmark 404s rather than forwarding. +Three residues a map cannot carry: -### The three MoonLive pages became one +- **A re-learned remote.** `InfraredService` keeps the module but not its codes: a learned code used to be a control's value and is now a row. Press the remote's keys again against the rows you want. +- **`simulate` on AudioService** collapsed from five options to two, so a saved value past the second is clamped rather than mapped. +- **A driver's `peripheral`** is chip-dependent where the old type did not say which bus it used. Restore flags it for review rather than guessing. -**Action: nothing on a device. Update a bookmark to a documentation page.** -Affects readers, not devices. The engine is the same whichever job a script does, so three pages repeated it and differed only in which moment the host calls. The roles are now sections on the one page. +An external tool that POSTs to a control by name follows the same renames; the device answers only to the current name. -| Was | Now | -|---|---| -| `moonmodules/light/MoonLiveLayout.html` | `moonmodules/light/MoonLiveEffect.html#a-layout-written-as-a-script` | -| `moonmodules/light/MoonLiveModifier.html` | `moonmodules/light/MoonLiveEffect.html#a-modifier-written-as-a-script` | +### Boards move to the MoonBase partition table (4 MB on 2026-08-26, esp32-16mb on 2026-08-28) -The generated technical pages under `moonmodules/light/moxygen/` keep one page per class and are unchanged. +**Action: erase flash** (USB re-flash). Back up first: the File Manager's ⤓, or the installer's bookmarklet on older firmware. Restore after the install brings WiFi, config and scripts back. -### Four task pages moved from Tutorials to How-to +The dual-OTA layout gives way to [MoonBase](../explanation/architecture/moonbase.md), which keeps one app slot and a recovery image rather than two app copies. On the 4 MB variants (`esp32`, `esp32-wrover`, `esp32-eth`) the app slot grows 1856 to 2496 KB and the filesystem 256 to 548 KB. On `esp32-16mb` the filesystem grows 7168 to 11264 KB and the app slot keeps its full 4096 KB. Both gain the same recovery story: a power cut mid-install boots MoonBase, and the update is retried over the network. -**Action: nothing on a device. Update a bookmark to a documentation page.** -Affects readers, not devices. A tutorial is a lesson for someone learning; putting projectMM on a machine is a task somebody already has. The four moved to the folder naming what they are, and the build page shed the two halves that were never how-to. +**Every partition moves, so the new table looks elsewhere for the filesystem volume.** Without a backup, WiFi credentials, module config and scripts all re-enter through provisioning. A partition table only changes over USB. A device still on the old table keeps OTA-updating within it for as long as the app fits, and the web installer is the migration path. 8 MB boards keep their layout. -| Was | Now | -|---|---| -| `tutorials/installing-to-desktop.html` | `how-to/installing-to-desktop.html` | -| `tutorials/installing-on-linux.html` | `how-to/installing-on-linux.html` | -| `tutorials/panel-cards.html` | `how-to/panel-cards.html` | -| `tutorials/control-surface.html` | `how-to/control-surface.html` | -| `how-to/building.html#tooling-overview` | `reference/build-system.html` | -| `how-to/building.html#why-not-arduino` | `explanation/why-we-write-our-own.html#esp-idf-directly-rather-than-arduino` | -| `how-to/building.html#third-party-libraries` | `explanation/why-we-write-our-own.html#third-party-libraries` | +### System: `expertMode` became `mode`, with three levels +**Action: re-set a control, and only if you had expert mode on.** + +The switch that revealed advanced controls is now a three-way select: `user`, `expert` (🎚️) and `developer` (🔧), each level showing what the one below it shows. A saved `expertMode` no longer matches a control and is dropped, so a device comes up in `user` mode whatever it held before. Pick the level you want again on the System card. + +The old flag could only say "show more" or "show less", which left diagnostics that mean nothing without the source sitting beside the controls a light show is built from. ### Audio: `floor` is now the silence threshold in both level modes @@ -108,67 +107,11 @@ MoonBase served `/install`, `/install-url`, `/boot-app`, `/last-url` and `/cance The break is between the two images on a device, not between a device and its config. A device whose MoonBase predates this change still answers only the old names, so an updated application handing over to it leaves the browser calling routes that image does not have. The way through is the same as any MoonBase update: flash both images over serial once ([building.md](../how-to/building.md#flashing-a-running-device-over-the-network)). A device flashed serially from this version on is consistent and needs nothing. -### `soundReactive` is now `audioReactive` - -**Action: re-set one control.** Affects Fish Tank, Flying Toasters, Pacman, Pong, Space Invaders, -Sprite Fountain and MovingHead, if you had turned the control on. - -One name for one thing: the service is `AudioService`, the frame is `AudioFrame`, the effects are audio-reactive. The control that made a sprite effect follow the music was the last place still calling it sound, so it is renamed rather than left as the odd one out. - -A restored config maps the old name to the new one and carries its value. On a device upgraded in place the control returns to its default (off); switch it back on where you had it. - ### AudioVolume is gone **Action: pick another effect.** Affects any device with an AudioVolume effect on a layer. -It drew one bar from the audio level, which every audio-reactive effect does as a side effect of what it actually draws. There is no successor to map it onto, so a restored config carrying an `AudioVolumeEffect` node finds no such type and the layer comes up without it. `GEQ` is the nearest thing if a literal meter is what you want. - -### The Firmware card describes one image at a time - -**Action: none.** Affects nothing a user has set: every control involved is read-only. - -`firmwarePartition` is now `partition`, and `update_pct` is gone (an install's progress belongs in the overlay the UI raises while it runs, not in a row that sits at zero for the life of a device that is not mid-install). Where a device carries two images, a new `image` control selects whether those rows describe the running app or MoonBase in the factory slot. - -### Noise2D is gone; Noise renders it - -**Action: re-set one control.** Affects any device with a Noise2D effect on a layer. - -The two noise effects were one effect with two names: `Noise` is `Dim::D3` and draws the identical field on a panel, so the 2D variant earned nothing. A restored config maps `Noise2DEffect` to `NoiseEffect` and carries `scale` across. - -What does not carry is `speed`. Noise2D took a 0..15 divisor of its own; Noise takes its rate from `bpm` on the shared beat clock, so there is no value to map onto. Set `bpm` to taste after restoring. - -### Infrared is a list of learned rows, and the remote must be re-learned - -**Action: re-learn the remote.** Affects any device with a configured infrared service. - -`IrService` becomes `InfraredService`, rebuilt around rows: a row learns a code and points it at any `Module.control`, where the old module carried five fixed actions (`code on/off`, `code brightness up`, and so on) each bound to one predetermined behavior. The module itself carries over through [migrate.js](https://github.com/MoonModules/projectMM/blob/main/src/ui/migrate.js)'s type map, so it does not vanish from the tree, but the codes it held have no equivalent: a learned code used to be a control's value, and is now a row. Press the remote's keys again against the rows you want. - -Restoring a backup taken before the change reports the rename and flags the module for review rather than silently dropping it. A device upgraded WITHOUT restoring a backup keeps its infrared module and loses the codes. - -### The desktop build keeps its files in `build/fs`, not `build` - -**Action: move your data, or lose your settings.** Affects the DESKTOP build only, and only a -developer running it from a repository checkout; devices are unaffected. - -A desktop install used the build directory itself as the device's filesystem, so the File Manager's root listed CMake caches, object archives and every ESP32 variant's build folder alongside the four directories a device actually has. It now roots at `build/fs`, so what the desktop shows is what a board shows. - -An existing checkout starts with an empty-looking device, because its `.config` is one level up. -Move what you want to keep: - -```sh -mkdir -p build/fs -mv build/.config build/moonlive build/.hls build/fs/ 2>/dev/null -``` - -Nothing is deleted if you skip this: the old directories stay where they are, and the device simply starts fresh. `MM_DATA_DIR` still overrides the location, and a packaged desktop install (which uses the per-user data directory) is unchanged. - -### projectMM no longer appears in WLED apps by default - -Device discovery now announces on the multicast group `239.255.77.77` and, by default, **not** on the broadcast address WLED apps and devices browse. A projectMM device therefore stops showing up in them until you turn on `wledCompatible` in the Devices module. - -projectMM devices still find each other either way: presence always goes to the group and every device always joins it, so a fleet can mix the setting freely. - -The reason for the default: a broadcast at discovery cadence makes every phone, printer and laptop on the LAN take an interrupt and parse a packet none of them want. Multicast reaches only the devices that joined the group. See [multicast and IGMP snooping](../explanation/architecture/moonlight.md#multicast-and-igmp-snooping) for when that saving is real (a switch that snoops) and when it is not. +It drew one bar from the audio level, which every audio-reactive effect does as a side effect of what it draws. There is no successor to map it onto, so a restored config carrying an `AudioVolumeEffect` node finds no such type and the layer comes up without it. `GEQ` is the nearest thing if a literal meter is what you want. ### A light preset's Dimmer channel is now driven @@ -178,176 +121,51 @@ The dimmer is now held open (255) every frame, with per-light brightness staying Routing brightness to the dimmer channel rather than holding it open is the better model and is [backlogged](../work/future/backlog-light.md), so this value will change again. +### WLED apps find a device only when you ask them to -### esp32-16mb moves to the MoonBase partition table (2026-08-28) +Device discovery now announces on the multicast group `239.255.77.77` and, by default, **not** on the broadcast address WLED apps and devices browse. A MoonLight device therefore stops showing up in them until you turn on `wledCompatible` in the Devices module. -**Action: erase flash** (USB re-flash). Back up first (File Manager, or the installer's -bookmarklet on older firmware); restore after the install brings WiFi, config and scripts back. +MoonLight devices still find each other either way: presence always goes to the group and every device always joins it, so a fleet can mix the setting freely. -`esp32-16mb` replaces its dual-OTA layout with [MoonBase](../explanation/architecture/moonbase.md), the same trade the 4 MB variants made in the entry below, taken here by choice rather than necessity: the second app slot was idle except during an update, so the filesystem grows 7168 to 11264 KB and the device gains MoonBase's stronger recovery story (a power cut mid-install boots MoonBase and the user retries over the network). One app slot remains, at its full 4096 KB. - -Every partition moves, so the existing filesystem volume is not where the new table looks: -without a backup, WiFi credentials, module config and scripts all re-enter through provisioning. -A partition table only changes over USB, so an OTA update leaves a device on the old layout. - -### 4 MB boards move to the MoonBase partition table (2026-08-26) - -**Action: erase flash** (USB re-flash). Back up first (File Manager ⤓, or the installer's -bookmarklet on older firmware); restore after the install brings WiFi, config and scripts back. - -The 4 MB variants (`esp32`, `esp32-wrover`, `esp32-eth`) replace the dual-OTA layout with [MoonBase](../explanation/architecture/moonbase.md): the app slot grows 1856 → 2496 KB and the filesystem 256 → 548 KB, but the filesystem moves (0x3B0000 → 0x360000), so the existing volume is not where the new table looks; without a backup, WiFi credentials, module config and scripts all re-enter through provisioning. A partition table only changes over USB: a device still on the old table keeps OTA-updating *within* that table for as long as the app fits its 1856 KB slot; the web installer is the migration path. 8/16 MB boards are unaffected. +The reason for the default: a broadcast at discovery cadence makes every phone, printer and laptop on the LAN take an interrupt and parse a packet none of them want. Multicast reaches only the devices that joined the group. See [multicast and IGMP snooping](../explanation/architecture/moonlight.md#multicast-and-igmp-snooping) for when that saving is real (a switch that snoops) and when it is not. ### PreviewDriver's `fps` becomes `targetFps`, and now trades resolution (2026-08-25) The control is renamed and its meaning changed, so the rename is the point rather than cosmetic. -**Before:** `fps` was a ceiling. The driver never exceeded it, but a link that could not sustain the rate simply delivered fewer frames and the control did nothing about it. +**Before:** `fps` was a ceiling. The driver never exceeded it, but a link short of that rate delivered fewer frames and the control did nothing about it. **Now:** `targetFps` is the rate you *want*. The driver still never exceeds it, and when the link cannot keep up it **trades preview resolution** to get closer, lower it for full detail at a slower rate, raise it for a smoother but coarser preview. That makes the slider the place where you choose between detail and smoothness, which is what users were reaching for. **Action: none required.** The preview is a view, not output. A device that had a non-default `fps` saved falls back to the default 24 on first boot with this firmware, because the persisted key changed; set `targetFps` if you had tuned it. Mixed versions degrade soft: an old UI against new firmware sends no detail request and gets full detail (capped by memory); a new UI against old firmware sends an uplink message the device ignores. -### 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 -``` +### The desktop build keeps its files in `build/fs` -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. +**Action: move your data, or lose your settings.** Affects the DESKTOP build only, and only a +developer running it from a repository checkout; devices are unaffected. -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. +A desktop install used the build directory itself as the device's filesystem. The File Manager's root therefore listed CMake caches, object archives and every build folder alongside the four directories a device carries. It now roots at `build/fs`, so what the desktop shows is what a board shows. -**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. +An existing checkout starts with an empty-looking device, because its `.config` is one level up. +Move what you want to keep: -**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 behavior change: the overloads bind by exact reference type, so a call that compiles produces the widget it always did. +```sh +mkdir -p build/fs +mv build/.config build/moonlive build/.hls build/fs/ 2>/dev/null +``` +Nothing is deleted if you skip this: the old directories stay where they are, and the device starts fresh. `MM_DATA_DIR` still overrides the location, and a packaged desktop install (which uses the per-user data directory) is unchanged. ### 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. -Settings now live with the user: `%LOCALAPPDATA%\projectMM` on Windows, `~/Library/Application Support/projectMM` on macOS, and `$XDG_DATA_HOME/projectMM` on Linux, falling back to `~/.local/share/projectMM` when that is unset. `MM_DATA_DIR` overrides it. **A source checkout is unchanged** and still uses `build/.config`, so a development tree and every gate script behave exactly as before. +Settings now live with the user: `%LOCALAPPDATA%\projectMM` on Windows, `~/Library/Application Support/projectMM` on macOS, and `$XDG_DATA_HOME/projectMM` on Linux, falling back to `~/.local/share/projectMM` when that is unset. `MM_DATA_DIR` overrides it. **A source checkout stays in the tree**, under `build/fs` since the entry above moved it there, so its settings are at `build/fs/.config` and every gate script behaves as before. -**Action: *nothing*, unless your settings actually persisted before.** The old behavior had two modes, and only one of them leaves anything to move: +**Action: *nothing*, unless your settings persisted before.** The old behavior had two modes, and only one of them leaves anything to move: - **Saves were failing.** The log showed `write failed for /.config/...` on every change and nothing survived a restart. Nothing to carry across. - **Saves were succeeding, per folder.** They are in a `build/.config` folder beside wherever you launched from: the folder you unzipped into on Windows and Linux, and `~/build/.config` on macOS, because the `.app` launcher starts in your home directory. **Action: *move a folder*.** Move the `.config` directory itself into the new per-user directory, so it lands as `/.config` rather than spilling its files into the root. Or leave it and reconfigure from scratch. ESP32 is unaffected: LittleFS mounts at a fixed partition and never used this path. -### The `Layers` container is renamed to `Effects` (2026-08-08) - -The three top-level light containers are now **Layouts, Effects, Drivers**, L.E.D. The old name sat one character from its own child (`Layers` holding `Layer`s) and read as a near-twin of `Layouts`, which is the pair a newcomer actually has to tell apart. The tree is unchanged in shape: `Effects` → `Layer`s → effects and modifiers. - -**Action: *re-add a module* and *re-save presets*.** - -The type name is the persisted filename and the preset capture key, so two things do not survive the update: - -| What | Why | What to do | -|---|---|---| -| The saved light tree | The device looks for `/.config/Effects.json` and the old file is `Layers.json`, so the light tree boots empty | Re-add your Layer, effect and modifiers, then let it save | -| Presets that capture the look | A preset file records `"captures": "Layers"`, a name no module now answers to | Re-save each preset once the tree is rebuilt | - -A preset also records the ROLE it covers, and that role is now named after the container rather than after a module inside it: `"layer"` becomes `"effects"`. A preset carrying the old role still loads, but shows no tint on its pad until it is re-saved: the UI has no `layer` role to color it by. - -The child `Layer` keeps its name, as does everything under it. - - -### The `peripheral` options are renamed to name the peripheral, not the bus protocol (2026-07-30) - -The `peripheral` dropdown no longer says `i80` / `MoonI80`. "i80" is the Intel 8080 bus shape `esp_lcd` speaks, it is not a peripheral any ESP32 datasheet lists, and it matched nothing a user could look up: on the classic ESP32 that backend **is the I2S peripheral**, on the S3/P4/S31 it is the **LCD** peripheral. The new labels name the silicon block plus who drives it, which is the actual choice being made. - -| Old | New (classic ESP32) | New (S3 / P4 / S31) | -|---|---|---| -| `i80` | `I2S-IDF` | `LCD-IDF` | -| `MoonI80` | (not available) | `LCD-MM` | -| `Parlio` | — | `Parlio` (unchanged, it *is* the peripheral's name) | - -`-IDF` = driven through ESP-IDF's `esp_lcd`; `-MM` = driven by our own GDMA layer below it, which is what buys the streaming ring and the 74HCT595 pin expander. - -**Action: re-set the `peripheral` control**, but only on a device that already holds a persisted parallel driver AND had a non-default peripheral selected. The stored string no longer matches any option, so the loader falls back to the board's default backend; if that was already your choice, nothing changes. The web installer's board catalog ships the new names, so a fresh install or catalog re-inject is correct without action. - -### The three parallel LED drivers merge into one `ParallelLedDriver` with a `peripheral` selector (2026-07-23) - -`MultiPinLedDriver`, `MoonLedDriver`, and `ParlioLedDriver` are now one registered module, **`ParallelLedDriver`**, whose `peripheral` control picks which DMA peripheral drives the parallel WS2812 bus. They were always the same driver with a different bus backend; the merge makes that one card with a dropdown, offering only the peripherals the chip supports. - -| Old registered type | New | -|---|---| -| `MultiPinLedDriver` | `ParallelLedDriver` + `peripheral` = `i80` (esp_lcd: LCD_CAM on S3/P4, I2S on classic), renamed again below | -| `MoonLedDriver` | `ParallelLedDriver` + `peripheral` = `MoonI80` (own-GDMA below esp_lcd, LCD_CAM), renamed again below | -| `ParlioLedDriver` | `ParallelLedDriver` + `peripheral` = `Parlio` (P4) | - -**Action: re-add the driver.** A persisted module whose type is one of the three old names no longer resolves (the type isn't registered), so the robust loader drops it on boot, the driver, and its pins/settings, vanish from the tree. Add a **Parallel LED** driver again, choose the `peripheral` your board uses (the same backend the old type named, see the table), and re-enter its `pins` / `ledsPerPin` plus whatever the chosen peripheral needs: `i80` has `clockPin`/`dcPin`, `MoonI80` has `clockPin` + the ring/expander controls, `Parlio` has no clock or DC pins at all. The web installer's board catalog already names the new type, so a fresh install or a catalog re-inject wires it correctly; only a device carrying an OLD persisted tree needs the manual re-add. - -### The per-driver `preset` control is renamed to `lightPreset` (2026-07-23) - -**Action: nothing** on-device (the saved value survives, see the `lightPreset` [persistence contract](../moonmodules/light/drivers.md#parallel-led-details)). Only an external script or automation that POSTs the control by name (`/api/control` with `"control":"preset"`) must switch to `lightPreset`. - -### `AudioService`: the `sync` control becomes `mode` + `send audio`, and `simulate` is renumbered (2026-07-22) - -The audio module's identity is now a single `mode` control (Local audio / Receive network / Simulate), each showing only its own detail controls, replacing the separate `sync` (off / send / receive) toggle. Broadcasting the locally-analyzed frame moved to a `send audio` switch, meaningful only in Local mode. `simulate` was also renumbered, from a five-option list to two. - -| Old | New | -|---|---| -| control `sync` (Select: `off`/`send`/`receive`) | `mode` (Select: `local audio`/`receive network`/`simulate`) + `send audio` (a switch, Local mode only) | -| control `simulate` (Select, 5 options incl. a mic-fill-on-silence mode) | `simulate` (Select: 2 options), used only when `mode` is Simulate | - -**Action: re-set `mode` (and `send audio`) if you had `sync` on `send` or `receive`; re-set `simulate` if you had chosen a non-default option.** - -`sync` and the old `simulate` value read as absent → ignored, so `mode` takes its default (**Local audio**) and `send audio` its default (**off**). A device that was on `sync=receive` therefore comes up as Local audio, set `mode` to Receive network again. One that broadcast (`sync=send`) comes up not broadcasting, turn `send audio` on. The five-option `simulate` collapsed to two, so a device on one of the dropped options (e.g. the mic-fill-on-silence mode, a removed capability) takes the new default; re-pick if needed. Receive network and every sync control exist only on network-capable targets. - -### `MoonLedDriver`: `forceRing` → `useRing`, and the ring's geometry is now settable (2026-07-17) - -The pin-expander path selector was a three-option Select (`auto` / `ring` / `wholeFrame`) named for a *diagnostic override*. The auto-router is gone, at the size the expander exists for (48 strands × 256 lights) a whole frame never fits internal DMA RAM, so "auto" had exactly one right answer while presenting itself as a choice, and its silent fallback hid which path was actually running. What remains is the honest question, as a switch: - -| Old | New | -|---|---| -| control `forceRing` (Select: `auto`/`ring`/`wholeFrame`) | `useRing` (a switch: on = ring, off = whole frame) | -| — | `ringRows` (new: lights per DMA buffer, 1..64) | -| — | `ringBufs` (new: buffers the DMA circulates, 2..32) | - -**Action: re-set `useRing` if you had `forceRing` on `wholeFrame`.** - -`forceRing` reads as absent → ignored, and `useRing` takes its default (**on**, the ring). A device that had explicitly selected whole-frame therefore comes up on the ring; flip `useRing` off to get it back. `ringRows`/`ringBufs` default to 16 and 12, the geometry the driver effectively ran. (It shipped with a pool of 16, but 16 buffers never fit the S3's internal DMA heap, so the ring build failed its own fit check and the driver quietly fell back to whole-frame; 12 is what actually held. A config on the old defaults may therefore start *ringing* where it used to fall back.) They exist so the RAM / encode-overhead / interrupt-rate / lap-time trade-off can be swept on a live board rather than fixed at compile time. - -### LED driver + control rename: a human-readable UI (2026-07-16) - -The LED driver module types and several controls were renamed so the UI reads in plain language rather than peripheral jargon (the UI shows a control's name verbatim, so the name *is* the label). - -| Old | New | -|---|---| -| module type `I80LedDriver` | `MultiPinLedDriver` | -| module type `MoonI80LedDriver` | `MoonLedDriver` | -| control `shiftRegister` | `pinExpander` | -| control `asyncTransmit` | `doubleBuffer` | -| read-only `wireUs` | `frameTime` | -| read-only `stall` (Drivers) | `renderWait` | - -**Action: re-add the module, then re-set `pinExpander` / `doubleBuffer` if you had changed them.** - -A device whose persisted config names the old module type loads a module type that no longer exists, the unknown type is ignored, so **the driver is absent from the tree on boot**. Re-add a **Parallel LED** driver (the single type the two later merged into, see the 2026-07-23 entry above for the `peripheral` value that matches the old `I80LedDriver` / `MoonI80LedDriver`) and re-enter its controls. Within a re-added driver, the two renamed *settable* controls (`pinExpander`, `doubleBuffer`) read as absent → they take their defaults (`pinExpander` off, `doubleBuffer` on); set them again if your board needs otherwise. `frameTime` and `renderWait` are read-only KPIs, nothing to restore. - -This rename left `RmtLedDriver` untouched, and `ParlioLedDriver` untouched *at the time*; the later 2026-07-23 entry above then merges `ParlioLedDriver` into `ParallelLedDriver` along with the other two. The `pins` / `ledsPerPin` / `clockPin` / `latchPin` / `loopback*` controls are unchanged by this rename. - ---- - -## Earlier - -These pre-date this log. A device that persisted state on an older build and loads a newer one loses only the noted value, which re-populates on next use. - -### UI last-selected module (`mm.selectedModule` → `mm_selected`) - -The browser localStorage key for the UI's last-selected module. - -**Action: nothing.** Lost: the remembered selection resets to the first module. - -### Device-list `color` → `color` (US-spelling rename) - -The DevicesModule persisted-list key for a Hue bridge's color-capable light count (`DevicesModule::restoreList()`). A device list persisted under the old key reads the count as absent → 0. - -**Action: nothing.** The cached bridge count resets to 0 until the bridge is re-heard live and re-populates it. diff --git a/docs/reference/hardware/control-surfaces.md b/docs/reference/hardware/control-surfaces.md index 0fc577f2..583fd05f 100644 --- a/docs/reference/hardware/control-surfaces.md +++ b/docs/reference/hardware/control-surfaces.md @@ -1,6 +1,6 @@ # Control surfaces: hardware reference -What projectMM needs to know about the physical desks on the bench, so a control-ingest plan can be written from facts rather than from a product page. A desk here is a candidate source for [ControlModule](../../moonmodules/core/system.md#control)'s pads, encoders and faders, which were laid out to match this class of hardware in the first place. +What MoonLight needs to know about the physical desks on the bench, so a control-ingest plan can be written from facts rather than from a product page. A desk here is a candidate source for [ControlModule](../../moonmodules/core/system.md#control)'s pads, encoders and faders, which were laid out to match this class of hardware in the first place. **The headline, because it contradicts the obvious assumption:** neither desk speaks OSC. Both are **Mackie Control** surfaces. OSC is the right protocol for the wider ecosystem (Resolume, @@ -57,9 +57,9 @@ Enough to judge the size of the job. MCU is ordinary MIDI carrying agreed meanin | Scribble strips | SysEx, `0x12` after the header, then the text | **It is bidirectional by nature, and that is the point of the hardware.** The motors only move -because the host sends fader positions back; the scribble strips only show anything because the host writes them. A projectMM implementation that only *reads* the desk would work, but would waste what makes these desks worth owning: a preset change should move the faders and relabel the strips. +because the host sends fader positions back; the scribble strips only show anything because the host writes them. A MoonLight implementation that only *reads* the desk would work, but would waste what makes these desks worth owning: a preset change should move the faders and relabel the strips. -## What this means for projectMM +## What this means for MoonLight Three routes, in increasing cost: @@ -68,4 +68,4 @@ Three routes, in increasing cost: 2. **A bridge.** An existing MCU-to-OSC translator on a laptop turns either desk into an OSC source. No firmware work; costs a machine in the rack, which a festival podium may already have. 3. **RTP-MIDI + MCU in firmware.** Reaches the X-Touch over Ethernet with no host machine, and is - the only route that drives the motors from projectMM directly. Needs RFC 6295 (session handshake, journalling) plus the MCU semantics above, both directions. Does not help the QCon, which has no network port. + the only route that drives the motors from MoonLight directly. Needs RFC 6295 (session handshake, journalling) plus the MCU semantics above, both directions. Does not help the QCon, which has no network port. diff --git a/docs/reference/hardware/esp32-s31-coreboard.md b/docs/reference/hardware/esp32-s31-coreboard.md index a482cd2c..7189a94e 100644 --- a/docs/reference/hardware/esp32-s31-coreboard.md +++ b/docs/reference/hardware/esp32-s31-coreboard.md @@ -1,6 +1,6 @@ # ESP32-S31 Function-CoreBoard-1 hardware reference -Pin maps and onboard features for the Espressif **ESP32-S31 Function-CoreBoard-1**, read from the official schematic so projectMM work (Ethernet, audio, SD, USB-host) reads this instead of re-scraping the PDF. The board is the bench S31 (`esp32s31` firmware). +Pin maps and onboard features for the Espressif **ESP32-S31 Function-CoreBoard-1**, read from the official schematic so MoonLight work (Ethernet, audio, SD, USB-host) reads this instead of re-scraping the PDF. The board is the bench S31 (`esp32s31` firmware). **Sources** - Schematic (rev C, 2026-05-13): @@ -57,7 +57,7 @@ Pin map, **bench-verified** (link + DHCP confirmed on the CoreBoard). The RGMII | | | | ETH_RXD1 | 18 | | | | | ETH_RXD0 | 19 | -> **RGMII, not RMII.** projectMM's classic/P4 Ethernet is RMII (fewer data lines, 50 MHz ref clock); +> **RGMII, not RMII.** MoonLight's classic/P4 Ethernet is RMII (fewer data lines, 50 MHz ref clock); > the S31's 1 Gbps EMAC is RGMII (4-bit data each way + TX/RX clocks). The shared `ethInitEmac()` in > `src/platform/esp32/platform_esp32.cpp` drives both: an `#ifdef CONFIG_IDF_TARGET_ESP32S31` block > selects the RGMII interface and sets the CoreBoard's data/clock pins from the table above, then diff --git a/docs/reference/hardware/gpio-usage.md b/docs/reference/hardware/gpio-usage.md index c786e934..66a78575 100644 --- a/docs/reference/hardware/gpio-usage.md +++ b/docs/reference/hardware/gpio-usage.md @@ -1,6 +1,6 @@ # GPIO usage per MCU — hardware reference -Which GPIOs are safe to wire a mic, an LED strand, or an Ethernet PHY to on each MCU projectMM supports, so a bench build picks a pin from here instead of re-scraping the datasheet. This is **chip**-level (which GPIOs the silicon reserves or role-assigns); a specific product's *board* wiring is its catalog entry in [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json), and the one fully-mapped board is the [ESP32-S31 coreboard](esp32-s31-coreboard.md). +Which GPIOs are safe to wire a mic, an LED strand, or an Ethernet PHY to on each MCU MoonLight supports, so a bench build picks a pin from here instead of re-scraping the datasheet. This is **chip**-level (which GPIOs the silicon reserves or role-assigns); a specific product's *board* wiring is its catalog entry in [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json), and the one fully-mapped board is the [ESP32-S31 coreboard](esp32-s31-coreboard.md). Each chip lists two kinds of pin to avoid: @@ -86,4 +86,4 @@ For the S31 coreboard, take pins from the coreboard reference's "free" set rathe ## The rule behind the defaults -projectMM leaves peripheral pins **unset** by default and lets the deviceModel catalog fix them only where the *product* wires them (the [Defaults rule](../../contributing/coding-standards.md#defaults)): a board-soldered PHY or codec defaults its pins; a user-soldered mic or LED strand stays unset so a guess can't drive a pin the user committed elsewhere. This table is what to consult when choosing that user pin, or when writing a new board's catalog entry. +MoonLight leaves peripheral pins **unset** by default and lets the deviceModel catalog fix them only where the *product* wires them (the [Defaults rule](../../contributing/coding-standards.md#defaults)): a board-soldered PHY or codec defaults its pins; a user-soldered mic or LED strand stays unset so a guess can't drive a pin the user committed elsewhere. This table is what to consult when choosing that user pin, or when writing a new board's catalog entry. diff --git a/docs/reference/hardware/light-fixtures.md b/docs/reference/hardware/light-fixtures.md index c4bb5804..ac7b2d82 100644 --- a/docs/reference/hardware/light-fixtures.md +++ b/docs/reference/hardware/light-fixtures.md @@ -24,7 +24,7 @@ That is a DIFFERENT model: this one is 11/13 channels with a plain linear dimmer | DMX modes | 11 or 13 channels | | Display | `d001` address (001-512), `CH11` mode, `Au01` auto, `Snon` sound, `rPAN`/`rTIL` axis reverse | -### 11-channel mode (the projectMM preset) +### 11-channel mode (the MoonLight preset) | CH | Value | Function | Preset role | |---|---|---|---| @@ -46,13 +46,13 @@ Two things matter when driving it by hand. Every channel at 255 makes the fixtur ### 13-channel mode (the alternative) -The same first 11 channels, plus CH12 (0-249 auto run, 250-255 sound mode) and CH13 (150-200 reset). Both are program modes that take control away from DMX, so the 11-channel mode is the one worth driving from projectMM. +The same first 11 channels, plus CH12 (0-249 auto run, 250-255 sound mode) and CH13 (150-200 reset). Both are program modes that take control away from DMX, so the 11-channel mode is the one worth driving from MoonLight. ## P-Knight Art-Net2 CR021R ![P-Knight Art-Net2 CR021R](../../assets/light/fixtures/pknight-artnet2-cr021r.jpg){ width="280" } -A two-universe Art-Net to DMX512 node: Ethernet in, XLR DMX out. It is how projectMM drives a wired fixture, the counterpart to the LED drivers that speak to addressable strips directly. +A two-universe Art-Net to DMX512 node: Ethernet in, XLR DMX out. It is how MoonLight drives a wired fixture, the counterpart to the LED drivers that speak to addressable strips directly. | | | |---|---| diff --git a/docs/reference/hardware/mhc-wled-esp32-p4-shield.md b/docs/reference/hardware/mhc-wled-esp32-p4-shield.md index 1152b2fb..3387bcaf 100644 --- a/docs/reference/hardware/mhc-wled-esp32-p4-shield.md +++ b/docs/reference/hardware/mhc-wled-esp32-p4-shield.md @@ -1,6 +1,6 @@ # MHC-WLED ESP32-P4 shield — hardware reference -Terminal pinout and onboard features for the **MHC-WLED ESP32-P4 shield** (myhome-control), the P4-NANO carrier used on the bench (catalog `deviceModel: "MHC-WLED ESP32-P4 shield"`, `esp32p4rev1-eth` firmware). Read from the board silkscreen + the builder's schematics so projectMM work reads this instead of the marketing render. The shield sits on a **Waveshare ESP32-P4-NANO**; GPIO numbers are the P4's. +Terminal pinout and onboard features for the **MHC-WLED ESP32-P4 shield** (myhome-control), the P4-NANO carrier used on the bench (catalog `deviceModel: "MHC-WLED ESP32-P4 shield"`, `esp32p4rev1-eth` firmware). Read from the board silkscreen + the builder's schematics so MoonLight work reads this instead of the marketing render. The shield sits on a **Waveshare ESP32-P4-NANO**; GPIO numbers are the P4's. > **Board revision:** the terminal map and RS-485 wiring below are transcribed from a **V1** board (the builder's labeled V1 photos + schematics). The overview render is a **V2** render. Whether V2 keeps the identical GPIO↔terminal wiring is **not confirmed here**, so treat the map as V1-specific and verify against your own board's silkscreen if you have a different revision. @@ -90,4 +90,4 @@ The loopback self-test drives a WS2812 frame out one pin and reads it back on a ## Cross-reference -Chip-level GPIO constraints (straps, flash/PSRAM) for the P4 are in [gpio-usage.md § ESP32-P4](gpio-usage.md#esp32-p4); this page is the *board* wiring. The catalog entry is [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json) (`MHC-WLED ESP32-P4 shield`). RS-485 / DMX-512 as a first-class projectMM output is tracked in the [RS-485 / DMX-512 wired-output backlog item](../../work/future/backlog-light.md#rs-485-dmx-512-wired-output-future-the-physical-dmx-driver). +Chip-level GPIO constraints (straps, flash/PSRAM) for the P4 are in [gpio-usage.md § ESP32-P4](gpio-usage.md#esp32-p4); this page is the *board* wiring. The catalog entry is [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json) (`MHC-WLED ESP32-P4 shield`). RS-485 / DMX-512 as a first-class MoonLight output is tracked in the [RS-485 / DMX-512 wired-output backlog item](../../work/future/backlog-light.md#rs-485-dmx-512-wired-output-future-the-physical-dmx-driver). diff --git a/docs/reference/metrics/docgen.md b/docs/reference/metrics/docgen.md index bd897e1f..98aa5d03 100644 --- a/docs/reference/metrics/docgen.md +++ b/docs/reference/metrics/docgen.md @@ -4,7 +4,7 @@ Generated by [`moondeck/check/check_docgen.py`](../../../moondeck/check/check_do Every place the generated documentation breaks the shape [the standards](../../contributing/documentation-standards.md#the-card) define. Current state only: the trend is this file's git history. The list only shrinks. -**0 error(s)** and **3090 warning(s)** across 215 page(s). +**0 error(s)** and **2918 warning(s)** across 207 page(s). An error is in a file that generates a documentation page, a header or a catalog page, so the finding is a defect in what gets published and it fails the gate. A warning is in an implementation file, which publishes nothing: its comments are a note to the next reader, worth fixing without being worth stopping a commit for. Both are counted here, because a warning nobody sees is a warning nobody fixes. @@ -14,11 +14,11 @@ The split is temporary. It stages the sweep rather than ranking the two kinds of | Rule | Errors | Warnings | |---|---:|---:| -| over-wide comment lines | 0 | 2192 | -| multi-line comment blocks | 0 | 596 | -| over-long sentences | 0 | 108 | -| over-wide doc lines | 0 | 102 | -| member deep dives | 0 | 92 | +| over-wide comment lines | 0 | 2088 | +| multi-line comment blocks | 0 | 542 | +| over-long sentences | 0 | 103 | +| over-wide doc lines | 0 | 96 | +| member deep dives | 0 | 89 | ## By documentation area @@ -26,9 +26,9 @@ The unit a sweep runs in: one summary page and the headers it owns, as the [hier | Summary page | Errors | Warnings | |---|---:|---:| -| `(tests, no card)` | 0 | 2203 | +| `(tests, no card)` | 0 | 2111 | | `platform/index.md` | 0 | 477 | -| `core/system.md` | 0 | 410 | +| `core/system.md` | 0 | 330 | ## Where the work is @@ -36,7 +36,7 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul ### (tests, no card) -**0 error(s)** and **2203 warning(s)** across 184 file(s). +**0 error(s)** and **2111 warning(s)** across 178 file(s). | Findings | File | | |---:|---|---| @@ -49,10 +49,10 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul | 48 | `test/unit/light/unit_Effects_gridsweep.cpp` | warning | | 44 | `test/unit/core/unit_moonlive_codegen_xtensa.cpp` | warning | | 42 | `test/unit/light/unit_ParallelSlots.cpp` | warning | -| 41 | `test/scenario_runner.cpp` | warning | -| 1-41 each | *174 more warning files, 1537 findings* | | +| 41 | `test/unit/core/unit_ControlModule.cpp` | warning | +| 1-38 each | *168 more warning files, 1445 findings* | | -By rule, warnings: 1668 over-wide comment lines, 239 multi-line comment blocks, 106 over-long sentences, 101 over-wide doc lines, 89 member deep dives. +By rule, warnings: 1600 over-wide comment lines, 229 multi-line comment blocks, 101 over-long sentences, 95 over-wide doc lines, 86 member deep dives. ### platform/index.md @@ -76,18 +76,16 @@ By rule, warnings: 330 over-wide comment lines, 147 multi-line comment blocks. ### core/system.md -**0 error(s)** and **410 warning(s)** across 8 file(s). +**0 error(s)** and **330 warning(s)** across 6 file(s). | Findings | File | | |---:|---|---| | 197 | `src/core/system/HttpServerModule.cpp` | warning | -| 46 | `src/core/system/MqttModule.cpp` | warning | | 40 | `src/core/system/FilesystemModule.cpp` | warning | | 39 | `moonbase/main/moonbase_main.cpp` | warning | -| 34 | `src/main.cpp` | warning | | 26 | `src/core/module/Control.cpp` | warning | | 19 | `src/core/module/Scheduler.cpp` | warning | | 9 | `src/core/system/FileManagerModule.cpp` | warning | -By rule, warnings: 210 multi-line comment blocks, 194 over-wide comment lines, 3 member deep dives, 2 over-long sentences, 1 over-wide doc lines. +By rule, warnings: 166 multi-line comment blocks, 158 over-wide comment lines, 3 member deep dives, 2 over-long sentences, 1 over-wide doc lines. diff --git a/docs/reference/metrics/repo-health.json b/docs/reference/metrics/repo-health.json index e53c733e..224d551e 100644 --- a/docs/reference/metrics/repo-health.json +++ b/docs/reference/metrics/repo-health.json @@ -1,9 +1,9 @@ { - "commit": "09e28ac6", + "commit": "ccfe4ec1", "flash": { - "esp32s3-n16r8": 2134032, - "desktop": 1991400, - "esp32": 2085360, + "esp32s3-n16r8": 2142976, + "desktop": 1992392, + "esp32": 2091120, "esp32p4rev1-eth": 2045840, "esp32p4rev1-eth-wifi": 2331904, "esp32s3-n8r8": 2087168, @@ -19,10 +19,10 @@ "measured": { "esp32p4rev1-eth": "2026-09-22", "esp32s31": "2026-09-22", - "esp32": "2026-09-22", + "esp32": "2026-09-25", "esp32-pico": "2026-09-09", - "esp32s3-n16r8": "2026-09-21", - "desktop": "2026-09-22", + "esp32s3-n16r8": "2026-09-25", + "desktop": "2026-09-25", "esp32s3-n8r8": "2026-09-08", "esp32s3-zero": "2026-09-08", "esp32-16mb": "2026-09-09", @@ -31,20 +31,20 @@ }, "perf": { "desktop": { - "tick_us": 180, - "fps": 5555, + "tick_us": 1, + "fps": 1000000, "scenario_p50": { - "Layer_base_pipeline": { - "p50": 69, - "p95": 74, - "n": 32, - "last": "2026-09-22" + "Effects_pipeline_builds_and_renders": { + "p50": 8, + "p95": 19, + "n": 25, + "last": "2026-09-25" }, - "Layer_memory_1to1": { - "p50": 5, - "p95": 24, - "n": 32, - "last": "2026-09-21" + "Layouts_resize_reallocates_live": { + "p50": 66, + "p95": 94, + "n": 8, + "last": "2026-09-24" } } }, @@ -53,665 +53,63 @@ "fps": 119 }, "scenario_matrix": { - "MoonModule_control_change": { - "desktop-macos": { - "p50": 127, - "p95": 139, - "n": 32, - "last": "2026-09-21" - }, - "esp32-eth-wifi": { - "p50": 89895, - "p95": 89895, - "n": 1, - "last": "2026-06-02" - }, - "esp32-eth": { - "p50": 111731, - "p95": 111731, - "n": 1, - "last": "2026-06-02" - }, - "esp32": { - "p50": 212, - "p95": 212, - "n": 1, - "last": "2026-07-24" - }, - "desktop-windows": { - "p50": 262, - "p95": 262, - "n": 1, - "last": "2026-08-21" - }, - "esp32p4rev1-eth": { - "p50": 165, - "p95": 165, - "n": 1, - "last": "2026-06-17" - }, - "esp32s3-n16r8": { - "p50": 166, - "p95": 166, - "n": 1, - "last": "2026-07-24" - } - }, - "MqttModule_haDiscovery_toggle": { - "esp32": { - "p50": 36, - "p95": 36, - "n": 1, - "last": "2026-07-24" - }, - "esp32s3-n16r8": { - "p50": 36, - "p95": 36, - "n": 1, - "last": "2026-07-24" - }, - "desktop-macos": { - "p50": 3, - "p95": 3, - "n": 1, - "last": "2026-08-08" - } - }, - "NetworkModule_eth_reconfigure": { - "esp32": { - "p50": 1169, - "p95": 1169, - "n": 1, - "last": "2026-07-24" - }, - "esp32s3-n16r8": { - "p50": 97843, - "p95": 97843, - "n": 1, - "last": "2026-07-24" - } - }, - "NetworkModule_mdns_toggle": { - "esp32-eth-wifi": { - "p50": 93963, - "p95": 93963, - "n": 1, - "last": "2026-06-02" - }, - "esp32-eth": { - "p50": 109767, - "p95": 109767, - "n": 1, - "last": "2026-06-02" - }, - "esp32": { - "p50": 36, - "p95": 36, - "n": 1, - "last": "2026-07-22" - }, - "esp32p4rev1-eth": { - "p50": 21, - "p95": 21, - "n": 1, - "last": "2026-06-17" - }, - "esp32s3-n16r8": { - "p50": 36, - "p95": 36, - "n": 1, - "last": "2026-07-24" - }, - "desktop-macos": { - "p50": 13, - "p95": 13, - "n": 1, - "last": "2026-08-08" - } - }, - "Audio_mutation": { - "desktop-macos": { - "p50": 22, - "p95": 41, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 40, - "p95": 40, - "n": 1, - "last": "2026-08-23" - }, - "esp32": { - "p50": 13152, - "p95": 15347, - "n": 4, - "last": "2026-09-01" - }, - "esp32s3-n16r8": { - "p50": 47, - "p95": 47, - "n": 1, - "last": "2026-07-24" - } - }, - "Aurora_fps": { - "desktop-macos": { - "p50": 1522, - "p95": 1951, - "n": 32, - "last": "2026-09-21" - } - }, - "Driver_mutation": { - "desktop-macos": { - "p50": 20, - "p95": 38, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 42, - "p95": 42, - "n": 1, - "last": "2026-08-23" - }, - "esp32": { - "p50": 12812, - "p95": 16772, - "n": 4, - "last": "2026-09-01" - }, - "esp32s3-n16r8": { - "p50": 39, - "p95": 39, - "n": 1, - "last": "2026-07-24" - } - }, - "Effects_composition": { - "desktop-macos": { - "p50": 145, - "p95": 184, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 549, - "p95": 549, - "n": 1, - "last": "2026-07-08" - } - }, - "Fields_polar_lut": { + "Firmware_reports_what_is_running": { "desktop-macos": { - "p50": 1263, - "p95": 1624, - "n": 32, - "last": "2026-09-21" + "p50": 38, + "p95": 68, + "n": 8, + "last": "2026-09-24" } }, - "Fluid_solver": { + "Drivers_output_and_brightness": { "desktop-macos": { - "p50": 217, - "p95": 250, - "n": 32, - "last": "2026-09-21" + "p50": 48, + "p95": 62, + "n": 9, + "last": "2026-09-24" } }, - "GridBlacks_blackpixel": { + "Effects_pipeline_builds_and_renders": { "desktop-macos": { - "p50": 2, - "p95": 2, - "n": 32, - "last": "2026-09-21" - }, - "esp32s3-n16r8": { - "p50": 267, - "p95": 267, - "n": 1, - "last": "2026-07-24" - }, - "esp32": { - "p50": 269, - "p95": 269, - "n": 1, - "last": "2026-07-24" - }, - "desktop-windows": { "p50": 8, - "p95": 8, - "n": 1, - "last": "2026-08-23" - } - }, - "GridLayout_resize": { - "desktop-macos": { - "p50": 120, - "p95": 142, - "n": 32, - "last": "2026-09-21" - }, - "esp32-eth-wifi": { - "p50": 82231, - "p95": 82231, - "n": 1, - "last": "2026-06-02" - }, - "esp32-eth": { - "p50": 95771, - "p95": 95771, - "n": 1, - "last": "2026-06-02" - }, - "esp32": { - "p50": 1352, - "p95": 1352, - "n": 1, - "last": "2026-07-24" - }, - "desktop-windows": { - "p50": 219, - "p95": 219, - "n": 1, - "last": "2026-07-08" - }, - "esp32p4rev1-eth": { - "p50": 1143, - "p95": 1143, - "n": 1, - "last": "2026-06-22" - }, - "esp32s3-n16r8": { - "p50": 1011, - "p95": 1011, - "n": 1, - "last": "2026-07-24" - } - }, - "Layer_base_pipeline": { - "desktop-macos": { - "p50": 69, - "p95": 74, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 118, - "p95": 118, - "n": 1, - "last": "2026-07-08" - } - }, - "Layer_memory_1to1": { - "desktop-macos": { - "p50": 5, - "p95": 24, - "n": 32, - "last": "2026-09-21" - }, - "desktop-windows": { - "p50": 1, - "p95": 1, - "n": 1, - "last": "2026-08-22" - } - }, - "Layouts_mutation": { - "desktop-macos": { - "p50": 93, - "p95": 104, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 111, - "p95": 111, - "n": 1, - "last": "2026-07-08" - }, - "esp32-eth": { - "p50": 27, - "p95": 27, - "n": 1, - "last": "2026-06-08" - }, - "esp32": { - "p50": 13692, - "p95": 16311, - "n": 4, - "last": "2026-09-01" - }, - "esp32s3-n16r8": { - "p50": 45, - "p95": 45, - "n": 1, - "last": "2026-07-24" - } - }, - "MoonLiveEffect_controls": { - "desktop-macos": { - "p50": 11, - "p95": 11, - "n": 1, - "last": "2026-08-28" - }, - "esp32s3-n16r8": { - "p50": 4624, - "p95": 4624, - "n": 1, - "last": "2026-07-24" - }, - "esp32": { - "p50": 12901, - "p95": 13473, - "n": 4, - "last": "2026-09-01" - } - }, - "MoonLiveEffect_livescript": { - "desktop-macos": { - "p50": 5, - "p95": 6, - "n": 32, - "last": "2026-09-22" - }, - "esp32s3-n16r8": { - "p50": 8255, - "p95": 8255, - "n": 1, - "last": "2026-07-24" - }, - "esp32p4rev1-eth": { - "p50": 11336, - "p95": 11336, - "n": 1, - "last": "2026-06-27" - }, - "esp32": { - "p50": 13433, - "p95": 13470, - "n": 3, - "last": "2026-09-01" - } - }, - "MoonLive_pipeline": { - "esp32s3-n16r8": { - "p50": 3278, - "p95": 3278, - "n": 1, - "last": "2026-08-20" - }, - "esp32s31": { - "p50": 11398, - "p95": 11398, - "n": 1, - "last": "2026-08-20" - }, - "unknown": { - "p50": 4393, - "p95": 4393, - "n": 1, - "last": "2026-08-20" - }, - "esp32": { - "p50": 9604, - "p95": 9604, - "n": 1, - "last": "2026-08-20" - }, - "desktop-macos": { - "p50": 5, - "p95": 5, - "n": 32, - "last": "2026-09-21" - }, - "desktop-windows": { - "p50": 1, - "p95": 1, - "n": 1, - "last": "2026-08-22" - } - }, - "MultiplyModifier_memory_lut": { - "desktop-macos": { - "p50": 3, - "p95": 3, - "n": 32, - "last": "2026-09-21" - }, - "desktop-windows": { - "p50": 3, - "p95": 3, - "n": 1, - "last": "2026-08-23" - } - }, - "MultiplyModifier_pipeline": { - "desktop-macos": { - "p50": 119, - "p95": 142, - "n": 32, - "last": "2026-09-21" - }, - "desktop-windows": { - "p50": 225, - "p95": 225, - "n": 1, - "last": "2026-07-08" - } - }, - "Trails_ladder": { - "desktop-macos": { - "p50": 358, - "p95": 658, - "n": 32, - "last": "2026-09-21" - } - }, - "modifier_chain": { - "desktop-macos": { - "p50": 43, - "p95": 46, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 69, - "p95": 69, - "n": 1, - "last": "2026-08-23" - }, - "esp32": { - "p50": 13337, - "p95": 14326, - "n": 5, - "last": "2026-09-01" - } - }, - "modifier_swap": { - "desktop-macos": { - "p50": 23, - "p95": 25, - "n": 32, - "last": "2026-09-22" - }, - "esp32-eth": { - "p50": 1010, - "p95": 1010, - "n": 1, - "last": "2026-06-08" - }, - "esp32p4rev1-eth": { - "p50": 362, - "p95": 362, - "n": 1, - "last": "2026-06-25" - }, - "esp32s3-n16r8": { - "p50": 354, - "p95": 354, - "n": 1, - "last": "2026-07-24" - }, - "esp32": { - "p50": 12250, - "p95": 14024, - "n": 4, - "last": "2026-09-01" - }, - "desktop-windows": { - "p50": 41, - "p95": 41, - "n": 1, - "last": "2026-08-22" + "p95": 19, + "n": 25, + "last": "2026-09-25" } }, - "perf_full": { + "Effects_swap_while_running": { "desktop-macos": { - "p50": 252, - "p95": 295, - "n": 32, - "last": "2026-09-22" - }, - "esp32s3-n16r8": { - "p50": 16915, - "p95": 16915, - "n": 1, - "last": "2026-07-24" - }, - "esp32": { - "p50": 10392, - "p95": 14887, - "n": 4, - "last": "2026-09-01" - }, - "esp32p4rev1-eth": { - "p50": 17433, - "p95": 17433, - "n": 1, - "last": "2026-06-25" - }, - "desktop-windows": { - "p50": 592, - "p95": 592, - "n": 1, - "last": "2026-08-23" - } - }, - "perf_light": { - "desktop-macos": { - "p50": 16, - "p95": 21, - "n": 32, - "last": "2026-09-22" - }, - "esp32s3-n16r8": { - "p50": 2485, - "p95": 2485, - "n": 1, - "last": "2026-07-24" - }, - "esp32": { - "p50": 2183, - "p95": 3671, - "n": 4, - "last": "2026-09-01" - }, - "esp32p4rev1-eth": { - "p50": 2038, - "p95": 2038, - "n": 1, - "last": "2026-06-25" - }, - "desktop-windows": { - "p50": 35, - "p95": 35, - "n": 1, - "last": "2026-08-23" - } - }, - "peripheral_grid_sweep": { - "esp32s31": { - "p50": 12273, - "p95": 12273, - "n": 1, - "last": "2026-07-25" - }, - "esp32p4rev1-eth": { - "p50": 11495, - "p95": 11495, - "n": 1, - "last": "2026-07-25" - }, - "desktop-macos": { - "p50": 254, - "p95": 324, - "n": 32, - "last": "2026-09-22" - }, - "desktop-windows": { - "p50": 649, - "p95": 649, - "n": 1, - "last": "2026-08-23" - }, - "esp32": { - "p50": 6991, - "p95": 7362, - "n": 3, - "last": "2026-09-01" + "p50": 48, + "p95": 100, + "n": 8, + "last": "2026-09-24" } }, - "peripheral_switch": { - "esp32s3-n16r8": { - "p50": 46, - "p95": 46, - "n": 1, - "last": "2026-07-24" - }, + "Layouts_resize_reallocates_live": { "desktop-macos": { - "p50": 4, - "p95": 4, - "n": 32, - "last": "2026-09-22" - }, - "esp32p4rev1-eth": { - "p50": 217, - "p95": 217, - "n": 1, - "last": "2026-07-24" - }, - "esp32": { - "p50": 437, - "p95": 11064, + "p50": 66, + "p95": 94, "n": 8, - "last": "2026-09-02" - }, - "desktop-windows": { - "p50": 9, - "p95": 9, - "n": 1, - "last": "2026-08-22" + "last": "2026-09-24" } } } }, "loc": { - "core": 22120, - "light": 30733, - "platform": 16771, - "ui": 11329, - "test": 56882, - "moondeck": 27247 + "core": 22174, + "light": 30956, + "platform": 16776, + "ui": 11360, + "test": 57352, + "moondeck": 27952 }, "comments": { "core": { - "lines": 5611, - "ratio": 0.28 + "lines": 5659, + "ratio": 0.282 }, "light": { - "lines": 7392, + "lines": 7456, "ratio": 0.27 }, "platform": { @@ -719,33 +117,33 @@ "ratio": 0.24 }, "ui": { - "lines": 3384, - "ratio": 0.315 + "lines": 3396, + "ratio": 0.316 }, "test": { - "lines": 6408, - "ratio": 0.131 + "lines": 6675, + "ratio": 0.135 }, "moondeck": { - "lines": 4578, - "ratio": 0.191 + "lines": 4673, + "ratio": 0.19 } }, "tests": { - "cases": 2099, - "scenarios": 27 + "cases": 2100, + "scenarios": 11 }, "docs": { - "md_files": 140, - "md_lines": 28611, + "md_files": 134, + "md_lines": 28056, "plans_files": 37, "backlog_lines": 3110, "lessons_lines": 526, "claude_md_lines": 281 }, "complexity": { - "functions": 3688, - "over_threshold": 276, + "functions": 3702, + "over_threshold": 277, "worst_ccn": 128 } } diff --git a/docs/reference/metrics/repo-health.md b/docs/reference/metrics/repo-health.md index 39df888f..8173e067 100644 --- a/docs/reference/metrics/repo-health.md +++ b/docs/reference/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `09e28ac6`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `ccfe4ec1`. 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/reference/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. @@ -8,19 +8,19 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Target | Flash | Capacity | Used | Built | |---|---:|---:|---:|:--:| -| desktop | 1,945 KB (+560 B) ⚠ | - | - | yes | -| esp32 | 2,036 KB | 2,496 KB | 82% | carried 0d | -| esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 13d** | -| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 11d** | -| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 13d** | +| desktop | 1,946 KB | - | - | yes | +| esp32 | 2,042 KB | 2,496 KB | 82% | yes | +| esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 16d** | +| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 14d** | +| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 16d** | | esp32-wrover | 1,801 KB | - | - | carried (age?) | -| esp32p4rev1-eth | 1,998 KB (+2 KB) ⚠ | 4,096 KB | 49% | yes | -| esp32p4rev1-eth-wifi | 2,277 KB | 4,096 KB | 56% | yes | +| esp32p4rev1-eth | 1,998 KB | 4,096 KB | 49% | carried 3d | +| esp32p4rev1-eth-wifi | 2,277 KB | 4,096 KB | 56% | carried 3d | | esp32p4rev3-eth | 1,605 KB | - | - | carried (age?) | -| esp32s3-n16r8 | 2,084 KB | 4,096 KB | 51% | carried 1d | -| esp32s3-n8r8 | 2,038 KB | 3,072 KB | 66% | **STALE 14d** | -| esp32s3-zero | 1,977 KB | 2,496 KB | 79% | **STALE 14d** | -| esp32s31 | 2,371 KB | 4,096 KB | 58% | carried 0d | +| esp32s3-n16r8 | 2,093 KB | 4,096 KB | 51% | yes | +| esp32s3-n8r8 | 2,038 KB | 3,072 KB | 66% | **STALE 17d** | +| esp32s3-zero | 1,977 KB | 2,496 KB | 79% | **STALE 17d** | +| esp32s31 | 2,371 KB | 4,096 KB | 58% | carried 3d | | qemu | 1,351 KB | - | - | carried (age?) | `Built: yes` was measured this run. `carried (age?)` was not rebuilt either and predates this record, so its age is unknown: it dates itself on the next build. `carried Nd` was NOT rebuilt and its number is N days old, so an absent delta says nothing about the change. **STALE** marks a carry older than 7 days: the number has gone unchecked long enough that growth will surface later as one jump, blamed on whichever commit happens to rebuild that target. `Used` is against the app slot in the firmware's own partition table. @@ -29,51 +29,29 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Target | Tick | FPS | |---|---:|---:| -| desktop | 180 µs (+28 µs) ⚠ | 5,555 (−1,023) ⚠ | +| desktop | 1 µs | 1,000,000 | | esp32 | 8,354 µs | 119 | ### Scenario tick by target (p50 of each sample window) -| Scenario | desktop-macos | desktop-windows | esp32 | esp32s3-n16r8 | esp32p4rev1-eth | esp32s31 | esp32-eth | esp32-eth-wifi | unknown | -|---|---|---|---|---|---|---|---|---|---| -| Audio_mutation | 22 | 40 ? | 13,152 | 47 ? | - | - | - | - | - | -| Aurora_fps | 1,522 | - | - | - | - | - | - | - | - | -| Driver_mutation | 20 | 42 ? | 12,812 | 39 ? | - | - | - | - | - | -| Effects_composition | 145 (+1) ⚠ | 549 ? | - | - | - | - | - | - | - | -| Fields_polar_lut | 1,263 | - | - | - | - | - | - | - | - | -| Fluid_solver | 217 | - | - | - | - | - | - | - | - | -| GridBlacks_blackpixel | 2 | 8 ? | 269 ? | 267 ? | - | - | - | - | - | -| GridLayout_resize | 120 | 219 ? | 1,352 ? | 1,011 ? | 1,143 ? | - | 95,771 ? | 82,231 ? | - | -| Layer_base_pipeline | 69 (−1) ✓ | 118 ? | - | - | - | - | - | - | - | -| Layer_memory_1to1 | 5 | 1 ? | - | - | - | - | - | - | - | -| Layouts_mutation | 93 | 111 ? | 13,692 | 45 ? | - | - | 27 ? | - | - | -| MoonLiveEffect_controls | 11 ? | - | 12,901 | 4,624 ? | - | - | - | - | - | -| MoonLiveEffect_livescript | 5 | - | 13,433 ? | 8,255 ? | 11,336 ? | - | - | - | - | -| MoonLive_pipeline | 5 | 1 ? | 9,604 ? | 3,278 ? | - | 11,398 ? | - | - | 4,393 ? | -| MoonModule_control_change | 127 | 262 ? | 212 ? | 166 ? | 165 ? | - | 111,731 ? | 89,895 ? | - | -| MqttModule_haDiscovery_toggle | 3 ? | - | 36 ? | 36 ? | - | - | - | - | - | -| MultiplyModifier_memory_lut | 3 | 3 ? | - | - | - | - | - | - | - | -| MultiplyModifier_pipeline | 119 | 225 ? | - | - | - | - | - | - | - | -| NetworkModule_eth_reconfigure | - | - | 1,169 ? | 97,843 ? | - | - | - | - | - | -| NetworkModule_mdns_toggle | 13 ? | - | 36 ? | 36 ? | 21 ? | - | 109,767 ? | 93,963 ? | - | -| Trails_ladder | 358 | - | - | - | - | - | - | - | - | -| modifier_chain | 43 | 69 ? | 13,337 | - | - | - | - | - | - | -| modifier_swap | 23 | 41 ? | 12,250 | 354 ? | 362 ? | - | 1,010 ? | - | - | -| perf_full | 252 (+1) ⚠ | 592 ? | 10,392 | 16,915 ? | 17,433 ? | - | - | - | - | -| perf_light | 16 | 35 ? | 2,183 | 2,485 ? | 2,038 ? | - | - | - | - | -| peripheral_grid_sweep | 254 | 649 ? | 6,991 ? | - | 11,495 ? | 12,273 ? | - | - | - | -| peripheral_switch | 4 | 9 ? | 437 | 46 ? | 217 ? | - | - | - | - | +| Scenario | desktop-macos | +|---|---| +| Drivers_output_and_brightness | 48 | +| Effects_pipeline_builds_and_renders | 8 | +| Effects_swap_while_running | 48 | +| Firmware_reports_what_is_running | 38 | +| Layouts_resize_reallocates_live | 66 | Microseconds. `?` marks a cell backed by fewer than 4 samples, which is a first impression rather than a percentile; several are months old and were captured during a network reconfigure, so they read as whole milliseconds. `-` means that target has never run that scenario. -**Coverage: 98/243 cells measured (40%), 32 of them with 4+ samples (13%).** The blanks are the point: a regression on a target that has never run a scenario cannot be DETECTED in it, and the target cannot be compared against the others. Filling the matrix means running the scenario suite on each bench board, which is a standing task rather than a one-off. +**Coverage: 5/5 cells measured (100%), 5 of them with 4+ samples (100%).** The blanks are the point: a regression on a target that has never run a scenario cannot be DETECTED in it, and the target cannot be compared against the others. Filling the matrix means running the scenario suite on each bench board, which is a standing task rather than a one-off. ### desktop: isolated scenarios (p50 of the sample window) | Scenario | p50 | p95 | n | |---|---:|---:|---:| -| Layer_base_pipeline | 69 µs (−1 µs) ✓ | 74 µs | 32 | -| Layer_memory_1to1 | 5 µs | 24 µs | 32 | +| Effects_pipeline_builds_and_renders | 8 µs | 19 µs | 25 | +| Layouts_resize_reallocates_live | 66 µs | 94 µs | 8 | These build a bare pipeline with no optional modules, so a change here is a change in the pipeline itself rather than in what was measured. A new module belongs in an advanced scenario, which keeps its own numbers. @@ -81,36 +59,36 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 22,120 (+30) ⚠ | 5,611 | 28.0 % (+0.1 %) ⚠ | -| light | 30,733 (+102) ⚠ | 7,392 | 27.0 % | -| platform | 16,771 (+148) ⚠ | 3,605 | 24.0 % (+0.3 %) ⚠ | -| ui | 11,329 | 3,384 | 31.5 % | -| test | 56,882 (+66) ⚠ | 6,408 | 13.1 % | -| moondeck | 27,247 | 4,578 | 19.1 % | +| core | 22,174 | 5,659 | 28.2 % | +| light | 30,956 | 7,456 | 27.0 % | +| platform | 16,776 | 3,605 | 24.0 % | +| ui | 11,360 | 3,396 | 31.6 % | +| test | 57,352 | 6,675 | 13.5 % | +| moondeck | 27,952 (+3) ⚠ | 4,673 | 19.0 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 2,099 (+3) ✓ | -| scenarios | 27 | +| unit cases | 2,100 | +| scenarios | 11 | ## Complexity | Metric | Value | |---|---:| -| functions | 3,688 (+9) ✓ | -| over threshold | 276 (+3) ⚠ | +| functions | 3,702 | +| over threshold | 277 | | worst CCN | 128 | ## Documentation | Metric | Value | |---|---:| -| markdown files | 140 (+1) ⚠ | -| markdown lines | 28,611 (+203) ⚠ | -| plan files | 37 (+5) ⚠ | -| backlog lines | 3,110 (+74) ⚠ | +| markdown files | 134 | +| markdown lines | 28,056 | +| plan files | 37 | +| backlog lines | 3,110 | | lessons lines | 526 | | CLAUDE.md lines | 281 | diff --git a/docs/reference/performance.md b/docs/reference/performance.md index f5c6f03f..01225270 100644 --- a/docs/reference/performance.md +++ b/docs/reference/performance.md @@ -1,6 +1,6 @@ # Performance & Memory -projectMM's per-step **performance contracts** live in the scenario JSONs: each `test/scenarios/*.json` step carries a per-target `contract` block (`tick_us` ceiling + `free_heap` floor) and an `observed` block (the latest reading per target). The scenarios are the source of truth and the assertion surface: every PR runs against them. See [testing.md § Performance contracts](testing.md#performance-contracts-contracttarget) for the contract semantics and renegotiation workflow. The headline numbers users care about are in [README.md § Performance](../README.md#performance). +MoonLight's per-step **performance contracts** live in the scenario JSONs: each `test/scenarios/*.json` step carries a per-target `contract` block (`tick_us` ceiling + `free_heap` floor) and an `observed` block (the latest reading per target). The scenarios are the source of truth and the assertion surface: every PR runs against them. See [testing.md § Performance contracts](testing.md#performance-contracts-contracttarget) for the contract semantics and renegotiation workflow. The headline numbers users care about are in [README.md § Performance](../README.md#performance). This document holds what scenarios can't carry: structural sizes (`sizeof`), build-variant deltas, and the WiFi/Ethernet physics that explain *why* a contract comes out where it does. @@ -447,4 +447,4 @@ So the modifier roughly **halves** the heavy tick at every grid (¼ logical area Per-target image size, capacity and headroom are generated every commit into [repo-health](metrics/repo-health.md#firmware-size), across all 14 firmware variants. -What that table cannot show is where the bytes go. On the default `esp32`, roughly a third of the image is the WiFi stack (`esp_wifi`, `wpa_supplicant`, `esp_phy`), then lwIP at ~180 KB, mbedTLS plus the Mozilla root bundle at ~170 KB, FreeRTOS and the IDF core at ~150 KB, and projectMM's own code at ~120 KB, about a tenth of the binary. That is why `esp32-eth` is the smaller build: excluding WiFi is the single largest saving available. Proportions shift with the IDF version and sdkconfig; measure with `idf.py -B build/esp32-esp32 size-components`. +What that table cannot show is where the bytes go. On the default `esp32`, roughly a third of the image is the WiFi stack (`esp_wifi`, `wpa_supplicant`, `esp_phy`), then lwIP at ~180 KB, mbedTLS plus the Mozilla root bundle at ~170 KB, FreeRTOS and the IDF core at ~150 KB, and MoonLight's own code at ~120 KB, about a tenth of the binary. That is why `esp32-eth` is the smaller build: excluding WiFi is the single largest saving available. Proportions shift with the IDF version and sdkconfig; measure with `idf.py -B build/esp32-esp32 size-components`. diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 9a6ca166..1c1a1d61 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -15,6 +15,10 @@ Three test categories, each with a clear purpose: - **In-process scenarios** (desktop, `test/scenarios/{core,light}/scenario_*.json`), exercise the system as an integrated pipeline. Each scenario is a declarative JSON file with a sequence of steps (`add_module`, `set_control`, `measure`) and optional performance bounds. The scenario runner (`test/scenario_runner.cpp`) replays the steps in-process and reports tick + heap per `measure` step. Same JSON files run against a live device through the HTTP API, that's the next tier. - **Live scenarios**: the same scenarios driven against a running device over REST. See [Live scenarios](#live-scenarios) below. +A live run is worth watching once, because it is the tier that proves the device rather than a model of it. The suite walks the cards in the order the interface lists them, and every step happens through the same API the page uses: + + + **Picking a tier for a new test.** When the behavior you want to pin only makes sense with modules wired together (e.g. "the pipeline reallocates cleanly when the grid resizes," "Drivers correctly hands the source buffer through after a child swap"), reach for a scenario first: that is what scenarios are *for*. When the behavior lives inside a single module (one function's contract, one edge case, one bug regression on a small surface), a unit test is the cheaper and faster fit. Don't extend the scenario runner with new predicates just to migrate an existing unit test, which is adding abstraction without an active need. Add predicates when a *new* scenario you're writing needs them. **Regression rule:** when a bug is found, the fix includes a new unit test or scenario that reproduces the bug. A comment in the test references the root cause so the connection stays traceable. @@ -82,9 +86,23 @@ A test written to pin a fix is shaped by that fix, so it agrees with the fix whe **The check is mechanical: after a test passes, break the thing it tests and confirm it fails.** Not for every test, but for any test written to pin a fix. +### A suite that stopped testing looks exactly like one that works + +Both scenario runners report a scenario that did not run and one that ran clean, and for a while they reported them the same way. +A skip returned the pass code, so ten of eleven scenarios skipped for a missing fixture while the summary read `11 passed`. +The three assertion bugs they would have caught stayed in for a commit. + +Two rules close that, and they are properties of the runner rather than of any scenario: + +- **A skip is counted as a skip.** The summary reads `N scenario(s), P passed, F failed, S skipped`, so a suite that stopped covering something says so. +- **A scenario that asserts nothing fails.** Zero checks is indistinguishable from every step silently doing nothing, so the runner fails it and names the step count. + ### A test that does not reproduce the user's conditions proves nothing -A green result means something only if the test could have gone red, and an agent's shell is a bad witness: it routinely runs with policies, permissions and paths no user has. A PowerShell script tested fine and would have shipped broken, because the agent's own shell had set `Process: Bypass` over the `RemoteSigned` a user actually has. A Defender false positive was declared cleared because a download succeeded, using a different client than the one still being blocked. +A green result means something only if the test could have gone red. +An agent's shell is a bad witness: it routinely runs with policies, permissions and paths no user has. +A PowerShell script tested fine and would have shipped broken, because the agent's own shell had set `Process: Bypass` over the `RemoteSigned` a user has. +A Defender false positive was declared cleared because a download succeeded, using a different client than the one still being blocked. **Name what would have to be true for the test to fail, and confirm that condition is present.** Print the setting the test depends on rather than inferring it from the outcome. @@ -196,12 +214,51 @@ Per-`TEST_CASE` description rules: - **One physical line above the `TEST_CASE`**, no hard-wrapping; the generator and MoonDeck handle layout. A second line is allowed only when the case does something genuinely non-obvious. - **Missing description** → the generator italicises the raw `TEST_CASE("…")` name in its place. +### Asserting a value, and what survives a restart + +Two ops exist for the things a measurement cannot see. + +**`expect_control`** asserts a control reads what the scenario says it must, and is the only op that fails a scenario on a value rather than on a timing contract: + +```json +{ "name": "the-prefix-is-what-ships", "op": "expect_control", "id": "Mqtt", "key": "topicPrefix", "equals": "MoonLight/563cfe" } +``` + +It exists for a string some other system keys on, where a change breaks a contract no compiler and no timing measurement can see. + +`not_equals` is the same assertion negated, for a value that moves every release where the only stable claim is a negative one: + +```json +{ "name": "a-version-is-reported", "op": "expect_control", "id": "Firmware", "key": "version", "not_equals": "" } +``` + +An empty string is what a read-only control renders when its source is missing. +So `not_equals: ""` asserts that a card reports anything at all, and it holds on every target and every release. + +Both tiers compare against the value as the API renders it, so `"equals": 180`, `"180"` and `true` all read the way a client would see them. +They reach it differently, which is worth knowing before asserting an unusual type. +In-process the runner calls `writeControlValue`, the serializer the HTTP layer uses; the live runner reads the parsed JSON back from `/api/modules/`. +The two agree on every scalar a control holds. +A `List` or a long `TextArea` is where they would part company, since the in-process render goes through a fixed buffer. +Assert those through a scalar the list drives rather than through the list itself. + +**`reboot`** restarts the device and waits for it to answer, so a later `expect_control` proves what survived rather than what is merely still in memory: + +```json +{ "name": "restart", "op": "reboot", "timeout": 60 }, +{ "name": "it-survived", "op": "expect_control", "id": "System", "key": "deviceName", "equals": "MM-Bench" } +``` + +On a board that is the reboot the endpoint performs. On a desktop the endpoint exits the process and nothing restarts it, so the live runner relaunches the binary with the data directory the exiting instance was using: a restart that came back on different files would prove nothing. In-process the op skips, because the scheduler is the process and exiting it would end the run. + ### Scenario modes (construct vs mutate) Every scenario carries a top-level `mode` field that says what shape the scenario expects the world to be in. Two values: - **`"mode": "construct"`**: the scenario builds the pipeline from an empty scheduler. Lots of `add_module` steps; the first `measure` happens after everything is wired. **Runs in-process only.** The live device's top-level shape is policy-fixed in `main.cpp` (see [src/core/HttpServerModule.cpp:639](../src/core/HttpServerModule.cpp#L639), `/api/modules` rejects top-level adds), so "build from scratch" can't happen on a live device without re-flashing. The live runner skips construct scenarios with a clear note. -- **`"mode": "mutate"`**: the scenario assumes a wired pipeline and tweaks it (`set_control` heavy). Runs in both tiers. The in-process runner replays an embedded **`fixture`** array (same shape as `steps`, but all `add_module`) that builds the same pipeline `main.cpp` does, then runs the actual steps. The live runner skips the fixture (device is its own fixture) and pre-flights that every id the steps touch is actually present on the device, a missing id is a hard fail, not a silent skip. +- **`"mode": "mutate"`**: the scenario assumes a wired pipeline and tweaks it (`set_control` heavy), and runs in both tiers. + The in-process runner replays an embedded **`fixture`** array (same shape as `steps`, but all `add_module`) that builds the same pipeline `main.cpp` does, then runs the steps. + The live runner skips the fixture (device is its own fixture) and pre-flights every id the steps touch: a missing id is a hard fail, not a silent skip. Picking the right mode: - If your scenario starts with empty Layouts/Layer/Drivers wiring, it's **construct**. It will not run live. @@ -209,7 +266,10 @@ Picking the right mode: A `mutate` scenario that needs platform-bound modules (Network mDNS, WiFi, OTA) the in-process runner can't honestly stand up should add `"live_only": true`. -**Bespoke convention.** The `mode` + `fixture` + `reset` trinity is projectMM-specific: no off-the-shelf BDD or scenario framework was borrowed wholesale. It exists because the same JSON has to serve both an in-process runner that owns the scheduler and a live runner that does not (main.cpp does). The closest analogs from widely recognized testing patterns: `fixture` ≈ xUnit fixtures (setup-once, replayed per scenario); `reset` ≈ SQL `BEGIN`/`ROLLBACK` (idempotent state restoration); `mode` ≈ pytest's parameterized execution modes (one test runs in different worlds). A future contributor who finds an off-the-shelf framework capturing this construct/mutate asymmetry is worth migrating to. +**Bespoke convention.** The `mode` + `fixture` + `reset` trinity is MoonLight-specific: no off-the-shelf BDD or scenario framework was borrowed wholesale. +It exists because the same JSON has to serve both an in-process runner that owns the scheduler and a live runner that does not (main.cpp does). +The closest analogs from widely recognized testing patterns: `fixture` ≈ xUnit fixtures (setup-once, replayed per scenario); `reset` ≈ SQL `BEGIN`/`ROLLBACK` (idempotent state restoration); `mode` ≈ pytest's parameterized execution modes. +A future contributor who finds an off-the-shelf framework capturing this construct/mutate asymmetry is worth migrating to. ### Reset block: idempotent scenarios @@ -228,7 +288,8 @@ Convention: reset every control your scenario writes, plus any production-defaul ### Performance contracts (`contract[]`) -Every measurable step carries a per-target `contract` block, the **performance contract** projectMM commits to delivering on that platform. The runner compares each measurement to the contract and fails if the device misses it. +Every measurable step carries a per-target `contract` block, the **performance contract** MoonLight commits to on that platform. +The runner compares each measurement to the contract and fails if the device misses it. ```json "contract": { @@ -323,7 +384,7 @@ Every `scenario_*.json` carries top-level metadata plus a `description` per step "fixture": [ { "name": "fix-layouts", "op": "add_module", "id": "Layouts", "type": "Layouts" }, { "name": "fix-grid", "op": "add_module", "id": "Grid", "type": "GridLayout", "parent_id": "Layouts", "props": {"width": 16, "height": 16} }, - { "name": "fix-layer", "op": "add_module", "id": "Layer", "type": "Layer", "props": {"layouts": "Layouts", "channelsPerLight": 3} }, + { "name": "fix-layer", "op": "add_module", "id": "Layer", "type": "Layer" }, { "name": "fix-noise", "op": "add_module", "id": "Noise", "type": "NoiseEffect", "parent_id": "Layer" }, { "name": "fix-mirror", "op": "add_module", "id": "Multiply", "type": "MultiplyModifier", "parent_id": "Layer" }, { "name": "fix-drivers", "op": "add_module", "id": "Drivers", "type": "Drivers", "props": {"layer": "Layer"} }, @@ -468,7 +529,7 @@ UI scenarios drive the web interface itself: a run file lists what a person does ```bash uv run moondeck/test/test_host.py --ui # the whole lane -uv run moondeck/uiscenario/uivideo.py --run test/uiscenarios/clips/add-a-layer.json +uv run moondeck/uiscenario/uivideo.py --run test/uiscenarios/clips/95-add-a-layer.json ``` The runs live in `test/uiscenarios/clips/`, the engine in `moondeck/uiscenario/`. Data under `test/`, runner under `moondeck/`: the same split the pipeline scenarios use. Tests are parameterized over the directory, so a new run file is a new test with nothing to wire up. Format and actions: [RUNS.md](../../moondeck/uiscenario/RUNS.md). diff --git a/docs/tutorials/build-your-own-moonmodules.md b/docs/tutorials/build-your-own-moonmodules.md index 5bac1a28..f296551b 100644 --- a/docs/tutorials/build-your-own-moonmodules.md +++ b/docs/tutorials/build-your-own-moonmodules.md @@ -1,6 +1,6 @@ # Build your own MoonModules -A hands-on guide to writing your own light **effects** (and later layouts, modifiers, and drivers) for projectMM. It's written for developers new to the codebase, including as a practical class in a school or workshop. If you can write a `for` loop in C++, you can write an effect. +A hands-on guide to writing your own light **effects** (and later layouts, modifiers, and drivers) for MoonLight. It's written for developers new to the codebase, including as a practical class in a school or workshop. If you can write a `for` loop in C++, you can write an effect. By the end you'll understand the one idea that makes modules easy here: **you write *what* your module does; the core decides *when* to run it.** You fill in a few functions; the engine handles lifecycle, threading, and memory timing, and calls your functions at the right moment. @@ -19,7 +19,7 @@ Each base just pre-fills the hooks specific to that job, so you fill in even les ## The big picture in one minute -A projectMM light show is a small tree of MoonModules: +A MoonLight light show is a small tree of MoonModules: ``` Layouts → where the LEDs are in space (a Grid, a sphere, a strip) diff --git a/docs/tutorials/first-light-show.md b/docs/tutorials/first-light-show.md index 5718dfdb..7a68a9f9 100644 --- a/docs/tutorials/first-light-show.md +++ b/docs/tutorials/first-light-show.md @@ -4,7 +4,7 @@ You have lights running and you know [what the cards are](how-projectmm-works.md Everything here happens in the device's own web interface, live. Nothing is compiled, nothing is saved and applied, nothing reboots. You change a number and the lights change while you are still holding the mouse. -You need a device with projectMM on it and some lights attached, real or previewed. A bare board with no strip works the whole way through: the 3D preview is what you will be watching anyway. +You need a device with MoonLight on it and some lights attached, real or previewed. A bare board with no strip works the whole way through: the 3D preview is what you will be watching anyway. ## 1. Say where the lights are @@ -12,7 +12,7 @@ Open **Layouts**. A fresh device has a **Grid**, which is the shape most rigs st Set **width** to 16 and **height** to 16. The preview reshapes as you type, and the light count under the card follows. - + That number is the whole point of a layout. Every effect downstream asks the layout how many lights there are and where each one sits, so this one card decides what the rest of the pipeline is painting on. Nothing else in the tree stores a size, which is why changing it here never leaves something stale behind. @@ -25,7 +25,7 @@ Open **Effects**. Under the Layer, press **+ add module** and pick an effect. ** Now change **numBalls** while it runs. Then **grav**. The lights respond as the slider moves, because an effect is not a rendered animation the device plays back: it is a function being run once per frame, reading its controls each time. - + Try a second effect. Press **+ add module** again and add **Ripples** beside the first. @@ -34,7 +34,7 @@ Both now run into the same Layer, in order, each writing over what the one befor Blending happens between **layers**. Press **+ add module** on the Effects card to add a second Layer, give it its own effect, and the Layer card carries a **blendMode** and an **opacity**. The drivers composite the layers bottom to top, so lowering the top layer's opacity tints what is underneath instead of replacing it. - + This is the same model an image editor uses, and it is worth a minute of play. Layers compose; you are not picking one effect from a list. @@ -44,7 +44,7 @@ Under the Layer, add a **modifier**: **Mirror**. The effect did not change. The modifier sits between the effect and the lights and folds the coordinates on the way through, so a pattern that ran across the whole grid now runs across half and reflects. - + That separation is why a modifier is worth having at all. Mirror, rotate and multiply are things you want on *any* effect, and writing them into each effect would be the same code many times over. diff --git a/docs/tutorials/first-script.md b/docs/tutorials/first-script.md index f5d4307e..2bff2927 100644 --- a/docs/tutorials/first-script.md +++ b/docs/tutorials/first-script.md @@ -28,7 +28,7 @@ class MyEffect { Click away from the editor, or press Ctrl/Cmd+S. The lights turn dim blue. - + That is the whole loop. `tick()` runs once per frame, `fill(r, g, b)` writes every light, and the numbers are 0 to 255. diff --git a/docs/tutorials/generative-effects.md b/docs/tutorials/generative-effects.md index b54c02eb..e2848fda 100644 --- a/docs/tutorials/generative-effects.md +++ b/docs/tutorials/generative-effects.md @@ -2,11 +2,11 @@ Most LED effects are written a light at a time: loop over the pixels, work out a color, write it. That works, and it takes a long time to get from "it lights up" to "I want to keep watching it". -This page is about the other way. projectMM ships a set of **power functions**: +This page is about the other way. MoonLight ships a set of **power functions**: the handful of algorithms that computer graphics has used for forty years to make things look alive. You do not implement them, you compose them. And because most of that vocabulary is reachable from **MoonLive** scripts as well as from compiled C++, a few dozen lines of script gets you an effect that would otherwise be a project. > Never opened the interface? Start with **[Install & first light](../gettingstarted.md)** -> and **[How projectMM works](how-projectmm-works.md)**, then come back. +> and **[How MoonLight works](how-projectmm-works.md)**, then come back. --- @@ -27,7 +27,7 @@ Fields also compose. Add two, and you get a third. Use one to bend another's coo Random numbers look like static. **Noise** looks like nature, because a noise value is close to its neighbors: it wanders instead of jumping. **Ken Perlin** published it in 1985 (*An Image Synthesizer*), after developing it -on the movie *Tron*; it later won him an Academy Award. projectMM uses his own revision, *improved noise* (SIGGRAPH 2002), which removes the directional bias of the original. +on the movie *Tron*; it later won him an Academy Award. MoonLight uses his own revision, *improved noise* (SIGGRAPH 2002), which removes the directional bias of the original. ``` int v = noise(x * 20, y * 20, div(t, 32)); // 0..255, smooth in every direction @@ -68,7 +68,7 @@ int v = fbm(x * 20, y * 20, 2); // 2 octaves: shape, plus texture on it Octaves are the **cost knob** of every field effect: doubling them doubles the work per light. Two is usually the sweet spot; four is for when the fixture is small enough to afford it. -One measured detail worth knowing: octaves partly cancel, so their sum is narrower than one octave's range. Four octaves span roughly 54..199 of 0..255 rather than the full sweep. projectMM re-widens the result for you, so `fbm` at any octave count still uses the whole range and your thresholds keep working. +One measured detail worth knowing: octaves partly cancel, so their sum is narrower than one octave's range. Four octaves span roughly 54..199 of 0..255 rather than the full sweep. MoonLight re-widens the result for you, so `fbm` at any octave count still uses the whole range and your thresholds keep working. --- @@ -140,7 +140,7 @@ Much of the vocabulary above exists twice: as a C++ kernel, and as a MoonLive bu The noise family (`noise`, `fbm`, `warp`), `line`, `circle` and the palette calls are the same function under both names. The **transport** kernels are reached differently: `advect16`, `decay16`, `quantize`, `blit16`, `upscale16` and `halfLifeKeep` are compiled-only, and a script uses them through six builtins that wrap them, `trail(1)`, `flowNoise`, `flowCurl`, `trailDecay`, `emitTrail` and `fieldRate`. That is deliberate: a script names a wind and a persistence, and the binding owns the planes, the ping-pong and the narrowing, which is what keeps a whole-plane operation one host call rather than one per light. Compiled-only for now: `disc`, `sphere`, `text`, `sprite`, the SDF catalog and the fluid solver. -**MoonLive** is projectMM's scripting language. Scripts are compiled to native +**MoonLive** is MoonLight's scripting language. Scripts are compiled to native code on the device, so a script is not interpreted per pixel: it runs at machine speed. You edit one in the browser and the picture changes as you type. The interesting number is how little a script has to say: diff --git a/docs/tutorials/how-projectmm-works.md b/docs/tutorials/how-projectmm-works.md index e93e9d1e..ff33a86d 100644 --- a/docs/tutorials/how-projectmm-works.md +++ b/docs/tutorials/how-projectmm-works.md @@ -1,21 +1,21 @@ -# How projectMM works +# How MoonLight works You have lights running. This page explains what you were actually looking at. -There is really only **one idea** in projectMM, and everything else follows from it. Ten minutes here and the rest of the interface stops being a wall of settings and becomes a place you know your way around. +There is really only **one idea** in MoonLight, and everything else follows from it. Ten minutes here and the rest of the interface stops being a wall of settings and becomes a place you know your way around. > New here? Start with **[Install & first light](../gettingstarted.md)** and come > back once something is lit. -![The projectMM interface: navigation on the left, live preview in the middle, module cards on the right](../assets/gettingstarted/02-01-UI-large.png) +![The MoonLight interface: navigation on the left, live preview in the middle, module cards on the right](../assets/gettingstarted/02-01-UI-large.png) - + --- ## 1. Everything is a card -Every single thing in projectMM, the WiFi settings, an effect, an LED driver, the file manager, is a **MoonModule**. And every MoonModule is a **card**. +Every single thing in MoonLight, the WiFi settings, an effect, an LED driver, the file manager, is a **MoonModule**. And every MoonModule is a **card**. There is no second kind of thing. Learn to read one card and you can read them all. @@ -38,7 +38,7 @@ Look at that picture again, because every card has the same parts: Two of those are unusual and worth pausing on. **The timing and memory are per module, and they are live.** Most software hides -this. projectMM shows you exactly what each part costs, so when something feels slow you can see which card is responsible instead of guessing. +this. MoonLight shows you exactly what each part costs, so when something feels slow you can see which card is responsible instead of guessing. **The nesting is the structure.** A child card is drawn *inside* its parent's border. When you see `Grid` inside `Layouts`, that is not decoration, the Grid really does belong to Layouts. The box is the truth. @@ -126,7 +126,7 @@ fold it, tile it. An effect written for one strip can drive a whole wall without Move a slider and the next frame uses it. Change the WiFi credentials, add an effect, resize a grid, all of it takes effect immediately, on a running device. - + **Files are the one exception, and only because typing is different.** A script you are editing is saved when you click away, press Ctrl/Cmd+S, or press Save, a half-typed line should not be compiled onto your fixture mid-word. The moment it is saved it recompiles and swaps in live, same as everything else. @@ -152,7 +152,7 @@ The rest the module declares about itself: | | | |---|---| -| 💫 projectMM / MoonLight · 🌙 MoonModules · 🐙 WLED · ⚡️ FastLED | where it came from | +| 💫 MoonLight / MoonLight · 🌙 MoonModules · 🐙 WLED · ⚡️ FastLED | where it came from | | 🦅 | a named contributor, credited on the module | | 🎵 volume · 🎶 frequency | it listens: one note reacts to how LOUD the room is, two to WHICH notes are playing | | 📡 | it takes its picture from the network | diff --git a/docs/work/future/audio-dsp-roadmap.md b/docs/work/future/audio-dsp-roadmap.md index f3b274c0..2209570b 100644 --- a/docs/work/future/audio-dsp-roadmap.md +++ b/docs/work/future/audio-dsp-roadmap.md @@ -8,14 +8,14 @@ ## Prior art studied (credit by name) Audio-reactive lighting is a long-standing idea in the LED-controller world (WLED-MM and MoonLight -are the closest lineage). projectMM's audio path is its own implementation, designed from the INMP441 +are the closest lineage). MoonLight's audio path is its own implementation, designed from the INMP441 datasheet and standard DSP — not traced from any one project — but three people's thinking is studied here with respect and credited by name (the *Industry standards, our own code* principle: study hard, write fresh). **Frank (softhack007)** — main author of the WLED-MM audioreactive usermod (the most-used open-source audio-reactive LED implementation), a direct ancestor of the ideas this module learns from. The -product owner worked alongside Frank for years on WLED-SR / WLED-MM before MoonLight and projectMM. +product owner worked alongside Frank for years on WLED-SR / WLED-MM before MoonLight and MoonLight. His concept is the worked example in the *Adaptive noise gate* section below: his idea, our analysis, written fresh against our architecture. @@ -23,7 +23,7 @@ written fresh against our architecture. onto Espressif's **esp-dsp** library ("stupid fast compared to ArduinoFFT"), very low latency on S3/P4. His contribution has two parts: - *esp-dsp FFT.* Troy uses esp-dsp's **radix-4** real FFT (`dsps_fft4r_fc32`) with a Blackman-Harris - window. This validates the path projectMM is already on — **we use esp-dsp too**, the **radix-2** + window. This validates the path MoonLight is already on — **we use esp-dsp too**, the **radix-2** float real FFT (`dsps_fft2r_fc32`) in `platform_esp32_i2s.cpp`. Same library; the one open optimisation is **radix-4 vs radix-2** (fewer butterfly stages, log₄N vs log₂N — a measure-then- maybe tune-up, not a gap; today the float FFT on the FPU is well inside one tick). Two adjacent, @@ -52,7 +52,7 @@ WLED lineage above, and its own source comments are unusually candid about which **Damian Schneider (DedeHai)** — WLED core dev; WLED's audioreactive usermod carries an integer / fixed-point FFT path (~1.5 ms on a C3, >10× ArduinoFFT on FPU-less chips). The consensus (Troy + Frank) -is that with esp-dsp FFT + biquads, **fixed-point is not necessary on FPU chips** (S3/P4) — projectMM's +is that with esp-dsp FFT + biquads, **fixed-point is not necessary on FPU chips** (S3/P4) — MoonLight's exact position: float on FPU targets, the int16 / `dl_fft` hardware path reserved for low-power chips. DedeHai's current audio experiment is a PoC MSGEQ7-based path (offloading the spectrum to a dedicated analyser chip) — a different point in the same space, noted for completeness. @@ -71,7 +71,7 @@ All of the following widen the **source seam** — what feeds the pipeline — l - **Analog line-in.** DedeHai got analog input working on the S3; Troy got it working in ParrotRadio. Troy's testing-confidence nuance worth recording: he considers his ParrotRadio analog path better-exercised (he actually recorded + played back through it), whereas an unlistened-to analog - path "may not be as accurate as it looks." **If projectMM adopts analog line-in, validate by + path "may not be as accurate as it looks." **If MoonLight adopts analog line-in, validate by listening**, not just by watching the level meter. - **I²C-configured codecs (e.g. ES8311).** Do **not** hand-roll each codec's register config — pull in Espressif's **`esp_codec_dev`** component (carries option tables for many codecs), supporting "a diff --git a/docs/work/future/backlog-core.md b/docs/work/future/backlog-core.md index 1fd060ee..6c5e1706 100644 --- a/docs/work/future/backlog-core.md +++ b/docs/work/future/backlog-core.md @@ -71,7 +71,7 @@ with the loop sleeping the remainder of each frame. The tick path already measur `--port` flag: a desktop-only concern, invisible on a board. **Also worth knowing:** it would cut power draw on a single instance too, which matters for anyone -running projectMM permanently on a NAS or a Pi. +running MoonLight permanently on a NAS or a Pi. ## Distribution @@ -96,7 +96,7 @@ dependency rather than installing something broken, which is the good failure mo **A user with a Pi settles it in one command.** Closing this needs a report, not a code change. -### A flashable SD image with projectMM already on it (robwomp, 2026-09-08) +### A flashable SD image with MoonLight already on it (robwomp, 2026-09-08) Suggested on Discord while bringing up a NanoPi R28S: most Pi users want to write an `.img` to a card and be running, not to install a toolchain. Armbian's own build tooling supports exactly this @@ -134,11 +134,11 @@ declared rather than for a buffer to fill, and to time out on stall rather than - **Teensy 4.1** — toolchain-file build, `.hex` for Teensy Loader. - **Raspberry Pi** — ARM64, cross-built or native. - **macOS code-signing (Developer ID)** — the release `.dmg` is now ad-hoc signed, which turns Gatekeeper's outright refusal into the "unidentified developer" prompt a user can accept via right-click Open. A paid Developer ID certificate plus notarization would drop that prompt too. -- **Windows code-signing** — drops the SmartScreen warning on first run of `projectMM.exe`. Same shape as macOS signing; needs an EV / OV code-signing certificate (Microsoft Trusted Signing is the cheapest current option). Until then, the README notes the SmartScreen prompt. +- **Windows code-signing** — drops the SmartScreen warning on first run of `MoonLight.exe`. Same shape as macOS signing; needs an EV / OV code-signing certificate (Microsoft Trusted Signing is the cheapest current option). Until then, the README notes the SmartScreen prompt. - **Live RMII Ethernet reconfigure** — runtime PHY/pin config shipped (`ethType` + pin controls in NetworkModule, per-board defaults in `deviceModels.json`, `platform::setEthConfig`/`ethInit` dispatch). W5500 (SPI) on S3 applies **live** — `ethStop()` tears down the SPI bus and `ethInit()` re-runs on the next `loop1s()` with no reboot. RMII (classic/P4 internal EMAC) still saves config and asks for a restart to apply, because the EMAC bring-up is fiddlier to hot-cycle cleanly. Make RMII live too: a hot `esp_eth_stop` + EMAC/netif teardown + re-init on config change, matching the W5500 path, so every interface honours the no-reboot principle. - **GCC below 16 needs four warnings demoted, and nothing exercises those versions** - `-Wnull-dereference`, `-Wrestrict`, `-Wstringop-overflow` and `-Wformat-truncation` fire on provably correct code from GCC 12 through 15 (five of the twelve inside libstdc++ and glibc headers, unreachable from our source), so CMakeLists demotes them to non-fatal there and keeps them fatal on 16+. That unblocks CI and from-source builds on Debian and Raspberry Pi OS alike, but it is a suppression, not an understanding: nobody routinely compiles with 12-15, so a REAL instance of one of these on those versions is now a warning nobody reads. Revisit when the runner's default GCC reaches 16, at which point the whole block can be deleted. - **Installer UX polish** — clear "Pre-release (beta)" warning on RC/latest picks, yank-by-asset-tag instead of yank-by-release-deletion. -- **Offer projectMM/MoonLight as a library** — a downstream sketch where another firmware/app consumes the light pipeline (or a subset) as an embeddable dependency rather than running the whole binary. `library.json` is already a PlatformIO *library* manifest, so the seed exists. When this is designed, give it a small public **identity surface**: one runtime constant the consumer reads (a `kProjectName`, likely a `ProjectInfo` bundle of name + version + url) that the network wire-strings (ArtNet/E1.31 source-name + CID), the UI banner, and any "About" string all *derive from* — the one place a consumer queries "what am I embedding." This is the genuine home for the name-centralisation that the rename ([the MoonLight plan](../present/Plan-20260922%20-%20MoonLight,%20from%20v5.0.0%20to%20the%20rename.md)) deliberately *didn't* do: the rename is a one-time sweep (a constant would just split it), but a library consumer references the identity ongoing and widely, which is the test a constant must pass. Build it *then*, against the real library API, not speculatively now. +- **Offer MoonLight/MoonLight as a library** — a downstream sketch where another firmware/app consumes the light pipeline (or a subset) as an embeddable dependency rather than running the whole binary. `library.json` is already a PlatformIO *library* manifest, so the seed exists. When this is designed, give it a small public **identity surface**: one runtime constant the consumer reads (a `kProjectName`, likely a `ProjectInfo` bundle of name + version + url) that the network wire-strings (ArtNet/E1.31 source-name + CID), the UI banner, and any "About" string all *derive from* — the one place a consumer queries "what am I embedding." This is the genuine home for the name-centralisation that the rename ([the MoonLight plan](../present/Plan-20260922%20-%20MoonLight,%20from%20v5.0.0%20to%20the%20rename.md)) deliberately *didn't* do: the rename is a one-time sweep (a constant would just split it), but a library consumer references the identity ongoing and widely, which is the test a constant must pass. Build it *then*, against the real library API, not speculatively now. - **HTTP: a request whose headers or body arrive a few ms late is dropped, intermittently (2026-08-20).** `handleConnection` runs SYNCHRONOUSLY inside `tick20ms`, so its waits are kept short to protect the render loop: a freshly accepted connection gets **~5 ms** for its request @@ -235,12 +235,12 @@ declared rather than for a buffer to fill, and to time out on stall rather than ### DevicesModule — interop plugins + the command half (discovery shipped) -DevicesModule discovers via **passive UDP presence** (UDP 65506) feeding a [`DevicePlugin`](../../src/core/util/DevicePlugin.h) seam (shipped: projectMM + WLED plugins). mDNS is advertise-only so projectMM appears in the native WLED apps + Home Assistant; the WLED-app interop (list + live color + brightness control) is shipped too. What remains is *growth on the seam*, each piece additive (one plugin file, no core change): +DevicesModule discovers via **passive UDP presence** (UDP 65506) feeding a [`DevicePlugin`](../../src/core/util/DevicePlugin.h) seam (shipped: MoonLight + WLED plugins). mDNS is advertise-only so MoonLight appears in the native WLED apps + Home Assistant; the WLED-app interop (list + live color + brightness control) is shipped too. What remains is *growth on the seam*, each piece additive (one plugin file, no core change): - **More discovery plugins** — ESPHome, Tasmota, Hue (*hub-shaped*: a bridge whose Zigbee bulbs are children behind it, with link-button auth). Each is a new `DevicePlugin` declaring its `discoveryPort()` + classifying the datagram (or, for a system that only does mDNS, a re-introduced advertise-side browse scoped to *foreign* services only — never the ones we advertise). Hue is the canonical "more than a flat device" case the seam is shaped for. (Note: Hue *control* already ships as an **output driver**, see [HueDriver](../../moonmodules/light/moxygen/HueDriver.md) — bulbs as effect pixels; the driver also *lists* its bridge in DevicesModule with the color-light count. Two complementary follow-ups remain: (a) auto-fill the driver's bridge IP from discovery so the user doesn't type it (the mDNS-browse plugin above); (b) **pair once, not per driver** — pairing + the app key currently live on each HueDriver, so two drivers on one bridge pair twice. The clean end-state moves the bridge identity (IP + key + Pair button + light list) into DevicesModule and makes HueDriver a pure output that reads the paired bridge by IP — do this together with the discovery plugin, since both hinge on DevicesModule owning the bridge.) -- **The command half** — `DevicePlugin::command()` (+ per-plugin capability/auth), so projectMM can *control* a discovered foreign device, not just list it: set WLED brightness via its JSON API, a Hue resource via the bridge's authenticated CLIP API, a Tasmota via `cmnd`. Built when a control consumer exists; the discovery seam is already shaped to accept it (incl. hub plugins). This is the **multi-ecosystem selling point** — one UI controlling WLED + ESPHome + Hue. Commands split by need (the rule, not "all REST"): must-arrive config over REST; latency-critical sync over UDP (~0.5–1 ms vs REST's 10–50 ms — REST would visibly de-sync). +- **The command half** — `DevicePlugin::command()` (+ per-plugin capability/auth), so MoonLight can *control* a discovered foreign device, not just list it: set WLED brightness via its JSON API, a Hue resource via the bridge's authenticated CLIP API, a Tasmota via `cmnd`. Built when a control consumer exists; the discovery seam is already shaped to accept it (incl. hub plugins). This is the **multi-ecosystem selling point** — one UI controlling WLED + ESPHome + Hue. Commands split by need (the rule, not "all REST"): must-arrive config over REST; latency-critical sync over UDP (~0.5–1 ms vs REST's 10–50 ms — REST would visibly de-sync). - **Live peer state** — a discovered peer's brightness / on-off shown in our list, refreshed by polling its REST `/json` after discovery gives the IP (discovery = UDP/mDNS, state = REST). The read-side complement to the command half. -- **Non-IP transports (board-gated, far future)** — Tasmota-MQTT / zigbee2mqtt need an MQTT client; **direct Zigbee/Thread** (S31/C6/H2 802.15.4 radio) makes projectMM the *hub itself*, driving bulbs over the mesh with no gateway — the standout differentiator, the biggest lift. Same plugin philosophy, a transport addition + board gate. +- **Non-IP transports (board-gated, far future)** — Tasmota-MQTT / zigbee2mqtt need an MQTT client; **direct Zigbee/Thread** (S31/C6/H2 802.15.4 radio) makes MoonLight the *hub itself*, driving bulbs over the mesh with no gateway — the standout differentiator, the biggest lift. Same plugin philosophy, a transport addition + board gate. Full design + the reasoned transport split: `Plan-20260629 - UDP device discovery + mDNS advertise-only` (in the plans archive). @@ -435,7 +435,7 @@ no address to unicast to before anything has been discovered. Document broadcast WLED-compatible mode rather than naming the flag after WLED. **It stays on DevicesModule rather than moving up to NetworkModule.** Three places in the codebase -send to a group, and only one of them is ours to choose: discovery uses projectMM's own +send to a group, and only one of them is ours to choose: discovery uses MoonLight's own `239.255.x.x`, audio sync uses WLED's `239.0.0.1`, and sACN send uses the universe-derived `239.255.{hi}.{lo}` that E1.31 mandates. A network-level "prefer broadcast" switch could not move the latter two without breaking the protocols they speak, so a control there would imply an @@ -687,7 +687,7 @@ Board preset catalog + upload (later, when the runtime config has real consumers Sequencing rule (unchanged): each functionality lands a device-side control first, then its preset field; the catalog grows one earned consumer at a time, never as a speculative pin dump. -**Module variant + PSRAM within the classic-ESP32 family.** `getChipDescription()` and MoonLight's `ModuleIO.h` both report only the *core* family ("ESP32"), not the *module* (WROOM / WROVER / PICO) — so neither distinguishes whether a classic-ESP32 board has PSRAM. This matters for projectMM (whose large-LED story leans on PSRAM) in a way it doesn't for MoonLight: e.g. the **QuinLED Dig-Next-2 is built on an ESP32-PICO with 2 MB PSRAM**, but projectMM's `esp32` build has no `CONFIG_SPIRAM` (see the `#ifdef CONFIG_SPIRAM` gate in `platform_esp32.cpp::psramAlloc`), so it flashes and runs as a no-PSRAM device and hits the non-PSRAM fragmentation ceiling at large grids that the 2 MB would otherwise relieve. A PSRAM-enabled classic-ESP32 firmware variant (e.g. `esp32-psram`) would unlock it; `deviceModels.json` could then carry a `psram` hint per board to steer the picker — but only once that variant exists (no consumer today). `deviceModels.json` currently maps every classic board to the WiFi-only `esp32` variant, which is correct-but-unoptimised for PSRAM-bearing PICO boards. +**Module variant + PSRAM within the classic-ESP32 family.** `getChipDescription()` and MoonLight's `ModuleIO.h` both report only the *core* family ("ESP32"), not the *module* (WROOM / WROVER / PICO) — so neither distinguishes whether a classic-ESP32 board has PSRAM. This matters for MoonLight (whose large-LED story leans on PSRAM) in a way it doesn't for MoonLight: e.g. the **QuinLED Dig-Next-2 is built on an ESP32-PICO with 2 MB PSRAM**, but MoonLight's `esp32` build has no `CONFIG_SPIRAM` (see the `#ifdef CONFIG_SPIRAM` gate in `platform_esp32.cpp::psramAlloc`), so it flashes and runs as a no-PSRAM device and hits the non-PSRAM fragmentation ceiling at large grids that the 2 MB would otherwise relieve. A PSRAM-enabled classic-ESP32 firmware variant (e.g. `esp32-psram`) would unlock it; `deviceModels.json` could then carry a `psram` hint per board to steer the picker — but only once that variant exists (no consumer today). `deviceModels.json` currently maps every classic board to the WiFi-only `esp32` variant, which is correct-but-unoptimised for PSRAM-bearing PICO boards. ### Per-layout coordinate offset for independent placement (backlog) @@ -791,7 +791,7 @@ with the [DevicesModule command half](#devicesmodule-interop-plugins-the-command ### HA update entity via MQTT discovery — release check (open follow-up) -**Discovery config + install command wiring — shipped.** [`MqttModule`](../../src/core/system/MqttModule.cpp) now publishes a second HA-discovery component at `homeassistant/update/projectMM_/config` (same haDiscovery gate, same MAC-stable id, same device card as the light), state on `/update/state`, and subscribes to `/update/set`. HA renders a *"Firmware: "* card in the diagnostic section of the device panel. An install command routes to `platform::http_fetch_to_ota` with the release-artifact URL built from the payload version + `kFirmwareName` (`https://github.com/MoonModules/projectMM/releases/download/v/firmware--v.bin`), reusing the same OTA task the `/api/firmware/url` route drives. +**Discovery config + install command wiring — shipped.** [`MqttModule`](../../src/core/system/MqttModule.cpp) now publishes a second HA-discovery component at `homeassistant/update/MoonLight_/config` (same haDiscovery gate, same MAC-stable id, same device card as the light), state on `/update/state`, and subscribes to `/update/set`. HA renders a *"Firmware: "* card in the diagnostic section of the device panel. An install command routes to `platform::http_fetch_to_ota` with the release-artifact URL built from the payload version + `kFirmwareName` (`https://github.com/MoonModules/projectMM/releases/download/v/firmware--v.bin`), reusing the same OTA task the `/api/firmware/url` route drives. **Still to build — device-side release check.** `installed_version` and `latest_version` are equal today (both `MM_VERSION`), so HA renders the entity as up-to-date and the Install button is disabled. What's missing is a periodic poll of `https://api.github.com/repos/MoonModules/projectMM/releases/latest`: at boot (~30 s post-network-up) and every 24 h thereafter, fetch the release JSON, extract `tag_name`, and if newer than `MM_VERSION` call `MqttModule::publishUpdateState()` with the updated `latest_version` (the publish path is already there — only the caller is missing). ~2 KB per check. @@ -1150,7 +1150,7 @@ Rounds 1 (board + Ethernet-only) and 2 (Parlio LED driver) have landed. Remainin Then, cheapest first: (a) read the new max/histogram to see whether the cost is uniform or spiky (uniform favors cache contention, spiky favors a blocking call, and the two coexisting is now the leading reading); (b) the P4 cache performance counters for a direct miss-rate read; (c) moving the hot render path to IRAM/internal RAM and re-measuring, which is also a candidate fix rather than only a diagnostic. 1. **Runtime SDIO re-init of the C6 fails — CONFIRMED a C6 slave-firmware problem (not a guess).** SystemModule now exposes a `wifiCoproc` read-only control (via `platform::coprocessorWifi()` → `esp_hosted_get_coprocessor_fwversion()`), and on the bench it read **`not detected`** at the time (the control now reports **`no version reply`**, and only after a bounded number of attempts): the C6 returns no valid firmware version. **What that means was overstated here.** It was read as the signature of absent / incompatible slave firmware, but the same board later associated and served traffic over that very link while this RPC still went unanswered — so an unanswered version query says the QUERY failed, not that the C6 is absent. The round-3 conclusion below rests on the `sdmmc_card_init failed` re-init evidence, not on this control. Likely a version mismatch on top of that: The host pulled esp_hosted **2.12.9**; Espressif's P4-Function-EV-Board ships its C6 pre-flashed with esp_hosted slave **v0.0.6**, and the **Waveshare NANO is a different board that may carry a different / absent C6 slave image**. The symptom fits: boot inits the host SDIO master fine, but resetting the C6 (GPIO 54) and re-enumerating it as a slave fails (`sdmmc_card_init failed`) because the C6 has no compatible slave firmware responding. **Primary next step: build + flash the version-matched esp_hosted slave firmware onto the NANO's C6.** The slave project is already vendored at `esp32/managed_components/espressif__esp_hosted/slave/` (`sdkconfig.defaults.esp32c6`, `partitions.esp32c6.csv`); `idf.py create-project-from-example "espressif/esp_hosted:slave"` → `set-target esp32c6` → flash. **Caveat / needs PO + bench hardware:** flashing the C6 on the EV board uses an **ESP-Prog wired to the `PROG_C6` header** with the P4 held in bootloader mode (esp_hosted `docs/esp32_p4_function_ev_board.md` §5.2) — the NANO's C6-flash path must be confirmed (separate USB? equivalent header? ESP-Prog?), and an ESP-Prog may be needed. An OTA slave-update path exists but needs a *working* link first (chicken-and-egg here). This is a hardware-provisioning task, not application code. Secondary fallbacks if firmware-match doesn't fix it: an esp_hosted option to skip the reconfigure/slave-reset when the transport is already up at boot; a slower SDIO freq or 1-bit mode; verify GPIO 54 reset polarity/timing for the NANO. **(Note: EIM — the building.md v6.0-adoption item — does NOT help here; it's a host-machine installer, unrelated to device-side C6 firmware.)** - **User lead (2026-07-09) — avoid the WiFi teardown/re-init on hosted targets entirely; it may sidestep the slave-reset failure.** A user hit the mirror symptom on a *different* codebase (ESP32-Sveltekit / WLED-MM, not projectMM — its `lib/framework/WiFiSettingsService.cpp` doesn't exist here): after a clean C6-flash the P4-NANO WiFi worked, but their app's boot-time `WiFi.disconnect(true)` fully tore down the WiFi stack, and the later AP bring-up then failed with `esp_hosted_transport_config: Transport already initialized` / `esp_hosted_init failed!` / `AP enable failed!`. Their working fix: on `CONFIG_ESP_WIFI_REMOTE_ENABLED` (= hosted) targets, **don't do a full stack reset** — keep STA enabled, reconnect *without* a full teardown/re-init, and never call `WiFi.disconnect(true)` in the disconnect callback. **Why this is relevant to us even though the file differs:** our own comment at `platform_esp32.cpp:796` already documents that the esp_hosted transport is set up **once at boot** and is fragile to re-init (`connect_to_slave` = a transport reconfigure that resets the slave + re-inits SDIO and fails on a live link). Our disconnect *callback* is already safe (`wifiEventHandler` on `STA_DISCONNECTED` only sets a flag — no teardown), BUT our **failover path is not**: `wifiStaStop()` (`platform_esp32.cpp:915`) calls `esp_wifi_deinit()`, and the STA-retry / AP-fallback then re-runs `ensureWifiInit()` → `esp_wifi_init()` — the exact deinit→reinit cycle that on a hosted target triggers the GPIO-54 slave reset (round-3 open-issue #2's `sdmmc_card_init failed`). So the round-3 failure ("runtime SDIO re-init of the C6 fails") and this user's report may be **the same root cause**: the re-init shouldn't happen at all on a hosted target. **Concrete next step to try on the bench:** guard the teardown/re-init on `platform::hasWifiCoprocessor` (already defined = `isEsp32P4 && hasWiFi`) — on hosted targets, do NOT `esp_wifi_deinit()` in `wifiStaStop()` and do NOT re-`esp_wifi_init()` in `ensureWifiInit()` once the boot-time init is up; instead just `esp_wifi_disconnect()` + `esp_wifi_set_config()` + `esp_wifi_connect()` (STA retry) or `esp_wifi_set_mode(APSTA)` for the fallback, reusing the live transport. This is cheaper than the C6 reflash and independent of #18759, so it's worth trying first once the board boots. If it works, it also removes the slave-reset from the normal failover, not just the AP case. Blocked behind the #18759 boot crash like everything else P4-WiFi, but this is the first thing to try when the board boots again. + **User lead (2026-07-09) — avoid the WiFi teardown/re-init on hosted targets entirely; it may sidestep the slave-reset failure.** A user hit the mirror symptom on a *different* codebase (ESP32-Sveltekit / WLED-MM, not MoonLight — its `lib/framework/WiFiSettingsService.cpp` doesn't exist here): after a clean C6-flash the P4-NANO WiFi worked, but their app's boot-time `WiFi.disconnect(true)` fully tore down the WiFi stack, and the later AP bring-up then failed with `esp_hosted_transport_config: Transport already initialized` / `esp_hosted_init failed!` / `AP enable failed!`. Their working fix: on `CONFIG_ESP_WIFI_REMOTE_ENABLED` (= hosted) targets, **don't do a full stack reset** — keep STA enabled, reconnect *without* a full teardown/re-init, and never call `WiFi.disconnect(true)` in the disconnect callback. **Why this is relevant to us even though the file differs:** our own comment at `platform_esp32.cpp:796` already documents that the esp_hosted transport is set up **once at boot** and is fragile to re-init (`connect_to_slave` = a transport reconfigure that resets the slave + re-inits SDIO and fails on a live link). Our disconnect *callback* is already safe (`wifiEventHandler` on `STA_DISCONNECTED` only sets a flag — no teardown), BUT our **failover path is not**: `wifiStaStop()` (`platform_esp32.cpp:915`) calls `esp_wifi_deinit()`, and the STA-retry / AP-fallback then re-runs `ensureWifiInit()` → `esp_wifi_init()` — the exact deinit→reinit cycle that on a hosted target triggers the GPIO-54 slave reset (round-3 open-issue #2's `sdmmc_card_init failed`). So the round-3 failure ("runtime SDIO re-init of the C6 fails") and this user's report may be **the same root cause**: the re-init shouldn't happen at all on a hosted target. **Concrete next step to try on the bench:** guard the teardown/re-init on `platform::hasWifiCoprocessor` (already defined = `isEsp32P4 && hasWiFi`) — on hosted targets, do NOT `esp_wifi_deinit()` in `wifiStaStop()` and do NOT re-`esp_wifi_init()` in `ensureWifiInit()` once the boot-time init is up; instead just `esp_wifi_disconnect()` + `esp_wifi_set_config()` + `esp_wifi_connect()` (STA retry) or `esp_wifi_set_mode(APSTA)` for the fallback, reusing the live transport. This is cheaper than the C6 reflash and independent of #18759, so it's worth trying first once the board boots. If it works, it also removes the slave-reset from the normal failover, not just the AP case. Blocked behind the #18759 boot crash like everything else P4-WiFi, but this is the first thing to try when the board boots again. 2. **Co-processor components no longer compile into `esp32p4rev1-eth` — FIXED.** The gate is now `rules: if "$CONFIG{MM_P4_WIFI} == True"` (the `$CONFIG{NAME}` form, no `CONFIG_` prefix inside the braces — the bare form silently skipped the dependency, see round 4 below) (a Kconfig option declared in `esp32/main/Kconfig.projbuild`, set only by `sdkconfig.defaults.esp32p4rev1-eth-wifi`), so `esp_hosted` / `esp_wifi_remote` are pulled **only** by the WiFi build, never by eth-only. The old `target == esp32p4` gate pulled them into `esp32p4rev1-eth` too; that wasn't merely build-time waste — esp_hosted self-inits its SDIO master at boot, which on the eth-only build interfered with the EMAC bring-up (a red herring chased during the P4 no-DHCP hunt). The eth-only image dropped 1.36→1.12 MB once gated out. The `wifiCoproc` read-out stays compile-gated on `platform::hasWifiCoprocessor` (`isEsp32P4 && hasWiFi`). 3. **Build reproducibility.** `build_esp32.py` does not yet build this variant reliably: the C6 slave-target Kconfig `default ... if IDF_TARGET_ESP32P4` only fires on `set-target`, and the reconfigure a plain `build` triggers drops it back to ESP32-H2 (no WiFi) → fails on missing `CONFIG_WIFI_RMT_*`. A clean manual sequence works (`rm -rf ` → `set-target esp32p4` → `build`, all with the same `-DSDKCONFIG`/`-DSDKCONFIG_DEFAULTS`); the wrapper needs a fix so the auto-default sticks across reconfigures (see the KNOWN ISSUE comment in `build_esp32.py`). @@ -1177,7 +1177,7 @@ Compile-time answer already ships: `--firmware esp32-eth` excludes the WiFi stac ## UI -Forward-looking companion to the shipped UI spec, [moonmodules/core/services.md](../../moonmodules/core/services.md). The live spec describes the UI as shipped; this file holds what is **not** in it yet: deferred items, open design questions for 1.0, and the gap analysis against projectMM v1. +Forward-looking companion to the shipped UI spec, [moonmodules/core/services.md](../../moonmodules/core/services.md). The live spec describes the UI as shipped; this file holds what is **not** in it yet: deferred items, open design questions for 1.0, and the gap analysis against MoonLight v1. ### Deferred to 1.x @@ -1282,7 +1282,7 @@ Mitigated in practice: prime-only fires exactly one EOF per frame (no intra-fram The P4 build runs at **360 MHz** because IDF's `Kconfig.cpu` caps a `SELECTS_REV_LESS_V3` build (which we set, so a stock binary boots on the v0.x/v1.x P4 chips in the field) at 360; 400 MHz is IDF's default only for rev ≥ 3. Forcing 400 on our bench P4 (a **rev v1.3** chip) was tried and **bootloops** — `assert failed: esp_clk_init clk.c:105` — so 400 is a genuine hardware limit on pre-rev-3 silicon, not marginal stability. The ~11% compute headroom is left on the table for rev-3+ owners. **Two ways to reach it, both deferred until rev-3 P4 hardware is on the bench to validate:** -- **A separate `esp32p4-400` variant** — `sdkconfig` with `ESP32P4_REV_MIN` = rev 3 + `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_400`, so IDF's normal boot-time clock init sets 400 and the bootloader refuses to run on a pre-rev-3 chip (safe). The web installer offers it only to rev-3+ boards. This is the projectMM-idiomatic per-variant path. +- **A separate `esp32p4-400` variant** — `sdkconfig` with `ESP32P4_REV_MIN` = rev 3 + `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_400`, so IDF's normal boot-time clock init sets 400 and the bootloader refuses to run on a pre-rev-3 chip (safe). The web installer offers it only to rev-3+ boards. This is the MoonLight-idiomatic per-variant path. - **A runtime bump** — keep the 360 build (boots everywhere) and, at `app_main`, read `esp_chip_info().revision` and if ≥ 300 call `rtc_clk_cpu_freq_mhz_to_config(400)` + `rtc_clk_cpu_freq_set_config`. One binary, self-selecting. Riskier: a runtime CPU-PLL change on the P4 couples to the flash/PSRAM clock trees configured for 360, so it needs validation on real rev-3 hardware before shipping. Neither ships until a rev-3 P4 can prove 400 runs clean — no untested clock config, per the same rule the S31/320 and this P4/400 bootloop both taught. @@ -1624,11 +1624,11 @@ have to make, and our figures would not be mixed into theirs. **We adopt their schema unchanged, and drop the one field that does not fit** (settled 2026-09-08). chip, version, previousVersion, releaseName, ledCount, isMatrix, flashSize, psramSize/Present, fsUsed/Total, busCount and busTypes mean the same thing in both projects, so those figures are -directly comparable and projectMM can sit in a pooled overview rather than being a special case. +directly comparable and MoonLight can sit in a pooled overview rather than being a special case. Our own vocabulary rides the free-form lists: ledFeatures, peripherals, integrations, and `usermods` for the enabled-module names. That works because migration V2026040301 (2026-04) replaced twenty-odd fixed boolean feature columns with three comma-separated lists, aggregated by counting whatever -values appear: adding projectMM's names costs their server nothing. +values appear: adding MoonLight's names costs their server nothing. The layout dimensions this entry originally wanted (width x height x depth, since we are 2D and 3D) have no equivalent there, and are DROPPED rather than added. `ledCount` plus `isMatrix` answers most @@ -1809,7 +1809,7 @@ machine holding the BROWSER. The container makes it visible because there the tw the same card lies on any desktop instance opened from another machine. The download it offers is the failure mode worth naming: on a Mac the picker serves -`projectMM-macos-arm64-*.dmg`, a real file that installs and runs perfectly, and updates the wrong +`MoonLight-macos-arm64-*.dmg`, a real file that installs and runs perfectly, and updates the wrong computer. Nothing errors, and the device stays on its old build. **Root cause is one missing build flag.** `MM_FIRMWARE_NAME` already exists for exactly this @@ -1912,7 +1912,7 @@ E (14988) H_SDIO_DRV: failed to read registers ``` and reboots without ever reaching the render loop. The same v4.0.0 image serves WiFi on the bench -P4 (.139) with no SDIO error and no GPIO 54 conflict, and no projectMM config on any P4 entry +P4 (.139) with no SDIO error and no GPIO 54 conflict, and no MoonLight config on any P4 entry references GPIO 54 (esp_hosted drives it as the slave reset). The two boards differ in silicon revision: this shield is chip rev **v1.0**, .139 is **v1.3**. So the C6 side of this shield does not come up, which is a board/slave-firmware matter rather than anything in our WiFi path, and the @@ -1926,7 +1926,7 @@ dead C6 unlikely and points at host-side SDIO configuration on 6.1. What was che would not move them. The preset mostly moves SPI pins, which we do not use. - `ESP_HOSTED_SDIO_GPIO_RESET_SLAVE` defaults to **54 on any P4** regardless of preset, so the `gpio: conflict found for GPIO[54]` line is esp_hosted resetting the slave twice, not a wrong pin - from our config. No projectMM P4 entry references 54. + from our config. No MoonLight P4 entry references 54. **Our SDIO configuration is not the difference.** Diffed against a known-good local reference (`ewowi/FlowFields/sdkconfig.esp32-p4`, an IDF 5.5-era P4 build), every hosted setting is IDENTICAL: @@ -1935,7 +1935,7 @@ pins (CLK 18, CMD 19, D0-D3 14-17), `GPIO_RESET_SLAVE` 54, `RESET_ACTIVE_HIGH=y` pin and SDIO clock are all ruled out as differences, and the identical released v4.0.0 image works on .139 and not on this shield. Same firmware, same config, two boards, two outcomes: what remains is on the board side (C6 slave firmware, power/strapping, or the SDIO traces on this revision), and -the only projectMM work left is the degrade-instead-of-reboot fix above. +the only MoonLight work left is the degrade-instead-of-reboot fix above. What esp_hosted's private `is_transport_tx_ready()` reports is the signal the slave-ready loop itself polls, but it lives in a PRIVATE include dir (`host/drivers/transport`, not in the @@ -1969,7 +1969,7 @@ and checks the control still reads the compiled one. ## MoonCloud: share a MoonLive script over MoonTalk (2026-09-10) -Sketched 2026-09-10 while Talk was built. **The unique feature nobody else has**: projectMM ships a scripting language whose programs are about a kilobyte of text, and a message board between devices. Together they mean a script someone wrote on their wall is one tap from running on yours. +Sketched 2026-09-10 while Talk was built. **The unique feature nobody else has**: MoonLight ships a scripting language whose programs are about a kilobyte of text, and a message board between devices. Together they mean a script someone wrote on their wall is one tap from running on yours. **What makes it plausible.** The scripts are tiny: the shipped `.mle` files run 780 to 1907 bytes and the whole library of twenty-odd is 36 KB. A script is self-contained by design, so the text is the artifact. And the device already compiles and runs arbitrary script text safely, which is the hard half and is done. diff --git a/docs/work/future/backlog-light.md b/docs/work/future/backlog-light.md index ed7a59a2..47659af7 100644 --- a/docs/work/future/backlog-light.md +++ b/docs/work/future/backlog-light.md @@ -328,7 +328,7 @@ The shipped render↔encode split (Step 2a, `multicore` control) uses one `Drive ### Frame pacing — decided against (record) -MoonLight targets a fixed 60 fps; projectMM deliberately does not (settled with the PO 2026-07-12). The architecture is *render-uncapped + time-aware effects* (`beatsin8`/`millis()`-driven, a CLAUDE.md hard rule), so a whole-engine fps cap is redundant with that rule and would only *reduce* quality below the hardware ceiling; the LED wire rate already paces render physically (30 µs/light), and UI/WiFi responsiveness comes from the per-tick `vTaskDelay(1)` yield, not frame-rate control. Parked as a ~15-line opt-in (`targetFps=0` = unlimited default) *only if* a genuinely CPU-starved device ever appears. +MoonLight targets a fixed 60 fps; MoonLight deliberately does not (settled with the PO 2026-07-12). The architecture is *render-uncapped + time-aware effects* (`beatsin8`/`millis()`-driven, a CLAUDE.md hard rule), so a whole-engine fps cap is redundant with that rule and would only *reduce* quality below the hardware ceiling; the LED wire rate already paces render physically (30 µs/light), and UI/WiFi responsiveness comes from the per-tick `vTaskDelay(1)` yield, not frame-rate control. Parked as a ~15-line opt-in (`targetFps=0` = unlimited default) *only if* a genuinely CPU-starved device ever appears. ### Brightness belongs on a fixture's DIMMER channel, not only in the color values (WANTED) @@ -449,7 +449,7 @@ Do it as its own increment. The multi-destination unicast it builds on has shipp ### RS-485 / DMX-512 wired output (future) — the physical-DMX driver -projectMM already speaks DMX **over the network** (Art-Net / sACN via `NetworkReceiveEffect`). The missing half is **wired DMX-512 out**: driving DMX fixtures (moving heads, par cans, wired pixel controllers) directly over an RS-485 differential pair, which is what the RS-485 hardware on carrier boards like the [MHC-WLED ESP32-P4 shield](../../reference/hardware/mhc-wled-esp32-p4-shield.md) is *for*. DMX-512 is a 250 kbps async serial frame (a break + mark-after-break + 513 bytes: start code + 512 channels) shipped over RS-485 — the textbook fixture-control transport. A DMX driver would map the light buffer (or a fixture/attribute model — see the [Fixture model — moving heads, beams](#fixture-model-moving-heads-beams-long-term) item below) to DMX channels and clock the frame out a UART in RS-485 mode. +MoonLight already speaks DMX **over the network** (Art-Net / sACN via `NetworkReceiveEffect`). The missing half is **wired DMX-512 out**: driving DMX fixtures (moving heads, par cans, wired pixel controllers) directly over an RS-485 differential pair, which is what the RS-485 hardware on carrier boards like the [MHC-WLED ESP32-P4 shield](../../reference/hardware/mhc-wled-esp32-p4-shield.md) is *for*. DMX-512 is a 250 kbps async serial frame (a break + mark-after-break + 513 bytes: start code + 512 channels) shipped over RS-485 — the textbook fixture-control transport. A DMX driver would map the light buffer (or a fixture/attribute model — see the [Fixture model — moving heads, beams](#fixture-model-moving-heads-beams-long-term) item below) to DMX channels and clock the frame out a UART in RS-485 mode. **What it needs that we don't have yet:** - **A `platform::` UART-RS485 seam.** The ESP32 UART has a hardware RS-485 half-duplex mode (`uart_set_mode(UART_MODE_RS485_HALF_DUPLEX)`) that auto-drives the transceiver's **DE/RE** (driver-enable / receiver-enable) line — the thing our current pin handling has no concept of (we drive pins as plain GPIO). A DMX driver is where DE/RE control first earns its place, and only for a **bidirectional** channel: firmware DE/RE toggling is what lets one channel switch Tx↔Rx without a hardware switch. A **fixed-transmit** channel needs none — its transceiver is hard-wired to drive. On the [MHC-WLED ESP32-P4 shield](../../reference/hardware/mhc-wled-esp32-p4-shield.md) that split is physical: GPIO 4, 22, 24 are fixed-transmit (no DE/RE control wanted), and only the switchable GPIO 3 channel is bidirectional — the shield handles it with a *mechanical* slide switch (which is how its loopback works). Firmware DE/RE control is what a board would need to make a channel bidirectional *without* such a switch. @@ -464,7 +464,7 @@ Sequencing: it's a **driver** (`src/light/drivers/`) + a platform UART-RS485 sea ## Integration with other LED and visuals tools -Distilled from a Discord thread with panel-card users (2026-08-24), where two people drove ColorLight walls from projectMM and described the pipelines they already run. +Distilled from a Discord thread with panel-card users (2026-08-24), where two people drove ColorLight walls from MoonLight and described the pipelines they already run. ### Preview does not resume after a long tab hibernation (observed once, 2026-08-27) @@ -512,17 +512,17 @@ signature (the 2D-DMA blitter the WLED-MM-P4 world uses via LovyanGFX; ours woul platform layer, no vendored GFX library), Porter-Duff alpha when a real consumer arrives, and MoonLive sprite data (needs the stage-3 builtin table + arrays). -### projectMM as a video source — NDI first, Spout/Syphon only if proven (open) +### MoonLight as a video source — NDI first, Spout/Syphon only if proven (open) -Users asked for projectMM's rendered output to feed *their* tools, not the other way round. One runs OBS → Spout → his own VLAN-tagged card driver; he asked whether projectMM could be a Spout source. Input is not the gap: `NetworkReceiveEffect` already binds Art-Net, E1.31/sACN and DDP at once and answers ArtPoll, so any controller can already drive projectMM. +Users asked for MoonLight's rendered output to feed *their* tools, not the other way round. One runs OBS → Spout → his own VLAN-tagged card driver; he asked whether MoonLight could be a Spout source. Input is not the gap: `NetworkReceiveEffect` already binds Art-Net, E1.31/sACN and DDP at once and answers ArtPoll, so any controller can already drive MoonLight. **NDI is the recommended first implementation.** It is the AV industry's standard for video over IP, one implementation covers Windows, macOS, Linux and ARM, it discovers by name, and it crosses machines. Spout (Windows, DirectX/OpenGL) and Syphon (macOS, Metal/OpenGL) share a GPU texture zero-copy, so they are lower latency and bit-exact, but they are **same-machine only**, are **two** platform implementations, and leave **Linux and the Pi with nothing**. At LED-wall pixel counts (a 256x256 wall is 65K pixels) the latency difference is far below one frame of the render loop, so it does not decide the choice; coverage does. A Spout user is also reachable through NDI in one hop, since OBS, Resolume and TouchDesigner all speak both. -**The licence shapes the design, and the shape is already established here.** projectMM is GPL-3.0 and the NDI runtime is proprietary, so projectMM must not *redistribute* it: bundling would require projectMM's own licence to carry NDI's restrictions downstream, which GPL-3 forbids. The user installs the NDI runtime themselves, exactly as they already install **Npcap** for the panel-card driver, and projectMM calls whatever is present. +**The licence shapes the design, and the shape is already established here.** MoonLight is GPL-3.0 and the NDI runtime is proprietary, so MoonLight must not *redistribute* it: bundling would require MoonLight's own licence to carry NDI's restrictions downstream, which GPL-3 forbids. The user installs the NDI runtime themselves, exactly as they already install **Npcap** for the panel-card driver, and MoonLight calls whatever is present. That is the arrangement `platform_desktop.cpp` uses for Npcap today: resolve the library with `LoadLibrary`/`dlopen` rather than linking it, declare the handful of functions with the library's own signatures rather than including its headers (so the SDK never becomes a build requirement for CI or contributors), and report the feature unavailable when it is absent instead of failing to link. Two independent installs that talk to each other, like Resolume on the same desktop. -Also note projectMM renders into a CPU buffer, so a Spout/Syphon path would upload to the GPU purely to hand off, spending the zero-copy advantage it was chosen for. +Also note MoonLight renders into a CPU buffer, so a Spout/Syphon path would upload to the GPU purely to hand off, spending the zero-copy advantage it was chosen for. ### M5Stack Tab5 as a display target — MIPI-DSI, not the H.264 path (open) @@ -537,7 +537,7 @@ a codec. So the three things a Tab5 could be are separate pieces of work, and on panel is incidental, and this needs nothing beyond the P4 HLS work itself. - **A local wall preview or touch console** — the interesting one, and the real ask: a `platform::` MIPI-DSI display seam plus a UI on the panel. Related to the PPA acceleration noted under sprite - follow-ups above (same 2D-DMA block), and it is a display *output* seam projectMM does not have + follow-ups above (same 2D-DMA block), and it is a display *output* seam MoonLight does not have today; the nearest prior art is the WLED-MM-P4 world's LovyanGFX usage, which we would not vendor. - **An HLS/video player**, showing another device's stream: blocked on the missing hardware decoder, so not worth planning. @@ -558,7 +558,7 @@ color-temperature feature below would key on. The industry-standard answer is **daisy-chaining** — a sending card's ports each drive a chain, and each card takes its region by position in the chain. That user works around it with per-card VLANs and a managed switch instead, which he built for throughput and for per-card color-temperature grouping across mixed panel batches; he described it as his own solution, not a standard. -**Establish first whether a daisy chain already works with projectMM** (one contact has a 96K daisy-chained rig). If the cards self-assign by chain position, the standard multi-card case is already solved and nothing is needed. Only if it does not work is there a feature here, and it should follow the daisy-chain standard rather than the VLAN workaround. 802.1Q tagging is technically a clean fit for a raw-L2 sender (the tag is part of the Ethernet header, the switch strips it before the card, so card firmware is unaffected), but it serves one bespoke architecture. +**Establish first whether a daisy chain already works with MoonLight** (one contact has a 96K daisy-chained rig). If the cards self-assign by chain position, the standard multi-card case is already solved and nothing is needed. Only if it does not work is there a feature here, and it should follow the daisy-chain standard rather than the VLAN workaround. 802.1Q tagging is technically a clean fit for a raw-L2 sender (the tag is part of the Ethernet header, the switch strips it before the card, so card firmware is unaffected), but it serves one bespoke architecture. ### Smaller asks from the same thread @@ -613,9 +613,9 @@ not), and a module each. ### Read a vehicle's CAN bus and drive lights from it (2026-09-11) -A projectMM device in a car, reading the vehicle's own bus and driving aftermarket lighting from what it sees: underglow that pulses with engine RPM, sweeps with the indicator, flares on the brake. The vehicle to develop against is a VW Transporter (the product owner's). +A MoonLight device in a car, reading the vehicle's own bus and driving aftermarket lighting from what it sees: underglow that pulses with engine RPM, sweeps with the indicator, flares on the brake. The vehicle to develop against is a VW Transporter (the product owner's). -**Read-only, and that is the design, not a limitation.** Writing to a vehicle's bus reaches systems that are mandated safety equipment (indicators and brake lights are UNECE R48), usually on a segment shared with ABS and airbags, and modifying them is illegal in most jurisdictions. Reading has none of that exposure and gets the interesting half anyway: the car reports its state, projectMM renders it. Anything the device drives is aftermarket lighting on its own output, untouched by the vehicle. +**Read-only, and that is the design, not a limitation.** Writing to a vehicle's bus reaches systems that are mandated safety equipment (indicators and brake lights are UNECE R48), usually on a segment shared with ABS and airbags, and modifying them is illegal in most jurisdictions. Reading has none of that exposure and gets the interesting half anyway: the car reports its state, MoonLight renders it. Anything the device drives is aftermarket lighting on its own output, untouched by the vehicle. **A Service, not a driver.** It senses rather than renders, so it belongs under the core `Services` container beside AudioService and ButtonService, and it produces both shapes the sensors entry above describes: an EVENT (indicator on, brake pressed) drives a control through `Scheduler::setControl`, while a continuous VALUE (RPM, speed) is published as a shared frame the way `AudioService::latestFrame()` is. Effects then read it, the input-mapping layer routes it, and a MoonLive script reads it as a system variable, so "underglow sweeps with the indicator" is a script someone writes and shares rather than firmware. @@ -626,13 +626,13 @@ A projectMM device in a car, reading the vehicle's own bus and driving aftermark | OBD-II / UDS | Yes, legally mandated (EU: 2001 petrol, 2004 diesel) | RPM, speed, coolant, throttle, engine load | Request/response against ID `0x7DF`, so polling, a few times a second | | Manufacturer body frames | No, proprietary per model and year | Indicators, brakes, doors, lights | Passive sniffing and reverse engineering | -J1939 is NOT the answer here despite being the "CAN lighting standard" people reach for: it is the heavy-duty protocol (trucks, agricultural, marine), and a Transporter is passenger-car architecture running VW's own frames. J1939 would only apply if projectMM targeted commercial vehicle equipment. +J1939 is NOT the answer here despite being the "CAN lighting standard" people reach for: it is the heavy-duty protocol (trucks, agricultural, marine), and a Transporter is passenger-car architecture running VW's own frames. J1939 would only apply if MoonLight targeted commercial vehicle equipment. So phase 1 is OBD-II only, which genuinely works on any car and needs no per-vehicle data. Phase 2 adds raw-frame sniffing, and there the frame IDs must be CONFIGURATION rather than compiled-in constants: a Transporter owner who discovers theirs shares a config file, and no firmware release is involved. That is what keeps a per-vehicle feature from becoming a per-vehicle maintenance burden. **What it needs:** -- A `platform::twai*` seam (init, send, receive, stop). TWAI is the ESP32's CAN controller and is present on every chip projectMM ships: 1 controller on classic and S3, 3 on the P4. +- A `platform::twai*` seam (init, send, receive, stop). TWAI is the ESP32's CAN controller and is present on every chip MoonLight ships: 1 controller on classic and S3, 3 on the P4. - **An external transceiver**: the ESP32 has the controller but no CAN PHY, and this is the only part that needs buying. An SN65HVD230 board (3.3V native, unlike the 5V TJA1050) plus an OBD-II pigtail to reach pins 6 (CAN-H) and 14 (CAN-L), so nothing on the vehicle is cut. Watch for a termination resistor fitted on the transceiver board: the vehicle bus is already terminated at both ends and a third resistor disturbs it. Power the device from USB while developing rather than OBD pin 16, so a crash cannot load the vehicle supply. - A `VehicleService` module, and a desktop stub so the logic is testable without a car. diff --git a/docs/work/future/generative-fields-analysis-bottom-up.md b/docs/work/future/generative-fields-analysis-bottom-up.md index 20c79e80..77c4bfc7 100644 --- a/docs/work/future/generative-fields-analysis-bottom-up.md +++ b/docs/work/future/generative-fields-analysis-bottom-up.md @@ -1,6 +1,6 @@ # Generative fields — bottom-up analysis -> **Forward-looking research document — exception to CLAUDE.md present-tense rule.** A Stage-1 bottom-up analysis of the effect family this document names *generative fields*: images computed from noise fields over a coordinate mapping, either fresh every frame (a shader) or by transporting the previous frame through a velocity field (advection). Part 1 states what we want to build in the terms of the field's primary sources. Part 2 lists implementations that do similar things and where each fills in a spot. Part 3 places the family in projectMM's architecture: what exists, measured, and what is missing. The **top-down** companion (to be written from the prompt at the end) turns Part 3 into the implementation spec. Written 2026-09-03. Modeled on [power-functions-analysis-bottom-up.md](power-functions-analysis-bottom-up.md), which this extends: the fields, shader and particle families it cataloged are taken as read. +> **Forward-looking research document — exception to CLAUDE.md present-tense rule.** A Stage-1 bottom-up analysis of the effect family this document names *generative fields*: images computed from noise fields over a coordinate mapping, either fresh every frame (a shader) or by transporting the previous frame through a velocity field (advection). Part 1 states what we want to build in the terms of the field's primary sources. Part 2 lists implementations that do similar things and where each fills in a spot. Part 3 places the family in MoonLight's architecture: what exists, measured, and what is missing. The **top-down** companion (to be written from the prompt at the end) turns Part 3 into the implementation spec. Written 2026-09-03. Modeled on [power-functions-analysis-bottom-up.md](power-functions-analysis-bottom-up.md), which this extends: the fields, shader and particle families it cataloged are taken as read. ## TL;DR @@ -15,7 +15,7 @@ - **Every algorithm has a name and an originator.** Perlin noise (1985, 2002), fBm, domain warping (Quilez), semi-Lagrangian advection and stable fluids (Stam 1999, 2003), curl noise (Bridson 2007), flow-field particle tracing (Hobbs; Shiffman), Wu's anti-aliased lines (1991), coverage from signed distance, exponential half-life decay, LFO modulation. None needs more than adds, multiplies, a sine table and a noise function (Part 1). - **The cost model is arithmetic and decides the design.** A shader costs samples per pixel and scales with area × samples; advection costs a few loads and lerps per pixel per pass and scales with area × channels, plus memory for color state above 8 bits. For both, framerate is part of the rendering method: transport must stay sub-pixel per frame, decay must not step, oscillators must not alias. Throughput in pixels per second, not frames per second, is the honest metric (Part 1 § Cost). - **The field has converged on the same shapes.** A polar-noise shader engine with an oscillator bank and a coordinate mapper; an emitter-plus-flow advection engine with six flow types, a modulator bank and a half-life fade; both now available in Q16.16 fixed point with bit-exact tests, on the ESP32 class we target; anti-aliased canvases; wave and fluid solvers; particle-trail systems. These are similar to what Part 1 describes, not identical, and each fills in a spot: measured throughput, parameter vocabularies users expect, fixed-point choices that work, and precision decisions (Part 2). -- **projectMM has most of the blocks and lacks three.** Present: polar addressing, 16-bit value noise with fBm/warp/turbulence, 16-bit oscillators, palettes, a GLSL-vocabulary shader runner, SDF coverage, a 24.8 sub-pixel splat, particles, a persisting Layer buffer, and 55 MoonLive builtins that already express a polar-noise pixel. Missing: **a bilinear resampler of the previous frame** (so no advection), **color state above 8 bits** (so trails posterize and sub-integer accumulation is impossible), and **gradient noise** (value noise reads coarser at low frequency). The MoonLive gap is structural: per-pixel host calls cost ~5 µs per pixel measured, and a script has no frame of state; both families want whole-frame kernels a script composes (Part 3). +- **MoonLight has most of the blocks and lacks three.** Present: polar addressing, 16-bit value noise with fBm/warp/turbulence, 16-bit oscillators, palettes, a GLSL-vocabulary shader runner, SDF coverage, a 24.8 sub-pixel splat, particles, a persisting Layer buffer, and 55 MoonLive builtins that already express a polar-noise pixel. Missing: **a bilinear resampler of the previous frame** (so no advection), **color state above 8 bits** (so trails posterize and sub-integer accumulation is impossible), and **gradient noise** (value noise reads coarser at low frequency). The MoonLive gap is structural: per-pixel host calls cost ~5 µs per pixel measured, and a script has no frame of state; both families want whole-frame kernels a script composes (Part 3). - **The ESP32 budget is known.** ~293 cycles per pixel at 128×128 @ 50 fps on 240 MHz; one noise sample per pixel measures ~750 cycles on the S3 today. A rich shader is a panel-class effect on any MCU; advection scales to walls in cycles and needs PSRAM for state. Every target has an FPU, the P4 and S31 add SIMD and hardware loops, the P4 measures ~3× the S3 and a desktop core 20-40×, so the P4 and S31 are where this family shines on an MCU: the ESP32 class stops at about one noise sample per pixel on a 128² wall, and the desktop continues on the same contract and drives the wall over the network. The levers are standard: LUTs, fixed point, fewer samples, a field below output resolution, a field below frame rate, per-target FPU and SIMD behind one contract (Part 3 § Budget, § Per-target headroom). - **Out of scope for Stage 1.** API names and signatures; the 8-bit versus 16-bit buffer decision; where advection state lives; the MoonLive frame-kernel shape; which showcase effects come first. All Stage 2 (Part 3 § Bridge). @@ -23,7 +23,7 @@ The product owner's goals for this family, recorded 2026-09-03: -1. **projectMM supports the building blocks of these effects**, via compiled functions and especially via MoonLive effects, on the power-function library ([power-functions.md](../../moonmodules/light/power-functions.md)), the same way it carries particles and SDFs. +1. **MoonLight supports the building blocks of these effects**, via compiled functions and especially via MoonLive effects, on the power-function library ([power-functions.md](../../moonmodules/light/power-functions.md)), the same way it carries particles and SDFs. 2. **A few genuinely beautiful showcase effects** are built on those blocks, as the proof the blocks are right. 3. **The ESP32 is CPU-bound; memory is not the constraint.** Every choice is made against the per-pixel cycle budget, and the fact that these effects look better the higher the framerate is a design input. 4. **Industry-standard terminology and algorithms throughout**, per [CLAUDE.md § Principles](../../CLAUDE.md#principles): the textbook construct, named by its textbook name, from the primary source. @@ -124,20 +124,20 @@ Implementations that do similar things to Part 1. None is the specification; eac | **Flow-field generative art** (Hobbs; Shiffman) | A grid of angles from noise; particles or pen strokes follow the angle in small steps | Part 1 noise-as-motion | The tracing form of a flow field, where the particle is the source and the field never touches the frame; Hobbs's advice to distort with something other than Perlin noise once the look is familiar | | **Stable fluids** (Stam 1999, GDC 2003) | The reference solver and its "linear backtrace" advection | Part 1 transport | The algorithm every advection effect above descends from, and the reason semi-Lagrangian advection is unconditionally stable at any `dt` | | **Earlier 8-bit work in the same idiom** (FunkyNoise, FunkyClouds, 2014; a self-modulating simplex noise gist) | Polar angle per pixel shifted by noise; noise modulating the offsets, scale and palette index of the next noise pass | Part 1 warping, feedback modulation | That the idiom predates FPUs and was first done in 8-bit FastLED terms | -| **MoonLight's port of ColorTrails** (2026-03) and **FastLED-MM's FlowFields sketch** (2026-04/05) | The advection engine on `fl::CanvasRGB` + `s16x16`; the same engine at 128×128 inside projectMM's earlier module runtime with 35 registered controls | Part 1 advection, in our own prior work | That the family has already run on our hardware and behind our UI; a bench reference for the advection showcase | +| **MoonLight's port of ColorTrails** (2026-03) and **FastLED-MM's FlowFields sketch** (2026-04/05) | The advection engine on `fl::CanvasRGB` + `s16x16`; the same engine at 128×128 inside MoonLight's earlier module runtime with 35 registered controls | Part 1 advection, in our own prior work | That the family has already run on our hardware and behind our UI; a bench reference for the advection showcase | Two things the examples agree on that Part 1 states as requirements: **compute above 8 bits and quantize last**, and **report pixels per second**. One thing none of them does yet: curl noise as a velocity rule, which Part 1 includes because it is the standard cheap answer to fluid-looking flow. --- -# Part 3: How this fits projectMM +# Part 3: How this fits MoonLight ## What we have Against Part 1's blocks, measured on this tree: -| Part 1 block | projectMM today | Status | +| Part 1 block | MoonLight today | Status | |---|---|---| | Polar mapping | `atan16`, `dist16`, `kaleido` ([math16.h](../../src/core/util/math16.h)), computed per pixel; `PolarNoiseEffect` uses them | ✅ per pixel; ⬜ no precomputed LUT type | | Polar transforms, `uv` | `shader.h`: `uv`, `rotate`, `repeat`, `mirror`; `kaleido` | ✅ | @@ -228,13 +228,13 @@ In Part 1's order, against the table above: ## Bridge to the top-down -The bottom-up settles what the top-down can take as given: the family is two techniques over one shared block set, every block has a textbook name and algorithm, the cost model is arithmetic and makes a shader a samples-per-pixel problem and advection a cycles-per-frame-and-memory problem, framerate is the rendering method for both, the field has proven the whole set exact in Q16.16 on our silicon class, and projectMM lacks three blocks (resampler, wide color state, gradient noise) plus the bank, the LUT and the kernels that make them composable. What it leaves open is design: names and signatures in our fixed-point vocabulary; where wide state lives and whether the Layer goes to 16 bits; the MoonLive frame-kernel shape and its dependency on the host-call ABI; which per-target accelerations sit behind the one contract; the three showcases to the control level; the tests, the timing contracts, and the order the work lands in. The top-down owns those decisions and their resource accounting. +The bottom-up settles what the top-down can take as given: the family is two techniques over one shared block set, every block has a textbook name and algorithm, the cost model is arithmetic and makes a shader a samples-per-pixel problem and advection a cycles-per-frame-and-memory problem, framerate is the rendering method for both, the field has proven the whole set exact in Q16.16 on our silicon class, and MoonLight lacks three blocks (resampler, wide color state, gradient noise) plus the bank, the LUT and the kernels that make them composable. What it leaves open is design: names and signatures in our fixed-point vocabulary; where wide state lives and whether the Layer goes to 16 bits; the MoonLive frame-kernel shape and its dependency on the host-call ABI; which per-target accelerations sit behind the one contract; the three showcases to the control level; the tests, the timing contracts, and the order the work lands in. The top-down owns those decisions and their resource accounting. ### Prompt for the agent that writes the top-down > Read `CLAUDE.md`, `docs/explanation/architecture/index.md` (§ Hot path discipline, § Effects and Layer, § Buffer persistence, § MoonLive), `docs/contributing/coding-standards.md`, `docs/backlog/generative-fields-analysis-bottom-up.md` (this document, in full: Part 1 is the specification language, Part 2 the examples, Part 3 the gap), `docs/backlog/power-functions-analysis-top-down.md` (the shape and the standing decisions: dimension-generic, one contract everywhere with per-target acceleration, fixed point invisible to the writer, the 16-bit contract, particles as the stateful precedent), `docs/moonmodules/light/power-functions.md` (what exists), and `docs/backlog/moonlive-language-roadmap.md` § 4c and § 2 (the per-pixel call cost and the multi-argument host-call blocker). > -> Write `docs/backlog/generative-fields-analysis-top-down.md`: the implementation spec for the generative-fields family in projectMM, for compiled effects and for MoonLive scripts, on the power-function library. Use the house format of the power-functions top-down (status legend, TL;DR, numbered sections, decisions for sign-off, out of scope). Present tense for what exists, forward-looking only under the banner. American spelling, no em-dashes. Specify every kernel from Part 1's primary sources by its textbook name and algorithm; use Part 2 only for measured numbers, parameter vocabularies and precision choices, never as a source of code, and record prior art per module the way `PolarNoiseEffect.h` does. +> Write `docs/backlog/generative-fields-analysis-top-down.md`: the implementation spec for the generative-fields family in MoonLight, for compiled effects and for MoonLive scripts, on the power-function library. Use the house format of the power-functions top-down (status legend, TL;DR, numbered sections, decisions for sign-off, out of scope). Present tense for what exists, forward-looking only under the banner. American spelling, no em-dashes. Specify every kernel from Part 1's primary sources by its textbook name and algorithm; use Part 2 only for measured numbers, parameter vocabularies and precision choices, never as a source of code, and record prior art per module the way `PolarNoiseEffect.h` does. > > Decide, with rationale and resource accounting per target (classic ESP32 without PSRAM, S3, P4, desktop): (1) the power functions to add for Part 3's ten items, with signatures in the repo's fixed-point vocabulary; (2) where wide color state lives (a 16-bit Layer buffer versus effect-owned Q16.16 state quantized into the 8-bit Layer), with the memory table per fixture size and the dithering plan; (3) the MoonLive frame-kernel shape: how a script composes whole-frame passes and reads a per-pixel shader through inline ops rather than per-pixel host calls, and which language-roadmap blockers it depends on; (4) the performance plan against Part 3's measured budget and per-target headroom table: samples-per-pixel targets for shaders, cycles-per-frame and bytes-of-state targets for advection, the field-below-resolution and field-below-frame-rate levers, which kernels get FPU or SIMD (PIE) variants per target behind the one contract, where the desktop takes over as the processing node, and how framerate is protected as the rendering method; (5) the three showcases, specified to the control level; (6) the tests that pin each kernel (golden frames on the desktop, per-target timing contracts in scenarios) and the bench criteria the product owner judges by eye on the S3 and on a wall; (7) the order the work lands in, each step verifiable on the desktop first. > diff --git a/docs/work/future/generative-fields-analysis-top-down.md b/docs/work/future/generative-fields-analysis-top-down.md index f1c4fbf3..1295fd43 100644 --- a/docs/work/future/generative-fields-analysis-top-down.md +++ b/docs/work/future/generative-fields-analysis-top-down.md @@ -24,7 +24,7 @@ An unmarked line is context or rationale. ## 0. Inputs and the decisions taken -From the bottom-up: two techniques over one block set; every block named from its primary source; the cost model; projectMM's three missing blocks (previous-frame resampler, color state above 8 bits, gradient noise) plus the LUT, the bank and the kernels that compose them; the per-target headroom table with the P4 and S31 as the MCU home of the family. From the power-functions top-down, carried unchanged: dimension-generic; one contract everywhere with per-target acceleration; fixed point invisible to the writer; the 16-bit contract (`angle16`, `frac16`, `pos_t` 24.8); particles as the stateful precedent; determinism (pure function of position, time, seed; stateful kernels declare a resync point); golden frames as hashes; the `Canvas` descriptor. +From the bottom-up: two techniques over one block set; every block named from its primary source; the cost model; MoonLight's three missing blocks (previous-frame resampler, color state above 8 bits, gradient noise) plus the LUT, the bank and the kernels that compose them; the per-target headroom table with the P4 and S31 as the MCU home of the family. From the power-functions top-down, carried unchanged: dimension-generic; one contract everywhere with per-target acceleration; fixed point invisible to the writer; the 16-bit contract (`angle16`, `frac16`, `pos_t` 24.8); particles as the stateful precedent; determinism (pure function of position, time, seed; stateful kernels declare a resync point); golden frames as hashes; the `Canvas` descriptor. Product-owner decisions, 2026-09-03: diff --git a/docs/work/future/led-driver-psram-ring-analysis.md b/docs/work/future/led-driver-psram-ring-analysis.md index 1925d577..d3b8d627 100644 --- a/docs/work/future/led-driver-psram-ring-analysis.md +++ b/docs/work/future/led-driver-psram-ring-analysis.md @@ -1,6 +1,6 @@ # The classic-ESP32 PSRAM ceiling — the refill-ring memory model -> **Forward-looking research document — exception to the CLAUDE.md present-tense rule.** One question: **can PSRAM lift the classic-ESP32 LED ceiling projectMM measured at 2048 lights?** Researched 2026-07 against primary sources — hpwit's driver source read line-by-line, plus Espressif's own docs. Every load-bearing claim is cited in § 4. +> **Forward-looking research document — exception to the CLAUDE.md present-tense rule.** One question: **can PSRAM lift the classic-ESP32 LED ceiling MoonLight measured at 2048 lights?** Researched 2026-07 against primary sources — hpwit's driver source read line-by-line, plus Espressif's own docs. Every load-bearing claim is cited in § 4. > > **Status: PARKED FINDING — not a queued task.** The answer is **yes, via a refill ring** — but that is a **second classic-ESP32 driver** (it cannot be a flag on the shipped i80 driver), and **nothing currently planned needs it**: the shift-register driver builds on the existing i80/Parlio base, S3/P4-first, where the DMA already reaches PSRAM. **§ 3 holds the verdict and the trigger that would un-park it.** Read § 3 before treating anything here as a plan. @@ -18,7 +18,7 @@ hpwit's driver uses a **streaming refill ring** instead: a handful of tiny DMA b ### 2.1 Two memory models -**Whole-frame (projectMM today, via `esp_lcd` i80).** The DMA buffer *is* the frame: every light's expanded WS2812 waveform — 3 slots per bit, one byte (8-lane) or two (16-lane) per slot — materialised in one contiguous DMA-capable block, handed to the peripheral in a single chained transfer. The CPU is out of the loop for the whole frame, which is why this model is **underrun-immune by construction**: nothing remains to refill, so no ISR storm can starve it. The price is linear scaling: +**Whole-frame (MoonLight today, via `esp_lcd` i80).** The DMA buffer *is* the frame: every light's expanded WS2812 waveform — 3 slots per bit, one byte (8-lane) or two (16-lane) per slot — materialised in one contiguous DMA-capable block, handed to the peripheral in a single chained transfer. The CPU is out of the loop for the whole frame, which is why this model is **underrun-immune by construction**: nothing remains to refill, so no ISR storm can starve it. The price is linear scaling: ```text internalBytes(whole-frame) = maxLaneLights × channels × 3 slots × slotBytes @@ -163,13 +163,13 @@ Keeping i80 is **not** legacy baggage: for a ≤2 K install on a WiFi-busy board - [I2SClocklessVirtualLedDriver](https://github.com/hpwit/I2SClocklessVirtualLedDriver) — "8 strips out of one single pin … 8x15=120 strips"; 74HC595 per virtual pin + 74HC245 level shifter; `__NB_DMA_BUFFER` default 2, buffers `(NUM_VIRT_PINS+1) * nb_components * 8 * 3 * 2` ≈ 1152 B; "Artifacts due to interrupts" → "calculate several buffers in advance"; `enableShowPixelsOnCore()`; 12,000-LED worked example; 75 → 129 fps second-core figures. - [I2SClockLessLedDriveresp32s3](https://github.com/hpwit/I2SClockLessLedDriveresp32s3) — the S3 lineage (PSRAM buffers; S3 EDMA can DMA from PSRAM). -- [MoonLight](https://moonmodules.org/MoonLight/) / [WLED-MM](https://github.com/MoonModules/WLED-MM) — a prior firmware riding hpwit's ring on PSRAM: LEDs pre-allocated in PSRAM, light boundary **130 K** (vs **4096** non-PSRAM). The empirical demonstration that the ring's ceiling lift is real. +- [MoonLight](https://moonmodules.org/projectMM/) / [WLED-MM](https://github.com/MoonModules/WLED-MM) — a prior firmware riding hpwit's ring on PSRAM: LEDs pre-allocated in PSRAM, light boundary **130 K** (vs **4096** non-PSRAM). The empirical demonstration that the ring's ceiling lift is real. **Espressif (the load-bearing hardware constraint):** - [`esp_lcd/i80/esp_lcd_panel_io_i2s.c`](https://github.com/espressif/esp-idf/blob/master/components/esp_lcd/i80/esp_lcd_panel_io_i2s.c) — `ESP_RETURN_ON_FALSE((caps & MALLOC_CAP_SPIRAM) == 0, NULL, TAG, "external memory is not supported");` - [Support for External RAM — ESP-IDF (ESP32)](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html) — external RAM "cannot be used as a place to store DMA transaction descriptors or as a buffer for a DMA transfer to read from or write into"; prescribed workaround = internal DMA-able buffer + copy. **Confirms: no IDF API feeds PSRAM into classic-ESP32 I2S DMA.** - [Support for External RAM — ESP-IDF (ESP32-S3)](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-guides/external-ram.html) — the S3 *does* have hardware DMA-to-PSRAM (descriptors still internal): why our S3 i80 reaches 16 K and classic cannot. -**projectMM's own measurements:** +**MoonLight's own measurements:** - [performance.md § Multi-pin LED driving](../../reference/performance.md#multi-pin-led-driving-all-three-peripherals-128128-grid) — classic i80 2048-light ceiling + `esp_lcd_i80_alloc_draw_buffer` rejecting `MALLOC_CAP_SPIRAM`; S3 16,384 @ ~34 fps; P4 Parlio 4096, 139 fps @ 1024; the `multicore` +44 % table. - [backlog-light.md](backlog-light.md) — the superseded chunk-streaming-ring decision; the shift-register driver's **48 × 256 = 12,288** acceptance floor. diff --git a/docs/work/future/leddriver-analysis-bottom-up.md b/docs/work/future/leddriver-analysis-bottom-up.md index 34c75126..3fcfbc6d 100644 --- a/docs/work/future/leddriver-analysis-bottom-up.md +++ b/docs/work/future/leddriver-analysis-bottom-up.md @@ -10,13 +10,13 @@ - **Identity-mapping fast path preserved.** When `MappingLUT::hasLUT()` is false, drivers receive a span pointing directly at `layer_->buffer()` — zero copy through the wire. This optimisation drove a major part of the architecture's shape. - **Feasibility on hpwit's code (read at HEAD, 2026-05-25).** Two-target paths interleaved through one file (30 `#ifdef CONFIG_IDF_TARGET_ESP32S3` blocks). The multiplex code isn't cleanly factored out today — refactoring it into a backend-agnostic `ShiftRegMultiplex` layer is real engineering work, not free composition. `Backend × ShiftReg` is high-confidence on classic ESP32 + S3 (hpwit's lib is the reference), medium-confidence on parlio + FlexIO (no reference, plausibly feasible), low-confidence on RP1 PIO (research-grade). - **What ESP-IDF gives us.** Peripheral plumbing (RMT5, parlio_tx_unit, esp_lcd_panel_io_i80, SPI master DMA, GDMA, GPIO LL) — yes. Anything LED-specific (WS2812 encoder, transposition helpers, multi-protocol abstraction) — no. The `led_strip` component is community, not first-party. Per-backend we'd write ~500-700 lines of new code; ~300-500 lines for the multiplex layer. -- **Recommendation: walk Scenario B (build ourselves).** Reasons in priority order: the multiplex axis is our projectMM-defining value-add and doesn't compose cleanly onto FastLED; runtime driver switching aligns with our own contract; the identity-mapping fast path needs the driver reading `layer_->buffer()` directly; IDF gives us the plumbing for free; license + binary-size tax shrinks Scenario A's surplus. **Cost accepted:** 3 weeks → 3 months to first working driver, mitigated by spike-ordering. +- **Recommendation: walk Scenario B (build ourselves).** Reasons in priority order: the multiplex axis is our MoonLight-defining value-add and doesn't compose cleanly onto FastLED; runtime driver switching aligns with our own contract; the identity-mapping fast path needs the driver reading `layer_->buffer()` directly; IDF gives us the plumbing for free; license + binary-size tax shrinks Scenario A's surplus. **Cost accepted:** 3 weeks → 3 months to first working driver, mitigated by spike-ordering. - **First two Stage-2 spikes (load-bearing).** (1) `LcdCamLedDriver (None)` on ESP32-S3 against the existing `Drivers::loop()` shape — ~1 week, proves the contract. (2) Refactor hpwit's S3 path into `LcdCamLedDriver (ShiftReg)` — ~2-3 weeks, validates that the multiplex can be factored out without losing performance. **If spike 2 fails, fall back to hybrid** (Scenario A for non-multiplex backends + Scenario B for multiplex). The architecture supports both. - **Out of scope for Stage 1.** Per-driver benchmarking on real hardware; PR-to-FastLED to expose its DMA buffer; final IDF version pin decision; license pick. Stage 2. ## Why this document exists -projectMM's `src/light/drivers/` ships an ArtNet send driver and a WebSocket preview driver. There is **no LED-strip driver yet** — that's the gap this survey serves. The pipeline expects a `DriverBase` (`src/light/drivers/Drivers.h:11`) child that reads from the Drivers container's shared output buffer and pushes bytes to hardware. The requirements (1-10K LEDs must, 10-30K should, 30K+ interesting, 50 FPS, hot-reconfigurable pin/protocol/count, WiFi-coexistent) narrow but don't pick a library. This document characterises the candidates. +MoonLight's `src/light/drivers/` ships an ArtNet send driver and a WebSocket preview driver. There is **no LED-strip driver yet** — that's the gap this survey serves. The pipeline expects a `DriverBase` (`src/light/drivers/Drivers.h:11`) child that reads from the Drivers container's shared output buffer and pushes bytes to hardware. The requirements (1-10K LEDs must, 10-30K should, 30K+ interesting, 50 FPS, hot-reconfigurable pin/protocol/count, WiFi-coexistent) narrow but don't pick a library. This document characterises the candidates. ## ESP32 — primary depth @@ -40,7 +40,7 @@ projectMM's `src/light/drivers/` ships an ArtNet send driver and a WebSocket pre - **Notably absent**: WLED-MM does **not** integrate hpwit's I2SClocklessLedDriver or the virtual driver — its HUB75 path uses `ESP32-HUB75-MatrixPanel-DMA` directly; its WS281x path is still NeoPixelBus via PolyBus. The "more pins for big installs" benefit comes from the upstream LCD-parallel I2S, not from a swap-in driver. - **Channel allocation tweaks**: `bus_wrapper.h:776-786` shifts RMT/I2S channel assignment to accommodate the I2S0 split. Adds `WLEDMM_SLOWPATH` / `WLEDMM_TWOPATH` build options. - **Maintenance**: active, 102 open issues. Smaller maintainer pool than upstream; product owner is co-maintainer here. -- **Field notes**: this fork's value-add is empirical tuning for installations beyond ~5K LEDs, not a fundamentally different driver architecture. If projectMM wants the WLED bus layer at all, the question is whether to take it from upstream or from MM. +- **Field notes**: this fork's value-add is empirical tuning for installations beyond ~5K LEDs, not a fundamentally different driver architecture. If MoonLight wants the WLED bus layer at all, the question is whether to take it from upstream or from MM. ### I2SClocklessLedDriver (hpwit) — https://github.com/hpwit/I2SClocklessLedDriver @@ -85,7 +85,7 @@ projectMM's `src/light/drivers/` ships an ArtNet send driver and a WebSocket pre - **Platforms**: ESP32 (classic, S2, S3) on a variety of dev boards (M5Stick, Heltec, LilyGo, plain DevKit). HUB75 via custom integration. - **Threading**: explicit multi-task design — render task pinned to core 1, networking and effects on core 0. The README's "core affinity" discipline is what makes the project recognisable. Documentation here is thin; the patterns live in the source. - **WiFi coexistence**: addressed by core pinning and explicit FreeRTOS task priorities, not by disabling WiFi. -- **GPL-3.0** is the key gotcha — copyleft constraints make code reuse in projectMM (under whatever final licence) a deal-breaker for direct copy. Read it for **patterns**, not lines. +- **GPL-3.0** is the key gotcha — copyleft constraints make code reuse in MoonLight (under whatever final licence) a deal-breaker for direct copy. Read it for **patterns**, not lines. - **Field notes**: the canonical example of "how to keep a multi-thousand-LED ESP32 install stable while WiFi is on". Worth studying, not vendoring. ### ESP-IDF `led_strip` — https://github.com/espressif/idf-extra-components/tree/master/led_strip @@ -161,7 +161,7 @@ Product owner's question: "Is there any way to do something with GPIO pins? E.g. Most libraries (NeoPixelBus, FastLED legacy RMT, Adafruit_NeoPixel) don't address this and rely on the CPU staying responsive — which is exactly when WiFi storms cause visible glitches. -**Hot reconfiguration.** A genuine differentiator. WLED's `BusManager::add()`/`removeAll()` supports runtime bus mutation but at the cost of full reallocation. hpwit's drivers require an `initled()` re-init for pin/count change. **FastLED master's runtime-driver-switch API** (May 2026) is the only library where switching the entire driver (RMT ↔ LCD) at runtime is a first-class supported operation. For projectMM, the requirement is GPIO/protocol/count via UI controls — that's "re-init on change" pattern, achievable with all of these but ergonomically cleanest with FastLED's new API. +**Hot reconfiguration.** A genuine differentiator. WLED's `BusManager::add()`/`removeAll()` supports runtime bus mutation but at the cost of full reallocation. hpwit's drivers require an `initled()` re-init for pin/count change. **FastLED master's runtime-driver-switch API** (May 2026) is the only library where switching the entire driver (RMT ↔ LCD) at runtime is a first-class supported operation. For MoonLight, the requirement is GPIO/protocol/count via UI controls — that's "re-init on change" pattern, achievable with all of these but ergonomically cleanest with FastLED's new API. ## Research question — can the virtual driver be reused on Teensy / Pi? @@ -182,9 +182,9 @@ The technique has **two components**: (a) parallel-clocked GPIO output via a per For the product owner to pick 2-3 from: 1. **FastLED master — modular driver layer** — what we'd learn: whether the new runtime driver-switching API and per-peripheral driver subdirectories are mature enough to use directly, or if vendoring a snapshot makes more sense. Estimated cost: 1.5 days. -2. **I2SClocklessVirtualLedDriver — fork-and-vendor scope** — what we'd learn: minimal API surface to wrap as a projectMM driver MoonModule; what config controls (pin map, brightness, gamma, virtual-pin count) need to be hot-reconfigurable; whether the init-time-only constraint is acceptable. Estimated cost: 1 day. -3. **WLED-MM bus layer extraction** — what we'd learn: whether the bus layer can be lifted out of WLED-MM and embedded in projectMM cleanly (or if it's too entangled with WLED's segment/effect plumbing). The EUPL-1.2 licence interaction with projectMM's licence is part of this. Estimated cost: 1 day. -4. **NeoPixelBus directly** — what we'd learn: bypass WLED entirely, use the underlying library as projectMM's WS281x backend. NeoPixelBus is what every WLED bus class wraps anyway. Estimated cost: 0.5 day. +2. **I2SClocklessVirtualLedDriver — fork-and-vendor scope** — what we'd learn: minimal API surface to wrap as a MoonLight driver MoonModule; what config controls (pin map, brightness, gamma, virtual-pin count) need to be hot-reconfigurable; whether the init-time-only constraint is acceptable. Estimated cost: 1 day. +3. **WLED-MM bus layer extraction** — what we'd learn: whether the bus layer can be lifted out of WLED-MM and embedded in MoonLight cleanly (or if it's too entangled with WLED's segment/effect plumbing). The EUPL-1.2 licence interaction with MoonLight's licence is part of this. Estimated cost: 1 day. +4. **NeoPixelBus directly** — what we'd learn: bypass WLED entirely, use the underlying library as MoonLight's WS281x backend. NeoPixelBus is what every WLED bus class wraps anyway. Estimated cost: 0.5 day. 5. **Hybrid: hpwit non-virtual + FastLED for non-WS281x** — what we'd learn: covers the 1-30K WS281x case with hpwit's I2S-LCD driver (no shift registers needed) and falls back to FastLED for APA102/SK9822/HD107S. Estimated cost: 1 day. 6. **ObjectFLED + FastLED Teensy path** — what we'd learn: feasibility of Teensy 4.1 as a parallel target alongside ESP32, especially for installations where WiFi is replaced by 4.1's built-in Ethernet. Estimated cost: 1 day. @@ -307,7 +307,7 @@ Below the line is **FastLED master** (the modular driver subdirectories `src/pla ```text +--------------------------------+--------------------------------------+ -| projectMM LedDriver (backend×Multiplex)| External implementation | +| MoonLight LedDriver (backend×Multiplex)| External implementation | +--------------------------------+--------------------------------------+ | RmtLedDriver (None) → FastLED::add | | LcdCamLedDriver (None) → FastLED::add | @@ -325,7 +325,7 @@ Below the line is **FastLED master** (the modular driver subdirectories `src/pla +--------------------------------+--------------------------------------+ ``` -Pros: ride FastLED's runtime driver-switch API and per-backend maturity (matches requirement 4). FastLED owns the peripheral churn (RMT4 → RMT5 → parlio API revisions); we own the multiplex transposition and the projectMM-side wiring. Teensy support comes essentially free via ObjectFLED. The multiplex code we vendor is small (~200 lines from hpwit's lib — just the transposition + cascade-shift protocol, not the peripheral init). ~2-3 weeks to a working ESP32 + Teensy hybrid; +1 week for the multiplex layer on top of FastLED lcd_cam. +Pros: ride FastLED's runtime driver-switch API and per-backend maturity (matches requirement 4). FastLED owns the peripheral churn (RMT4 → RMT5 → parlio API revisions); we own the multiplex transposition and the MoonLight-side wiring. Teensy support comes essentially free via ObjectFLED. The multiplex code we vendor is small (~200 lines from hpwit's lib — just the transposition + cascade-shift protocol, not the peripheral init). ~2-3 weeks to a working ESP32 + Teensy hybrid; +1 week for the multiplex layer on top of FastLED lcd_cam. Cons: FastLED master is in flux (May 2026 surface could shift), so we pin a commit SHA, not a version. The multiplex-on-top-of-FastLED-lcd_cam path needs FastLED to expose its DMA buffer for us to write into — its current API doesn't do that cleanly, so we either bypass FastLED for that one specific backend (effectively becoming Scenario B for it) or upstream a PR to FastLED adding the hook. **This is the brittle seam of Scenario A.** @@ -335,7 +335,7 @@ Below the line is **our own peripheral backends + our own multiplex layer**, all ```text +--------------------------------------+--------------------------------+ -| projectMM LedDriver (backend×Multiplex)| ESP-IDF / NXP HAL | +| MoonLight LedDriver (backend×Multiplex)| ESP-IDF / NXP HAL | +--------------------------------------+--------------------------------+ | RmtLedDriver (None) → rmt_tx_channel + encoder | | I2sLcdLedDriver (None) → legacy I2S in LCD mode + DMA | @@ -435,25 +435,25 @@ Per backend: ~500-700 lines of new code. For ESP32 (RMT) + S3 (LCD-CAM) + P4 (pa Importing a library means you get a lot that wasn't built for your case. Specifically, what we'd give up by adopting FastLED master under the line: -- **The pixel buffer layout is FastLED's, not ours.** Their CRGB struct, their channel ordering decisions, their gamma/brightness application points. We bridge — every `push()` call has to translate from projectMM's flat `std::span` (the existing `Buffer::data()` shape) into whatever FastLED's bus class expects. That bridge is a per-frame copy or a per-frame view rewrite; in either case it's hot-path overhead that disappears in Scenario B because we control both sides. +- **The pixel buffer layout is FastLED's, not ours.** Their CRGB struct, their channel ordering decisions, their gamma/brightness application points. We bridge — every `push()` call has to translate from MoonLight's flat `std::span` (the existing `Buffer::data()` shape) into whatever FastLED's bus class expects. That bridge is a per-frame copy or a per-frame view rewrite; in either case it's hot-path overhead that disappears in Scenario B because we control both sides. - **Hot-reconfigure is FastLED-paced.** Their new `Channel::create()` API is the closest the field has to runtime driver-switching, but it's young (May 2026), the API surface is in flux, and "switch RMT to LCD-CAM mid-flight without dropping a frame" is something we'd have to verify works the way we need — not assume. In Scenario B the contract is ours; if a frame drop on driver switch is unacceptable, we engineer around it. -- **No leverage on inner-loop optimisations specific to projectMM's MappingLUT / identity-mapping fast path.** FastLED knows nothing about our identity-mapping optimisation (`!hasLUT()` → direct Layer-buffer push). To use FastLED's bus path we either pre-compose into FastLED's CRGB buffer (defeating the identity path) or write a thin wrapper that exposes our raw buffer (possible but adds a coupling point we don't control). Scenario B keeps the identity path zero-copy through to the wire. -- **The multiplex seam is brittle on FastLED.** This is the strongest finding from the feasibility check above. FastLED has no shift-register multiplex backend. To use FastLED's lcd_cam DMA path *with* our shift-register multiplex, we need FastLED to hand us its DMA buffer for direct writes — its current API doesn't, and either (a) we bypass FastLED for that backend (effectively Scenario B for the most projectMM-defining backend), (b) we upstream a PR (slow, library-author dependent), or (c) we accept a per-frame copy from FastLED's buffer into our transposed buffer (hot-path cost). -- **No control over IDF version pinning.** FastLED master targets whatever IDF FastLED master targets. If our projectMM IDF version pin disagrees with FastLED's, we either bump them both in lockstep or carry a divergent fork. In Scenario B we own the IDF compatibility matrix. +- **No leverage on inner-loop optimisations specific to MoonLight's MappingLUT / identity-mapping fast path.** FastLED knows nothing about our identity-mapping optimisation (`!hasLUT()` → direct Layer-buffer push). To use FastLED's bus path we either pre-compose into FastLED's CRGB buffer (defeating the identity path) or write a thin wrapper that exposes our raw buffer (possible but adds a coupling point we don't control). Scenario B keeps the identity path zero-copy through to the wire. +- **The multiplex seam is brittle on FastLED.** This is the strongest finding from the feasibility check above. FastLED has no shift-register multiplex backend. To use FastLED's lcd_cam DMA path *with* our shift-register multiplex, we need FastLED to hand us its DMA buffer for direct writes — its current API doesn't, and either (a) we bypass FastLED for that backend (effectively Scenario B for the most MoonLight-defining backend), (b) we upstream a PR (slow, library-author dependent), or (c) we accept a per-frame copy from FastLED's buffer into our transposed buffer (hot-path cost). +- **No control over IDF version pinning.** FastLED master targets whatever IDF FastLED master targets. If our MoonLight IDF version pin disagrees with FastLED's, we either bump them both in lockstep or carry a divergent fork. In Scenario B we own the IDF compatibility matrix. - **Carrying FastLED is binary size we don't fully use.** FastLED master ships drivers for ~20 protocols, ~6 ESP32 variants, Teensy, ARM M0/M0+, and AVR; gamma tables, palette helpers, FX layer, blur kernels. We use the bus driver and the bus driver only. Dead code elimination at link time helps, but FastLED's runtime driver-registry is the opposite of dead-code-eliminable (the whole point is that drivers are reachable from a string lookup). Realistic estimate: 80-150 KB of binary we'd carry without using. -- **Profile-guided tuning is harder.** projectMM's hot path is unusual — most LED firmwares don't have a Layer/MappingLUT composition layer above the driver. Tuning the inner loop for projectMM's specific access patterns is straightforward if we own the inner loop; in Scenario A it's a sequence of conversations with the library maintainer. +- **Profile-guided tuning is harder.** MoonLight's hot path is unusual — most LED firmwares don't have a Layer/MappingLUT composition layer above the driver. Tuning the inner loop for MoonLight's specific access patterns is straightforward if we own the inner loop; in Scenario A it's a sequence of conversations with the library maintainer. What we **lose by building everything ourselves**: speed-to-first-working-driver. Scenario A puts an ESP32 + Teensy hybrid in your hands in ~3 weeks. Scenario B is multi-month before parity, and during those months we own every IDF API surprise. That cost is real. -What we **gain by building everything ourselves**: every byte of the hot path is ours, the multiplex axis is symmetric across backends without library-author cooperation, the binary stays focused on projectMM's actual needs, and the architecture stops being a negotiation between projectMM's design and FastLED's. **The architecture becomes a contract we own**, not a translation layer between two contracts. +What we **gain by building everything ourselves**: every byte of the hot path is ours, the multiplex axis is symmetric across backends without library-author cooperation, the binary stays focused on MoonLight's actual needs, and the architecture stops being a negotiation between MoonLight's design and FastLED's. **The architecture becomes a contract we own**, not a translation layer between two contracts. ### Recommendation — walk Scenario B Reading the whole document honestly: **build everything ourselves.** Reasons, in priority order: -1. **The multiplex axis is the projectMM-defining value-add for the >30K-LED case, and it doesn't compose cleanly onto FastLED.** Scenario A's "ride FastLED + vendor hpwit for the multiplex" framing was the first thing I wrote, and the feasibility check made it visibly worse. The cleanest path for the virtual driver is Scenario B; that's our heaviest single dependency on Scenario A, and it's the spot where Scenario A is structurally weakest. +1. **The multiplex axis is the MoonLight-defining value-add for the >30K-LED case, and it doesn't compose cleanly onto FastLED.** Scenario A's "ride FastLED + vendor hpwit for the multiplex" framing was the first thing I wrote, and the feasibility check made it visibly worse. The cleanest path for the virtual driver is Scenario B; that's our heaviest single dependency on Scenario A, and it's the spot where Scenario A is structurally weakest. 2. **Runtime driver switching as a first-class requirement aligns naturally with our own contract.** FastLED's API is moving toward this in May 2026 master, but it's young and we'd be downstream of their choices. Owning the contract means owning the semantics of "switch backend without dropping a frame" — the user-facing requirement. -3. **The identity-mapping fast path needs the driver to read directly from `layer_->buffer()`.** That's a projectMM-specific optimisation that the rest of the architecture is built around (Drivers.h:90 already implements it for ArtNetSend / Preview). FastLED has no incentive to support it; we'd lose the optimisation or carry a coupling layer. +3. **The identity-mapping fast path needs the driver to read directly from `layer_->buffer()`.** That's a MoonLight-specific optimisation that the rest of the architecture is built around (Drivers.h:90 already implements it for ArtNetSend / Preview). FastLED has no incentive to support it; we'd lose the optimisation or carry a coupling layer. 4. **ESP-IDF gives us the peripheral plumbing for free.** As the previous section showed, `parlio_tx_unit`, `esp_lcd_panel_io_i80`, `rmt_tx_channel` are all production-ready. The LED-specific layer on top is small (300-700 lines per backend); we're not writing a peripheral driver from scratch. 5. **License + binary-size tax shrink the surplus from Scenario A.** EUPL-1.2 / MIT / GPL-3.0 mixing is solvable but adds review burden; 80-150 KB of unused FastLED code is real on a 4 MB partition table. @@ -577,7 +577,7 @@ Notes on the shape: Two real numbers worth noting: - **DMA transmission is asynchronous to `push()`.** Wire-level WS2812 transmission at 800 kHz takes ~30 µs per LED × 16K = ~500 ms in serial; on parallel hardware (16 pins) it's ~30 ms. Both of these are larger than the frame budget — which is why double-buffering + DMA chains exist. The driver's `push()` returns immediately after handing the buffer to DMA; the next frame's `push()` blocks (or yields) until DMA is free. -- **`push()` budget shrinks fast at higher counts.** At 30K LEDs the effect compute alone is 20+ ms (linear in light count for most effects); the frame budget either grows (drop to 30 FPS) or moves to multi-core (effects on core 1, network on core 0). projectMM does not pin tasks today — the render runs on whichever core the scheduler task lands on. +- **`push()` budget shrinks fast at higher counts.** At 30K LEDs the effect compute alone is 20+ ms (linear in light count for most effects); the frame budget either grows (drop to 30 FPS) or moves to multi-core (effects on core 1, network on core 0). MoonLight does not pin tasks today — the render runs on whichever core the scheduler task lands on. ## Hot-path do-and-don't checklist diff --git a/docs/work/future/leddriver-analysis-top-down.md b/docs/work/future/leddriver-analysis-top-down.md index 36032fa9..e4805e9d 100644 --- a/docs/work/future/leddriver-analysis-top-down.md +++ b/docs/work/future/leddriver-analysis-top-down.md @@ -122,7 +122,7 @@ State of the art on Pi 5 in 2026: | [niklasr22/rpi5-ws2812](https://github.com/niklasr22/rpi5-ws2812) | Bit-bang WS2812 timing through SPI MOSI at 2.4 MHz. Single channel, no kernel module needed, well-trodden hack. | | Bridge to an MCU | Use the Pi 5 as the brains (HTTP, preview, scenes) and hand the strand timing to an ESP32 / Teensy / Pico over USB or SPI. **Cleanest path today.** | -**Recommendation**: in projectMM, treat the Pi 5 as a desktop-class platform that delegates strand timing to an attached MCU. If single-strand-from-Pi-5 ever becomes a requirement, wrap Adafruit's PIO library behind a `WS2812Rp1Driver` and accept the single-strand limit. +**Recommendation**: in MoonLight, treat the Pi 5 as a desktop-class platform that delegates strand timing to an attached MCU. If single-strand-from-Pi-5 ever becomes a requirement, wrap Adafruit's PIO library behind a `WS2812Rp1Driver` and accept the single-strand limit. ## 3. WiFi vs the driver — the failure that defines ESP32 driver design @@ -147,7 +147,7 @@ Contributing factors, ranked: Diagnostic signature for WiFi-induced corruption (vs power or signal-integrity faults): first frames after boot look perfect; symptoms appear only after WiFi associates; pixels are shifted/stuck *from a position outward* (one bit slipped, everything downstream inherits it); color flashes correlate with traffic bursts. Compare against power-fault signature: brownout, whole-strip dim, first-pixel corruption. -For projectMM, this means: **the LED driver task lives on core 1** (the quiet core, away from the WiFi stack and its interrupts) and **the effects / network path lives on core 0** (it can tolerate latency spikes — a late effect frame is invisible, a late driver bit is a corrupted pixel). This is the inverse of the WLED render-on-core-1 pattern, applied to a different observation about which task has the harder deadline; see § 7.2 for the full rationale and the projectMM-specific per-module core-affinity story. +For MoonLight, this means: **the LED driver task lives on core 1** (the quiet core, away from the WiFi stack and its interrupts) and **the effects / network path lives on core 0** (it can tolerate latency spikes — a late effect frame is invisible, a late driver bit is a corrupted pixel). This is the inverse of the WLED render-on-core-1 pattern, applied to a different observation about which task has the harder deadline; see § 7.2 for the full rationale and the MoonLight-specific per-module core-affinity story. ## 4. Generic driver architecture @@ -245,7 +245,7 @@ Putting 74HC595 (or similar) on the data lines is **not** a new driver layer — In the architecture above, an expander variant is a configuration of an existing driver (e.g. `TeensyFlexIoDriver` with `expanderChains = 32`), not a new class. The transpose step gets a wider stride; everything else is unchanged. -For projectMM this is **third-priority** behind WS2812 and SK6812: design the interface so it could host it (the K-lane abstraction is already general enough), but don't ship it day one. +For MoonLight this is **third-priority** behind WS2812 and SK6812: design the interface so it could host it (the K-lane abstraction is already general enough), but don't ship it day one. ### 4.6 Output correction (already shipped) @@ -410,11 +410,11 @@ Default direction is **inverted from §3 of this doc**: the LED driver task runs - If the **driver task is interrupted**, the LED signal corrupts → visible flicker. The driver must be on the quiet core. - If the **effects task is interrupted**, the next frame is a few ms late → invisible at 30+ FPS. Effects can tolerate core 0. -That is the inverse of what §3 of this doc proposes (which followed the WLED pattern of network-on-core-0, render-on-core-1 — same conclusion, applied to the *render* loop rather than the *driver* loop). In projectMM the driver and the render-of-effects are separate tasks, so the choice is about which one sits on the quiet core. The driver wins because its deadline is per-bit, not per-frame. +That is the inverse of what §3 of this doc proposes (which followed the WLED pattern of network-on-core-0, render-on-core-1 — same conclusion, applied to the *render* loop rather than the *driver* loop). In MoonLight the driver and the render-of-effects are separate tasks, so the choice is about which one sits on the quiet core. The driver wins because its deadline is per-bit, not per-frame. -This is also how MoonLight (v2 → projectMM's predecessor) is wired today: it works, the precedent matters. +This is also how MoonLight (v2 → MoonLight's predecessor) is wired today: it works, the precedent matters. -**Generalisation**: a single fixed default is ESP-classic-and-S3 sensible. P4 has no built-in WiFi so the choice is moot there. **Because projectMM already supports per-module core affinity** (the wider plan is to expose this as a control), the driver and effects task affinities become MoonModule-level controls with the defaults above. Other devices (Teensy, Pi 5) ignore the control. +**Generalisation**: a single fixed default is ESP-classic-and-S3 sensible. P4 has no built-in WiFi so the choice is moot there. **Because MoonLight already supports per-module core affinity** (the wider plan is to expose this as a control), the driver and effects task affinities become MoonModule-level controls with the defaults above. Other devices (Teensy, Pi 5) ignore the control. Practical wiring rule for hello-world: in `main.cpp`, pin the LED driver task to core 1 and the effects/render task to core 0 on classic ESP32 + S3; leave it unset on P4 / Teensy / Pi 5. Make the values overridable via the existing core-affinity control. @@ -463,38 +463,38 @@ This is the pattern the bottom-up doc calls "Backend × Multiplex" — confirmed ### 7.6 Pi 5 — accept "Pi 5 + attached MCU", defer firmware-shape decision -Confirmed: projectMM does **not** commit to driving WS2812 directly from the Pi 5's own GPIO pins. Bridge to an MCU. +Confirmed: MoonLight does **not** commit to driving WS2812 directly from the Pi 5's own GPIO pins. Bridge to an MCU. The shape of the firmware that runs on the bridged MCU is **deferred** until Pi 5 work actually starts. Two paths are on the table, both viable, both with trade-offs worth weighing at decision time rather than now: -#### Path A — Stripped projectMM on the bridge MCU +#### Path A — Stripped MoonLight on the bridge MCU -The bridge MCU runs a thin projectMM build: no UI, no effects engine, no full network listener — just the `Drivers` + `LedDriver` pipeline plus a USB-serial input parser that pushes incoming RGB byte frames into the driver. Estimate: 50–150 KB build target. +The bridge MCU runs a thin MoonLight build: no UI, no effects engine, no full network listener — just the `Drivers` + `LedDriver` pipeline plus a USB-serial input parser that pushes incoming RGB byte frames into the driver. Estimate: 50–150 KB build target. Pros: -- Code reuse with the main projectMM tree. Driver fixes flow into the bridge automatically. -- A bridge MCU is a regular projectMM build with three modules disabled — no second codebase. +- Code reuse with the main MoonLight tree. Driver fixes flow into the bridge automatically. +- A bridge MCU is a regular MoonLight build with three modules disabled — no second codebase. - The bridge can locally expose status/heartbeat/diagnostics via the existing MoonModule control surface. Cons: - Larger image. Slower to boot. More to flash. -- Couples the bridge to projectMM's release cadence and IDF version pin — bridge upgrades become projectMM upgrades. +- Couples the bridge to MoonLight's release cadence and IDF version pin — bridge upgrades become MoonLight upgrades. #### Path B — Dedicated minimal firmware -Standalone firmware, no projectMM code reuse: `read N RGB bytes from USB serial → push to a vendored LedDriver implementation → repeat`. Estimate: under 30 KB. +Standalone firmware, no MoonLight code reuse: `read N RGB bytes from USB serial → push to a vendored LedDriver implementation → repeat`. Estimate: under 30 KB. Pros: - Tiny, fast to boot, easy to flash from any laptop. -- Independent release cadence; the bridge stays stable while projectMM iterates. +- Independent release cadence; the bridge stays stable while MoonLight iterates. - No build-system overhead — single-file possibility. Cons: -- Driver code diverges from projectMM's tree over time unless we vendor + sync deliberately. +- Driver code diverges from MoonLight's tree over time unless we vendor + sync deliberately. - No diagnostic surface beyond what the protocol carries. - Two codebases to maintain in the longer run. -**Decision deferred** until the Pi 5 bridge becomes a real task. When it does, the deciding factor is likely: how often will the bridge's driver be touched? If rarely, Path B. If it tracks projectMM's main-line driver work, Path A. +**Decision deferred** until the Pi 5 bridge becomes a real task. When it does, the deciding factor is likely: how often will the bridge's driver be touched? If rarely, Path B. If it tracks MoonLight's main-line driver work, Path A. For *this* analysis it's enough to note: the `LedDriver` interface designed in §4 is the same in both paths — both reuse it. The choice between A and B is a *build target* decision, not an *interface* decision. diff --git a/docs/work/future/livescripts-analysis-bottom-up.md b/docs/work/future/livescripts-analysis-bottom-up.md index de63420f..942d6205 100644 --- a/docs/work/future/livescripts-analysis-bottom-up.md +++ b/docs/work/future/livescripts-analysis-bottom-up.md @@ -1,16 +1,16 @@ # MoonLive — live-script engine landscape analysis -> **Forward-looking research document — exception to CLAUDE.md present-tense rule.** This is a Stage-1 bottom-up survey of *live scripting* for projectMM: running user-authored scripts (LED effects, layouts, modifiers, drivers, sensor logic) on a running device without a recompile-and-flash cycle. It deep-reads one reference implementation — the [ewowi/ESPLiveScript `fix-warnings` fork](https://github.com/ewowi/ESPLiveScript/tree/fix-warnings) of [hpwit/ESPLiveScript](https://github.com/hpwit/ESPLiveScript) — at HEAD on **2026-06-25**, surveys the comparable field (WLED ARTI-FX, embedded VMs, WASM), and extracts the architectural primitives a clean projectMM redesign must decide. Companion to the monthly digest [friend-repos/hpwit-ESPLiveScript.md](../../friend-repos/hpwit-ESPLiveScript.md) (credits + activity log). The **top-down** redesign document ([livescripts-analysis-top-down.md](livescripts-analysis-top-down.md)) expands the decisions recorded here into the build spec. Source citations use `file:line` against the cloned fork; inferred claims are marked *(inferred)*. Modelled on [leddriver-analysis-bottom-up.md](../../work/future/leddriver-analysis-bottom-up.md). +> **Forward-looking research document — exception to CLAUDE.md present-tense rule.** This is a Stage-1 bottom-up survey of *live scripting* for MoonLight: running user-authored scripts (LED effects, layouts, modifiers, drivers, sensor logic) on a running device without a recompile-and-flash cycle. It deep-reads one reference implementation — the [ewowi/ESPLiveScript `fix-warnings` fork](https://github.com/ewowi/ESPLiveScript/tree/fix-warnings) of [hpwit/ESPLiveScript](https://github.com/hpwit/ESPLiveScript) — at HEAD on **2026-06-25**, surveys the comparable field (WLED ARTI-FX, embedded VMs, WASM), and extracts the architectural primitives a clean MoonLight redesign must decide. Companion to the monthly digest [friend-repos/hpwit-ESPLiveScript.md](../../friend-repos/hpwit-ESPLiveScript.md) (credits + activity log). The **top-down** redesign document ([livescripts-analysis-top-down.md](livescripts-analysis-top-down.md)) expands the decisions recorded here into the build spec. Source citations use `file:line` against the cloned fork; inferred claims are marked *(inferred)*. Modelled on [leddriver-analysis-bottom-up.md](../../work/future/leddriver-analysis-bottom-up.md). ## TL;DR -- **What live scripting is, and why projectMM wants it.** The same itch that produced WLED ARTI-FX and ESPLiveScript: author an effect (or layout, modifier, driver, sensor rule) *as text*, push it to a running device, see it run **on the next tick** — no toolchain, no flash, no reboot. It turns projectMM from "the effects we compiled in" into "any effect a user can write," and it's the natural home for a web-based pattern editor. -- **The design space has three corners, set by *how* a script becomes execution.** (1) **AST-walking interpreter** — parse to a tree, walk it every frame (WLED **ARTI-FX**: everything stored as `double`, flexible, slow). (2) **Bytecode VM** — compile to a compact opcode stream, run a dispatch loop (**PixelBlaze**, most embedded Lua/JS). (3) **Native JIT** — emit real machine code, call it as a function (**ESPLiveScript**). Speed climbs (1)→(3); portability and simplicity fall (1)→(3). projectMM's "blazingly fast like ESPLiveScript" requirement points at (3), but (3) is exactly where portability dies — see below. +- **What live scripting is, and why MoonLight wants it.** The same itch that produced WLED ARTI-FX and ESPLiveScript: author an effect (or layout, modifier, driver, sensor rule) *as text*, push it to a running device, see it run **on the next tick** — no toolchain, no flash, no reboot. It turns MoonLight from "the effects we compiled in" into "any effect a user can write," and it's the natural home for a web-based pattern editor. +- **The design space has three corners, set by *how* a script becomes execution.** (1) **AST-walking interpreter** — parse to a tree, walk it every frame (WLED **ARTI-FX**: everything stored as `double`, flexible, slow). (2) **Bytecode VM** — compile to a compact opcode stream, run a dispatch loop (**PixelBlaze**, most embedded Lua/JS). (3) **Native JIT** — emit real machine code, call it as a function (**ESPLiveScript**). Speed climbs (1)→(3); portability and simplicity fall (1)→(3). MoonLight's "blazingly fast like ESPLiveScript" requirement points at (3), but (3) is exactly where portability dies — see below. - **ESPLiveScript is a from-scratch C-like compiler that JIT-emits native Xtensa machine code** (confirmed, not bytecode): `tokenizer.h` → `asm_parser.h` (AST of `NodeToken`) → visitor methods emit Xtensa assembly *strings* → `asm_parser_LMbin.h` encodes them to 32-bit opcodes → the binary is copied to executable RAM (`heap_caps_malloc(..., MALLOC_CAP_EXEC)`) and **called as a function pointer** via inline `callx8` (`execute_asm.h:386-399`). That direct-call, no-dispatch-loop design is the entire "85 fps C++ vs 10 fps Gravity vs 3 fps Lua" speed story from its README. -- **The portability finding that reshapes everything: ESPLiveScript is Xtensa-only.** The agent deep-read confirmed **no RISC-V backend** — all codegen is Tensilica Xtensa LX (`asm_parser_LMbin.h`, the inline-asm executor). This matters enormously for projectMM's target order: classic ESP32 + S3 are Xtensa (✅ ESPLiveScript runs), but **ESP32-P4 is RISC-V** (❌), as are Teensy 4.x (ARM Cortex-M7) and desktop (x86-64 / ARM64). So ESPLiveScript as-is covers exactly the *first two* targets on projectMM's list and **none** of the rest. A native-codegen engine needs **one backend per ISA** (Xtensa, RISC-V, ARM Thumb-2, x86-64, ARM64) — that's the real cost of "blazingly fast everywhere." +- **The portability finding that reshapes everything: ESPLiveScript is Xtensa-only.** The agent deep-read confirmed **no RISC-V backend** — all codegen is Tensilica Xtensa LX (`asm_parser_LMbin.h`, the inline-asm executor). This matters enormously for MoonLight's target order: classic ESP32 + S3 are Xtensa (✅ ESPLiveScript runs), but **ESP32-P4 is RISC-V** (❌), as are Teensy 4.x (ARM Cortex-M7) and desktop (x86-64 / ARM64). So ESPLiveScript as-is covers exactly the *first two* targets on MoonLight's list and **none** of the rest. A native-codegen engine needs **one backend per ISA** (Xtensa, RISC-V, ARM Thumb-2, x86-64, ARM64) — that's the real cost of "blazingly fast everywhere." - **The front-end is portable; the back-end is not.** Tokenizer + parser + AST (`NodeToken`) are CPU-agnostic; only the *visitor → opcode* tier and the *load-and-execute* tier are ISA-bound. But today they're **deeply interleaved** — visitor methods emit Xtensa strings inline, there is **no intermediate representation (IR)** between AST and machine code. A clean redesign's load-bearing decision is whether to introduce that IR seam so one front-end feeds many back-ends (the LLVM shape, scaled down). -- **The "compatible with MoonModule" requirement is the projectMM-specific value-add.** ESPLiveScript binds to the host via `addExternalFunction(name, ret, sig, fnptr)` / `addExternalVariable(name, type, _, ptr)` (`asm_external.h`) — a flat C-pointer registry. projectMM needs scripts to read/write **controls**, consume the **producer/consumer data structures** (a `Buffer`, an `AudioFrame`), and slot into the **module tree** as a scripted effect/layout/modifier/driver/peripheral. That binding layer — script ⇄ MoonModule — is ours to design; no surveyed engine has it. -- **Memory + sync are already partly modelled in ESPLiveScript** and align with projectMM's constraints: compiled code lands in IRAM/PSRAM by target (`execute.h:10-15` gates PSRAM stack on S3/P4), a **save/load compiled-binary path** exists (`savebinary`/`executebinary` examples → compile once, ship the binary, skip re-compile on boot), and a `sync()` primitive coordinates concurrent script tasks. These are the right *ideas*; the redesign carries them forward against our `platform::` seam and `Scheduler`. +- **The "compatible with MoonModule" requirement is the MoonLight-specific value-add.** ESPLiveScript binds to the host via `addExternalFunction(name, ret, sig, fnptr)` / `addExternalVariable(name, type, _, ptr)` (`asm_external.h`) — a flat C-pointer registry. MoonLight needs scripts to read/write **controls**, consume the **producer/consumer data structures** (a `Buffer`, an `AudioFrame`), and slot into the **module tree** as a scripted effect/layout/modifier/driver/peripheral. That binding layer — script ⇄ MoonModule — is ours to design; no surveyed engine has it. +- **Memory + sync are already partly modelled in ESPLiveScript** and align with MoonLight's constraints: compiled code lands in IRAM/PSRAM by target (`execute.h:10-15` gates PSRAM stack on S3/P4), a **save/load compiled-binary path** exists (`savebinary`/`executebinary` examples → compile once, ship the binary, skip re-compile on boot), and a `sync()` primitive coordinates concurrent script tasks. These are the right *ideas*; the redesign carries them forward against our `platform::` seam and `Scheduler`. - **⚠️ Superseded upstream (noted 2026-08-06): hpwit has rewritten it as [ESPLiveScript2](https://github.com/hpwit/new-parser).** A from-scratch C++ reimplementation whose stated goal is precisely the gap this analysis identified below — a compiler you can *verify*: the whole toolchain builds and runs as a host program, and its tests execute the actual compiled Xtensa bytes under QEMU against v1's own example corpus. The rewrite landed in the first days of August 2026 (the repo was dormant May 2025 → August 2026), so this document's reading of v1 stands as written but is no longer a reading of hpwit's *current* work. **Before Stage 2 acts on any v1 conclusion, re-read v2** — the portability finding in particular (is codegen still Xtensa-only, or did the rewrite introduce the IR seam we concluded was missing?). Digest: [friend-repos/hpwit-new-parser.md](../../friend-repos/hpwit-new-parser.md). - **Code-quality reality (for the redesign).** Header-only, ~18K lines across 11 headers, **pervasive global state** (`string signature; Token __t;` and dozens of file-scope compiler counters), no IR, no unit tests, a 4,100-line `Parser` and a 5,824-line `NodeToken`. It works and it's fast, but it is **not** a base to extend in place — it's the reference to learn from and rewrite against our architecture (exactly the *Industry standards, our own code* method we used for LED drivers). - **Recommendation: build our own native engine, Xtensa-first, behind an IR seam — start small, start beautiful, no dead-ends.** Take the ESPLiveScript *approach* (native machine-code execution, near-100% speed — the standout, never-done-before-in-this-space when bound to a module system) and add the one thing our multi-target goal needs that a single-ISA engine doesn't: put an **IR seam** between a platform-independent front-end (tokenizer→parser→AST) and the code generator. **Ship one backend first — Xtensa (classic ESP32 + S3)** — exactly where ESPLiveScript already proves native speed; that's the small, beautiful, blazingly-fast first deliverable. The IR seam is the **no-dead-end guarantee**: adding RISC-V (P4), ARM (Teensy), or x86/ARM64 (desktop) later is "write another backend behind the same IR," never "go back to the drawing board." ESPLiveScript's real dead-end isn't *Xtensa-first* — it's *Xtensa-welded-in, no IR*; we start at the same fast place but with the seam it lacks. **WASM/WAMR is the named fallback, per target**: a target without a native backend yet can run the portable path through the same IR, so we're never blocked — but the *flagship* experience is native. (Detail + why-this-over-WASM-wholesale in § Recommendation.) @@ -20,7 +20,7 @@ ## Why this document exists -projectMM compiles its effects, layouts, modifiers, and drivers into the firmware. Adding one means writing C++, building, and flashing. **Live scripting removes that loop**: a user writes an effect as text in a browser, the device compiles/loads it, and it runs as a first-class module on the next tick — the same leap WLED took with ARTI-FX. +MoonLight compiles its effects, layouts, modifiers, and drivers into the firmware. Adding one means writing C++, building, and flashing. **Live scripting removes that loop**: a user writes an effect as text in a browser, the device compiles/loads it, and it runs as a first-class module on the next tick — the same leap WLED took with ARTI-FX. The product owner's requirements (verbatim intent): @@ -64,7 +64,7 @@ The agent's deep-read is unambiguous: **Xtensa-only, ESP-IDF-coupled.** - **Execution is ESP-IDF-specific**: `MALLOC_CAP_EXEC` IRAM allocation, inline `callx8`, `rsr a14,234` cycle-counter reads, `xTaskCreatePinnedToCoreWithCaps` (`execute.h:590`). - **Front-end is portable, back-end is not**: tokenizer + parser + AST are CPU-agnostic; tiers 2-4 (visit→opcode, encode, load-execute) are ISA/platform-bound and **interleaved** with no seam between them. -So on projectMM's target list, ESPLiveScript as-is runs on **classic ESP32 + S3** and stops there. P4 (RISC-V), Teensy (ARM), and desktop (x86-64/ARM64) each need a *new code generator* — 2-3K lines per ISA *(inferred, agent estimate)* — or a different execution strategy entirely. +So on MoonLight's target list, ESPLiveScript as-is runs on **classic ESP32 + S3** and stops there. P4 (RISC-V), Teensy (ARM), and desktop (x86-64/ARM64) each need a *new code generator* — 2-3K lines per ISA *(inferred, agent estimate)* — or a different execution strategy entirely. ### Host integration — the binding model @@ -77,7 +77,7 @@ A host C program drives it through `class Parser` (`ESPLiveScript.h:79`): - **JSON path** (`__JSON__OPTION__`): scripts exchange JSON with the host (`execute_asm.h:400-471`) — the `enjoy json` feature from the digest; the bridge a web editor would lean on. - **Precompiled binaries**: `parseScriptBinary()` → `saveBinary()/loadBinary()` → `createExecutableFromBinary()` (`execute_asm.h:276-384`). Compile once (on a desktop or web service), persist the `ESPLiveScript1.0.1`-format binary, load it on the device — but **external pointers are not serialized**, they re-bind at load (README). This is the seed of a smart compile-once memory strategy. -This binding is a **flat C-pointer registry** — exactly what projectMM must *replace* with a MoonModule-aware layer (controls, producer/consumer structs, the module tree). +This binding is a **flat C-pointer registry** — exactly what MoonLight must *replace* with a MoonModule-aware layer (controls, producer/consumer structs, the module tree). ### Memory model @@ -100,11 +100,11 @@ C-like, LED-oriented. From `sc_examples/*.sc` + README: ### What the `fix-warnings` fork changed -Despite the branch name, the fork's substantive change is **one commit (`4871509`, 2026-04-02): a null-pointer crash fix**, not a `-Wall` cleanup. `findMaxArgumentSize()` in `NodeToken.h` dereferenced `getChildAtPos(1)`/`(2)` unconditionally; a **nested external-function argument** (`setRGB(random16(NUM_LEDS), CRGB(0,0,255))`) produces a node without those children → `LoadProhibited` crash on device. The fix adds null guards (return 0 — a scalar needs no pre-call stack spill). *Relevance to projectMM*: this is precisely the class of bug a from-scratch hand-written parser breeds (no test harness caught it), and a data point for "rewrite with tests" over "extend in place." +Despite the branch name, the fork's substantive change is **one commit (`4871509`, 2026-04-02): a null-pointer crash fix**, not a `-Wall` cleanup. `findMaxArgumentSize()` in `NodeToken.h` dereferenced `getChildAtPos(1)`/`(2)` unconditionally; a **nested external-function argument** (`setRGB(random16(NUM_LEDS), CRGB(0,0,255))`) produces a node without those children → `LoadProhibited` crash on device. The fix adds null guards (return 0 — a scalar needs no pre-call stack spill). *Relevance to MoonLight*: this is precisely the class of bug a from-scratch hand-written parser breeds (no test harness caught it), and a data point for "rewrite with tests" over "extend in place." ### Structural observations (what a multi-target rewrite must account for — not a verdict on the original) -These are the differences between ESPLiveScript's design (one author, one ISA, maximum speed) and what projectMM's *different* goals (multi-target, module-bound, tested) need. They are reasons to write our own against our architecture, not faults — ESPLiveScript meets its own goals well. +These are the differences between ESPLiveScript's design (one author, one ISA, maximum speed) and what MoonLight's *different* goals (multi-target, module-bound, tested) need. They are reasons to write our own against our architecture, not faults — ESPLiveScript meets its own goals well. - **Header-only**, ~18K lines, 11 files; the two biggest (`NodeToken.h` 5,824, `ESPLiveScript.h` 4,100) carry several jobs each (tree + metadata + 47 visitors + asm emission). Fine for a single-include library; we'd split for testability. - **File-scope state**: `string signature; Token __t;` (`ESPLiveScript.h:29-30`), plus global register-allocation stacks, output buffers, compiler counters. A consequence: one compilation at a time. Acceptable on a device that compiles one script; we'd encapsulate it. @@ -115,11 +115,11 @@ The lesson is the LED-driver lesson: **study it hard, credit it (see § Prior ar ## The comparable field (what else to learn from) -projectMM's "industry standards" rule says: name the prior art, take the textbook approach. The live-scripting field has three established design points; ESPLiveScript is the extreme of one. +MoonLight's "industry standards" rule says: name the prior art, take the textbook approach. The live-scripting field has three established design points; ESPLiveScript is the extreme of one. ### WLED ARTI-FX — the AST-walking interpreter (our sibling project) -[ARTI-FX](https://mm.kno.wled.ge/moonmodules/arti-fx/) (MoonModules, by ewowi — the author of this analysis — built on the **ARTI** runtime, a PEG-grammar-driven interpreter) parses a script and **walks the AST every frame**. Every value is stored as a **`double`**, converted to int when needed; scripts define `renderFrame` (per-frame) + `renderLed` (per-LED) callbacks and call `setPixelColor`/`setRange`/`fill`. Host binding is `arti_external_function` / `arti_set/get_external_variable` (the same flat-registry shape as ESPLiveScript). **What it contributes:** it proved live scripting works end-to-end in this ecosystem, and its design is maximally flexible and portable — pure C++ tree-walking, runs anywhere unchanged. The `double`-everything per-LED walk trades per-frame speed for that flexibility, which is the trade projectMM's 16K hot path can't take (and exactly the gap ESPLiveScript's native path closes). So the two are complementary baselines: ARTI-FX is the **reach** baseline (runs everywhere, the product shape proven), ESPLiveScript is the **speed** baseline (native, Xtensa) — and projectMM wants both, which is why neither alone is the answer. +[ARTI-FX](https://mm.kno.wled.ge/moonmodules/arti-fx/) (MoonModules, by ewowi — the author of this analysis — built on the **ARTI** runtime, a PEG-grammar-driven interpreter) parses a script and **walks the AST every frame**. Every value is stored as a **`double`**, converted to int when needed; scripts define `renderFrame` (per-frame) + `renderLed` (per-LED) callbacks and call `setPixelColor`/`setRange`/`fill`. Host binding is `arti_external_function` / `arti_set/get_external_variable` (the same flat-registry shape as ESPLiveScript). **What it contributes:** it proved live scripting works end-to-end in this ecosystem, and its design is maximally flexible and portable — pure C++ tree-walking, runs anywhere unchanged. The `double`-everything per-LED walk trades per-frame speed for that flexibility, which is the trade MoonLight's 16K hot path can't take (and exactly the gap ESPLiveScript's native path closes). So the two are complementary baselines: ARTI-FX is the **reach** baseline (runs everywhere, the product shape proven), ESPLiveScript is the **speed** baseline (native, Xtensa) — and MoonLight wants both, which is why neither alone is the answer. ### WASM on ESP32 — the strongest off-the-shelf portable-runtime candidate (answering "is WASM what we want?") @@ -128,7 +128,7 @@ WebAssembly is a portable **bytecode standard** with mature small runtimes that - **wasm3** — an ultra-light **interpreter** in C (~64 KB code, ~10 KB RAM), runs Arduino-class MCUs upward. Pure interpretation, so *slower* (the per-opcode dispatch cost, same class as a bytecode VM) — fine for control logic, questionable for a 16K-pixel inner loop. - **WAMR (WebAssembly Micro Runtime)** — supports interpreter **and AOT/JIT**; in **AOT mode WAMR reaches ~50% of native speed**, "quite acceptable" for embedded use, at a larger footprint. Rule of thumb from the field: **RAM < 256 KB → wasm3, > 256 KB → WAMR**; the classic ESP32's 320 KB+ puts it in WAMR's range. ([arXiv survey](https://arxiv.org/html/2512.00035v1), [WAMR-ESP32](https://registry.platformio.org/libraries/mlaass/WAMR-ESP32)) -**Why this is genuinely relevant to projectMM**, and arguably *the* answer to "runs everywhere": +**Why this is genuinely relevant to MoonLight**, and arguably *the* answer to "runs everywhere": - **One artifact, every target.** A script compiled to `.wasm` runs on classic/S3/P4/Teensy/desktop through the same runtime — no per-ISA backend, which is exactly ESPLiveScript's missing piece. WASM **is** the portable IR + VM, off the shelf. - **WAMR-AOT is the "blazingly fast" bridge.** AOT-compiling the `.wasm` to native on the device (or on a host) gets ~50% of native — between a naive bytecode VM and ESPLiveScript's near-100%. That's the same "portable baseline + native acceleration" shape the bottom-up proposes, but **already built and multi-ISA** (WAMR's AOT backends cover Xtensa, RISC-V, ARM, x86). @@ -139,7 +139,7 @@ WebAssembly is a portable **bytecode standard** with mature small runtimes that ### FastLED's WASM — adjacent but **not** what we want -FastLED's `master` WASM support (`src/platforms/wasm/`) compiles **FastLED itself to WebAssembly via Emscripten to run in a *browser*** — a **simulation/preview** of effects, not a runtime that runs user scripts *on the ESP32*. It runs FastLED in a Web Worker (`PROXY_TO_PTHREAD`), bridges C++↔JS via `EMSCRIPTEN_KEEPALIVE` exports (`js_bindings.cpp`), and exports frame/strip/UI data as JSON for the page to draw. FastLED's `FxEngine` is likewise a *compiled* effect manager (switch/transition between C++ effects), not an on-device scripting language. So FastLED gives projectMM **two adjacent ideas, neither the live-script engine**: (1) "compile your effect library to WASM to **preview it in the browser**" — a preview technique that sits next to projectMM's own 3D WebGL preview, not the scripting engine; (2) precedent that the WASM toolchain is production-ready. It does **not** answer the on-device live-scripting need. ([FastLED wasm platform](https://github.com/FastLED/FastLED/tree/master/src/platforms/wasm)) +FastLED's `master` WASM support (`src/platforms/wasm/`) compiles **FastLED itself to WebAssembly via Emscripten to run in a *browser*** — a **simulation/preview** of effects, not a runtime that runs user scripts *on the ESP32*. It runs FastLED in a Web Worker (`PROXY_TO_PTHREAD`), bridges C++↔JS via `EMSCRIPTEN_KEEPALIVE` exports (`js_bindings.cpp`), and exports frame/strip/UI data as JSON for the page to draw. FastLED's `FxEngine` is likewise a *compiled* effect manager (switch/transition between C++ effects), not an on-device scripting language. So FastLED gives MoonLight **two adjacent ideas, neither the live-script engine**: (1) "compile your effect library to WASM to **preview it in the browser**" — a preview technique that sits next to MoonLight's own 3D WebGL preview, not the scripting engine; (2) precedent that the WASM toolchain is production-ready. It does **not** answer the on-device live-scripting need. ([FastLED wasm platform](https://github.com/FastLED/FastLED/tree/master/src/platforms/wasm)) ### Compiled languages (Rust / TinyGo) and interpreted Python — why neither is the answer (answering the Rust/Python question) @@ -163,7 +163,7 @@ FastLED's `master` WASM support (`src/platforms/wasm/`) compiles **FastLED itsel | **WASM AOT/JIT** | **WAMR** | **~50% native** | **Highest** (standard, multi-ISA) | 200 KB+ | **Built-in** | High (any→wasm) | | Native JIT (custom) | **ESPLiveScript** | **Highest** (native) | **Lowest** (one backend/ISA) | Medium | None (can crash) | Medium | -projectMM wants ESPLiveScript's **speed** *and* ARTI-FX's **reach** — no single *custom* corner gives both, which is why one redesign path is a **layered custom** engine (portable VM baseline + optional native back-end behind a shared IR). But **WASM+WAMR-AOT collapses that table into one row**: portable to every target *and* ~50% native *and* sandboxed, off the shelf. The two real finalists for the top-down are therefore **(A) build our own VM+IR (+ optional native backend)** vs **(B) adopt WASM/WAMR as the engine** — weighed on hot-path speed (does WAMR-AOT hold 16K×50FPS?), runtime footprint, and how cleanly each binds to the MoonModule data model. +MoonLight wants ESPLiveScript's **speed** *and* ARTI-FX's **reach** — no single *custom* corner gives both, which is why one redesign path is a **layered custom** engine (portable VM baseline + optional native back-end behind a shared IR). But **WASM+WAMR-AOT collapses that table into one row**: portable to every target *and* ~50% native *and* sandboxed, off the shelf. The two real finalists for the top-down are therefore **(A) build our own VM+IR (+ optional native backend)** vs **(B) adopt WASM/WAMR as the engine** — weighed on hot-path speed (does WAMR-AOT hold 16K×50FPS?), runtime footprint, and how cleanly each binds to the MoonModule data model. ## Architectural primitives observed (the decisions the redesign must make) @@ -171,7 +171,7 @@ Distilled across all four references, these are the load-bearing choices a clean 1. **Execution strategy** — AST-walk vs bytecode-VM vs native-JIT (or a tier ladder). *The* decision; everything follows. ([top-down §2](livescripts-analysis-top-down.md#2-why-native-and-why-our-own-expanding-decision-1)) 2. **The IR seam** — ESPLiveScript emits Xtensa directly (right for one ISA); a multi-target redesign adds a representation between AST and execution so one front-end feeds many backends. The highest-leverage structural change. ([§3.2](livescripts-analysis-top-down.md#32-the-three-tiers-where-the-ir-seam-lives)) -3. **Host-binding model** — all four references use a flat name→pointer registry (`addExternalFunction`/`arti_*`); projectMM's value-add is a MoonModule binding (controls, producer/consumer structs, module role) with no prior art to copy. (§3.4) +3. **Host-binding model** — all four references use a flat name→pointer registry (`addExternalFunction`/`arti_*`); MoonLight's value-add is a MoonModule binding (controls, producer/consumer structs, module role) with no prior art to copy. (§3.4) 4. **Per-frame contract** — script writes its own `loop()` vs an engine-called `renderLed()`; determines the hot-loop shape and where per-pixel dispatch lands. (§3.4) 5. **Compile-once / persist** — a saved artifact skips device-side recompile; portable (one artifact) with a VM/WASM, per-ISA with native. (§3.7) 6. **Memory placement** — code IRAM/PSRAM, data internal/PSRAM, per-target; routed through `platform::` as one policy. (§3.7) @@ -179,7 +179,7 @@ Distilled across all four references, these are the load-bearing choices a clean 8. **Live reconfig + robustness** — a re-pushed script swaps in tick-atomically, old freed, no reboot, no mid-render crash. (§3.6) 9. **Safety / sandboxing** — native can crash, a VM can bound; a user-facing editor raises the stakes. Coupled to the execution-strategy choice. (§4) -## Mapping to projectMM's requirements +## Mapping to MoonLight's requirements | Requirement | ESPLiveScript today | What the redesign must add | |---|---|---| @@ -207,8 +207,8 @@ The full reasoning — why native over WASM-wholesale, why Xtensa-first isn't a Per *Industry standards, our own code*: study the prior art, credit it by name, write our own. This redesign rests on work others did first. - **ESPLiveScript — Yves Bazin (hpwit).** The native-codegen approach the recommendation builds on is his. A from-scratch C-like compiler — tokenizer, parser, register allocator, Xtensa code generator, save/load compiled-binary path, header-only — that runs a script at near hand-written-C++ speed on an ESP32 (85 fps on a 12,288-LED panel where Lua managed 3 and Gravity 10). That is the result that makes "go native, not interpreted" the right call, and the reason this document exists. The structural notes below (global state, no IR, large files) are what a *rewrite toward a different goal* — multi-target, module-bound — has to account for; they are not a verdict on the original, which does its job and does it fast. We carry the idea forward and add the IR seam + the MoonModule binding. -- **ARTI-FX / ARTI — ewowi.** The prior projectMM-family answer to the same problem, written by this analysis's author: the interpreted-effects runtime in WLED MoonModules, on the PEG-grammar ARTI interpreter. It proved the live-scripting idea works end-to-end in this ecosystem (load a script, run it live), and it is the source of lessons carried straight into this design — the `renderFrame`/`renderLed` split, the host-binding shape, and, by being the AST-walking design, the clearest demonstration of *why* a 16K hot path wants native or VM execution over tree-walking. The redesign trades its interpreter for native speed; it keeps its product shape and its lessons. -- **MoonLight — MoonModules** (the [effects tutorial](https://moonmodules.org/MoonLight/moonlight/effects-tutorial/), the staging spine and the host-binding surface model). See the per-engine sections above for what each contributes. +- **ARTI-FX / ARTI — ewowi.** The prior MoonLight-family answer to the same problem, written by this analysis's author: the interpreted-effects runtime in WLED MoonModules, on the PEG-grammar ARTI interpreter. It proved the live-scripting idea works end-to-end in this ecosystem (load a script, run it live), and it is the source of lessons carried straight into this design — the `renderFrame`/`renderLed` split, the host-binding shape, and, by being the AST-walking design, the clearest demonstration of *why* a 16K hot path wants native or VM execution over tree-walking. The redesign trades its interpreter for native speed; it keeps its product shape and its lessons. +- **MoonLight — MoonModules** (the [effects tutorial](https://moonmodules.org/projectMM/moonlight/effects-tutorial/), the staging spine and the host-binding surface model). See the per-engine sections above for what each contributes. Activity + credits also in the digest [friend-repos/hpwit-ESPLiveScript.md](../../friend-repos/hpwit-ESPLiveScript.md). @@ -220,18 +220,18 @@ The open questions the survey surfaced are **resolved in the top-down**, each in Decisions from the design discussion that produced this survey. These are *direction*, terse on purpose; the top-down expands each into full reasoning, an API/architecture, and a staged plan. (Mirrors the LED-driver bottom-up's product-owner-direction section.) -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. +1. **Execution = native, the standout.** Build our own **native-codegen** engine (ESPLiveScript-class speed, near-100%) — the differentiator; MoonLight 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_.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. +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 MoonLight 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/projectMM/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. 9. **Infinitely scalable.** Run *as many* live scripts concurrently as memory allows, exploiting PSRAM — each script is an independent compiled unit, the ceiling is free heap, not a fixed slot count. Many small scripted modules coexist; the device hosts what fits and degrades gracefully when it doesn't (the same scaling-to-available-memory contract the light pipeline already honours). 10. **Inline execution by default; task is the exception.** A scripted effect/layout/modifier/driver runs *inline in the `Scheduler` tick*, called exactly like a compiled effect's `loop()` — one mental model, no cross-thread sync to reach the buffer/`AudioFrame`, and it runs on the render task's *internal-RAM* stack (fast). Task-per-script isn't blocked on memory (a task stack can live in PSRAM), but it pays two costs inline doesn't: per-task **scheduling overhead** (a context switch per task per frame — hundreds of tasks thrash the scheduler, a ceiling independent of memory), and a **PSRAM-backed stack is hot-path-slow** (PSRAM latency on every per-pixel local access, ~12 vs ~80 MB/s). So inline keeps the per-script stack fast and free, and PSRAM is spent on script *code + data* (decision 9) rather than per-script stacks. A pinned task is the narrow, documented opt-in *only* for a long/blocking *core* script (e.g. slow sensor I/O) that must not share the render tick — never the default, never for a pipeline script. 11. **Sequencing: hybrid (depth-first to hello-world, then prove the seam on a 2nd ISA early).** Build the full vertical slice on Xtensa just far enough to run hello-world native (classic/S3), then *immediately* prove a minimal second-ISA backend (P4/RISC-V, or desktop x86-64) on that same slice — before deepening to controls/math/2D/3D. This retires the project's biggest risk (does the IR seam genuinely decouple front-end from backend?) at hello-world cost, when fixing it is cheap, rather than discovering a leak after six stages. Then deepen, primarily on Xtensa; the full second backend follows later. -12. **Domain-neutral engine core, thin binding.** The engine (front-end + IR + backends) is domain-neutral core — it never depends on projectMM; the binding (`MoonLiveHost`/`MoonLiveEffect`) depends on the engine, one-directionally, through the engine's public API + a tiny injectable platform seam (`allocExec`/`alloc`). This clean layering is adopted *because it is what makes projectMM-as-a-library optimal* (the [*Domain-neutral core*](../../CLAUDE.md) principle applied), and is **never compromised** for any separability — if a separation would cost projectMM's speed/simplicity/hot-path/principles, it isn't done. +12. **Domain-neutral engine core, thin binding.** The engine (front-end + IR + backends) is domain-neutral core — it never depends on MoonLight; the binding (`MoonLiveHost`/`MoonLiveEffect`) depends on the engine, one-directionally, through the engine's public API + a tiny injectable platform seam (`allocExec`/`alloc`). This clean layering is adopted *because it is what makes MoonLight-as-a-library optimal* (the [*Domain-neutral core*](../../CLAUDE.md) principle applied), and is **never compromised** for any separability — if a separation would cost MoonLight's speed/simplicity/hot-path/principles, it isn't done. ## Out of scope for Stage 1 Final VM-vs-JIT decision; the IR design; the language grammar spec; the web editor; per-engine benchmarking on real hardware; the MoonModule-binding mechanics; the sandboxing depth. All belong to the top-down document the prompt above generates. diff --git a/docs/work/future/livescripts-analysis-top-down.md b/docs/work/future/livescripts-analysis-top-down.md index 2db65990..eaa8296b 100644 --- a/docs/work/future/livescripts-analysis-top-down.md +++ b/docs/work/future/livescripts-analysis-top-down.md @@ -1,17 +1,17 @@ # MoonLive — live-script engine, top-down redesign -> **Forward-looking research document — exception to CLAUDE.md present-tense rule.** **MoonLive** is projectMM's live-script engine (the Moon family: MoonLight, MoonDeck, MoonLive — author an effect as text, see it live). Stage-2 companion to [livescripts-analysis-bottom-up.md](livescripts-analysis-bottom-up.md) (read first: it deep-reads the ESPLiveScript fork, surveys WLED ARTI-FX, the embedded-VM field, and a portable WASM fallback, and ends with the product-owner-direction decisions this document expands). It reasons from projectMM's end goal — *author a script as text, run it on a running device on the next tick* — down to a reference architecture, a concrete API, a performance budget, and a staged spike plan. Modelled on [leddriver-analysis-top-down.md](../../work/future/leddriver-analysis-top-down.md). This expands the eight decisions already made; it does not re-open them. All design is written fresh against projectMM's architecture — prior art (ESPLiveScript, ARTI-FX, MoonLight) is credited, not traced. +> **Forward-looking research document — exception to CLAUDE.md present-tense rule.** **MoonLive** is MoonLight's live-script engine (the Moon family: MoonLight, MoonDeck, MoonLive — author an effect as text, see it live). Stage-2 companion to [livescripts-analysis-bottom-up.md](livescripts-analysis-bottom-up.md) (read first: it deep-reads the ESPLiveScript fork, surveys WLED ARTI-FX, the embedded-VM field, and a portable WASM fallback, and ends with the product-owner-direction decisions this document expands). It reasons from MoonLight's end goal — *author a script as text, run it on a running device on the next tick* — down to a reference architecture, a concrete API, a performance budget, and a staged spike plan. Modelled on [leddriver-analysis-top-down.md](../../work/future/leddriver-analysis-top-down.md). This expands the eight decisions already made; it does not re-open them. All design is written fresh against MoonLight's architecture — prior art (ESPLiveScript, ARTI-FX, MoonLight) is credited, not traced. ## TL;DR -- **MoonLive is our native-codegen engine** — a real compiler (lex → parse → AST → **IR** → native machine code), executed by direct function-pointer call, so a scripted effect runs at **near-100% native speed** in the render hot path. That speed, bound to a real module system, is projectMM's standout. +- **MoonLive is our native-codegen engine** — a real compiler (lex → parse → AST → **IR** → native machine code), executed by direct function-pointer call, so a scripted effect runs at **near-100% native speed** in the render hot path. That speed, bound to a real module system, is MoonLight's standout. - **One narrow boundary, three tiers.** The seam is `MoonLive::run()` (the analog of `LedDriver::push()`). Above it: a **platform-independent front-end** (tokenizer → parser → typed AST). Below it: a **typed IR** (the seam that lets one front-end feed many backends) → a **per-ISA backend** (Xtensa first). The IR is compile-time only — **zero per-pixel runtime cost**; it lowers to the same native instructions a hand-written backend would emit. -- **Xtensa first, no dead-ends.** Ship the Xtensa backend (classic ESP32 + S3 — projectMM's first targets, the bench hardware) as a complete, blazing first increment. RISC-V (P4), ARM (Teensy), x86-64/ARM64 (desktop) are each *a new backend behind the unchanged IR* later — additive, never a rewrite. **WASM/WAMR is the per-target fallback** (IR→WASM is one more backend), so no target is ever blocked and a true sandbox stays reachable. +- **Xtensa first, no dead-ends.** Ship the Xtensa backend (classic ESP32 + S3 — MoonLight's first targets, the bench hardware) as a complete, blazing first increment. RISC-V (P4), ARM (Teensy), x86-64/ARM64 (desktop) are each *a new backend behind the unchanged IR* later — additive, never a rewrite. **WASM/WAMR is the per-target fallback** (IR→WASM is one more backend), so no target is ever blocked and a true sandbox stays reachable. - **Source language: a C-subset, "as close as possible" to a precompiled effect, with pragmatic simplifications.** The effect *body* ports near-verbatim from a `.h` (types, `for`, integer + float math, `static_cast`, `RGB`, `hsvToRgb`, buffer writes). The C++ *file/class ceremony* that buys nothing in a script (`#pragma`/`#include`/`namespace`; lightened: `class : public EffectBase`/`override`/the `controls_`-dance) is supplied by the engine. **Not** JS (doubles = slow + further from our code); **not** full C++ (object model = build + hot-path cost for zero-value boilerplate). - **Minimal-ceremony controls.** A control is a near-plain top-level variable with a range annotation; the engine derives the MoonModule control + UI + persistence. -- **MoonModule-first.** A scripted module **is** a `MoonModule` (role, controls, `loop()`, generic UI, lifecycle, robustness, live-reconfig). The script ⇄ host binding reaches the `Buffer` / `AudioFrame` / LUT through the producer/consumer pull pattern — no copy. This is the projectMM value-add with no prior art to trace. +- **MoonModule-first.** A scripted module **is** a `MoonModule` (role, controls, `loop()`, generic UI, lifecycle, robustness, live-reconfig). The script ⇄ host binding reaches the `Buffer` / `AudioFrame` / LUT through the producer/consumer pull pattern — no copy. This is the MoonLight value-add with no prior art to trace. - **Safety staged.** Ship cheap first — array **bounds-checking** (a compare-branch per indexed access, low single-digit %, switchable off) + a **watchdog / instruction budget** (kill a runaway loop, near-free). The expensive **true memory sandbox** (WASM gives it free; native can't cheaply) is deferred, reachable via the IR→WASM fallback only if a public editor in the field demands it. -- **Staging spine = the [MoonLight effects tutorial](https://moonmodules.org/MoonLight/moonlight/effects-tutorial/) ladder.** Each tutorial rung (random pixel → control → trails → oscillators → 2D → 3D → audio) is one engine-capability spike with a concrete acceptance bar. **RipplesEffect.h is the language-fidelity *graduation test*** (does the C-subset handle float trig + 3D + `memset` near-verbatim?) — the hard case, not the hello-world. +- **Staging spine = the [MoonLight effects tutorial](https://moonmodules.org/projectMM/moonlight/effects-tutorial/) ladder.** Each tutorial rung (random pixel → control → trails → oscillators → 2D → 3D → audio) is one engine-capability spike with a concrete acceptance bar. **RipplesEffect.h is the language-fidelity *graduation test*** (does the C-subset handle float trig + 3D + `memset` near-verbatim?) — the hard case, not the hello-world. - **Load-bearing spike:** a minimal native Xtensa engine running the tutorial's hello-world (`setRGB(random16(N), blue)`) live on an ESP32-S3, hitting the frame budget *and* surviving a deliberately-bad script via cheap safety. If native-with-cheap-safety can't hold 16K×50FPS, the fallback is demote-to-WASM/WAMR — a backend swap behind the IR, not a restart. - **Cost, eyes open.** A real compiler is more work than adopting an off-the-shelf VM — weeks to the first beautiful Xtensa increment, each ISA backend its own increment later. The deliberate trade for native speed + a differentiator, mitigated exactly as the LED drivers were: spike-ordered, one complete increment at a time, the hard multi-target part deferred behind a seam that keeps it reachable. @@ -31,7 +31,7 @@ A user writes a script — an effect, layout, modifier, driver, or a domain-neut The design space runs from interpreted to native, and the choice is driven by the hot-path requirement: -- **AST-walk** (ARTI-FX): stores values as `double` and walks the tree per frame — which buys maximum flexibility and runs on any platform unchanged, at the cost of per-frame speed. That speed cost is what rules it out *for the 16K×50FPS hot path specifically* — not a flaw, a different trade than projectMM needs here. (For a slow core script off the hot path, that trade would be fine.) +- **AST-walk** (ARTI-FX): stores values as `double` and walks the tree per frame — which buys maximum flexibility and runs on any platform unchanged, at the cost of per-frame speed. That speed cost is what rules it out *for the 16K×50FPS hot path specifically* — not a flaw, a different trade than MoonLight needs here. (For a slow core script off the hot path, that trade would be fine.) - **A bytecode VM is the middle ground** — a compact opcode stream run by a dispatch loop, far faster than tree-walking but paying a per-opcode dispatch tax every operation; at 16K×50FPS = 800K px/s that tax is the open question, not a given. - **Native JIT is the only thing that reaches ~100%** (ESPLiveScript's 85 fps ≈ hand-written C++ — hpwit's result). The differentiator we're after is the *combination*: native speed **and** multi-target **and** bound to a real module system. Each prior engine has part of it — ESPLiveScript has the native speed (Xtensa); ARTI-FX has the live-scripting product shape and runs anywhere (interpreted). Neither combines all three; that combination is the open space. - **WASM/WAMR** gets portability + free sandbox but tops at ~50% native (WAMR-AOT) with a 200KB+ runtime — kept as the fallback, not the flagship, because native speed is what we're chasing. @@ -103,13 +103,13 @@ public: void onBuildControls() override { // DYNAMIC controls — re-runs when the script changes controls_.addText("source", source_, kMaxSource); // the script text (persisted, editable) // The engine declares its controls as NEUTRAL data; the binding translates to controls_. - for (auto& c : engine_.declaredControls()) // {name,type,min,max,default} — no projectMM type - controls_.add(c.name, c.type, c.min, c.max); // binding maps neutral → projectMM control (§3.5) + for (auto& c : engine_.declaredControls()) // {name,type,min,max,default} — no MoonLight type + controls_.add(c.name, c.type, c.min, c.max); // binding maps neutral → MoonLight control (§3.5) } // onBuildState is the rebuild sweep: it fires on a source edit (recompile) AND on a // grid/size change (the engine re-sizes its script buffers for the new dimensions). - // projectMM has no separate onSizeChanged — resize routes through onBuildState, so the + // MoonLight has no separate onSizeChanged — resize routes through onBuildState, so the // dynamic-memory re-allocation rides the same hook every config change already uses. void onBuildState() override { engine_.compile(source_); // recompile if source changed @@ -136,17 +136,17 @@ The same shape gives `MoonLiveLayout` (role `Layout`, emits coordinates), `MoonL **A scripted module implements the whole `MoonModule` lifecycle, not just `loop()`** — that's what makes it a first-class module and what answers dynamic controls / dynamic memory / cleanup: - **`onBuildControls()` — dynamic controls.** Re-runs whenever the module rebuilds, so a script that declares different controls (a new `@control` var) gets a different control set in the UI + persistence, live. The controls are *the script's*, not a fixed list. -- **`onBuildState()` — dynamic memory on size change.** projectMM routes a grid/size change through `onBuildState` (the same rebuild sweep that applies every config change without a reboot), so MoonLive re-allocates its per-size script buffers here (`allocForSize`, PSRAM-first per §3.7). There is no bespoke `onSizeChanged` — using the existing hook means a scripted module resizes exactly like a compiled one, and inherits the no-reboot + robustness contracts for free. +- **`onBuildState()` — dynamic memory on size change.** MoonLight routes a grid/size change through `onBuildState` (the same rebuild sweep that applies every config change without a reboot), so MoonLive re-allocates its per-size script buffers here (`allocForSize`, PSRAM-first per §3.7). There is no bespoke `onSizeChanged` — using the existing hook means a scripted module resizes exactly like a compiled one, and inherits the no-reboot + robustness contracts for free. - **`setup()` / `teardown()` — acquire / release (the destructor role).** `teardown` frees the compiled code block + the script's data arena back to the heap, so deleting a scripted module returns all its memory — the lifecycle that makes "as many scripts as memory allows" (§3.7) safe to add *and remove* in any order. - **`onUpdate(name)` — cheap per-control reaction.** A control edit pokes the running script's bound variable without a full recompile (the fast path for a slider drag); only a *source* edit triggers the heavier `onBuildState` recompile. 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_.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.) +**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 MoonLight 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 MoonLight's `controls_.addControl(...)`; it maps a grid resize to `allocForSize`. **The engine never takes a `ControlList`, a `Buffer`, or any MoonLight 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) -Rather than a flat name→pointer registry (the host-binding shape surveyed engines share), projectMM uses a **MoonModule-aware `MoonLiveHost`** that exposes the producer/consumer data the script needs, by reference, no copy — the same pull pattern effects already use (`EffectBase::buffer()/width()/elapsed()`, [Layer.h:499-504](../../src/light/layers/Layer.h)): +Rather than a flat name→pointer registry (the host-binding shape surveyed engines share), MoonLight uses a **MoonModule-aware `MoonLiveHost`** that exposes the producer/consumer data the script needs, by reference, no copy — the same pull pattern effects already use (`EffectBase::buffer()/width()/elapsed()`, [Layer.h:499-504](../../src/light/layers/Layer.h)): - **Buffer + geometry** — `width()`, `height()`, `depth()`, `channelsPerLight()`, `nrOfLights()`, `elapsed()`, and pixel writers `setRGB(i,c)` / `setRGBXY(x,y,c)` / `setRGBXYZ(x,y,z,c)` (the MoonLight tutorial's exact surface). These compile to direct loads/stores against `layer_->buffer()` — the **identity-mapping fast path** preserved (the script writes the real buffer, no intermediate copy). - **Controls** — the script's declared variables (§3.5) bind by reference so a UI control edit updates the running script live. @@ -164,7 +164,7 @@ byte speed = 50; // @control 0..99 → controls_.addControl("speed" 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` + `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".) +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 MoonLight type); the *binding* reads that list and calls the normal `controls_.add(...)` the rest of MoonLight 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 MoonLight-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) @@ -198,7 +198,7 @@ Net: no resource cliff — at-rest per-effect cost stays PSRAM-friendly (code + ### 3.8 Execution model — inline by default, task as the exception (decision: sync) -**A script runs inline in the `Scheduler` tick by default — not in its own task.** A scripted effect's `loop()` is called exactly like a compiled effect's `loop()`, on the render task, each tick. The task-per-script model some engines use fits when a script *is* the top-level loop and owns the device; in projectMM a scripted module is one `MoonModule` among many, called from the same single-threaded render loop as every compiled module, so inline is the consistent shape. Three reasons make inline the default, not just a choice: +**A script runs inline in the `Scheduler` tick by default — not in its own task.** A scripted effect's `loop()` is called exactly like a compiled effect's `loop()`, on the render task, each tick. The task-per-script model some engines use fits when a script *is* the top-level loop and owns the device; in MoonLight a scripted module is one `MoonModule` among many, called from the same single-threaded render loop as every compiled module, so inline is the consistent shape. Three reasons make inline the default, not just a choice: - **Consistency.** A scripted effect behaves identically to a compiled one — same call site, same hot-path rules, same `Scheduler`. One mental model, and the UI/persistence/lifecycle treat it like any other module. - **It sidesteps two costs task-per-script can't.** Task stacks *can* live in PSRAM (`xTaskCreateWithCaps`, `MALLOC_CAP_SPIRAM`), so task-per-script is not blocked on *internal* RAM — but it pays two costs inline doesn't: (a) **scheduling overhead** — each task is a TCB + scheduler bookkeeping + a context switch; hundreds of tasks all wanting to run each frame thrash the scheduler instead of rendering, a ceiling that has nothing to do with memory; and (b) **a PSRAM-backed task stack is hot-path-slow** — a per-pixel inner loop touching locals on a PSRAM stack pays PSRAM latency (~12 MB/s vs internal ~80 MB/s) every access, exactly what a 16K×50FPS loop can't afford. An inline script runs on the render task's *internal-RAM* stack, fast, with no per-task scheduler cost. So PSRAM scales the script's *code + data* (§3.7), and inline keeps the per-script *stack* fast and free — the two pull together, where task-per-script would put them in tension. @@ -210,17 +210,17 @@ An inline script obeys the no-blocking-hot-path rule (it can't `delay`); a runaw ### 3.9 Layering — a domain-neutral engine core, a thin binding (decision: domain-neutral core) -The tiers above already separate cleanly along projectMM's own *Domain-neutral core* principle, and the layering is held to it deliberately: +The tiers above already separate cleanly along MoonLight's own *Domain-neutral core* principle, and the layering is held to it deliberately: -- **The engine core (MoonLive) is domain-neutral.** "MoonLive" is the engine's *name*, not a coupling — the front-end (`src/core/moonlive/`) and the IR + backends (`src/platform//moonlive_backend_*`) know the *language* and the *ISA*, never `Buffer`, `EffectBase`, the module tree, or anything light- or projectMM-specific. The core's only outward contract is a tiny injectable platform seam (`platform::allocExec` / `alloc` / `millis`) — a handful of functions, not a reach into projectMM's full platform layer. -- **The binding is the only projectMM-coupled layer**, and it is *thin*. `MoonLiveHost` + `MoonLiveEffect`/`MoonLiveLayout`/… (`src/light/moonlive/`, with a core `MoonLiveModule` for sensor rules) sit **on top of** the engine's public API and consume it; they never reach into engine internals. -- **Dependency direction is one-way:** the binding depends on the engine; the engine never depends on the binding (or on projectMM). The engine does not `#include` projectMM; projectMM `#include`s the engine. +- **The engine core (MoonLive) is domain-neutral.** "MoonLive" is the engine's *name*, not a coupling — the front-end (`src/core/moonlive/`) and the IR + backends (`src/platform//moonlive_backend_*`) know the *language* and the *ISA*, never `Buffer`, `EffectBase`, the module tree, or anything light- or MoonLight-specific. The core's only outward contract is a tiny injectable platform seam (`platform::allocExec` / `alloc` / `millis`) — a handful of functions, not a reach into MoonLight's full platform layer. +- **The binding is the only MoonLight-coupled layer**, and it is *thin*. `MoonLiveHost` + `MoonLiveEffect`/`MoonLiveLayout`/… (`src/light/moonlive/`, with a core `MoonLiveModule` for sensor rules) sit **on top of** the engine's public API and consume it; they never reach into engine internals. +- **Dependency direction is one-way:** the binding depends on the engine; the engine never depends on the binding (or on MoonLight). The engine does not `#include` MoonLight; MoonLight `#include`s the engine. -Why this matters concretely: **it is what makes projectMM-as-a-library optimal.** A clean library needs exactly this — a domain-neutral core with a one-directional dependency and a thin, replaceable binding, so the whole stack composes without circular dependencies or hidden coupling. So this layering is not extra structure for its own sake; it is the *Domain-neutral core* + *Complexity lives in core, domain modules stay simple* principles applied, and it is the same boundary projectMM needs to be a well-formed library. +Why this matters concretely: **it is what makes MoonLight-as-a-library optimal.** A clean library needs exactly this — a domain-neutral core with a one-directional dependency and a thin, replaceable binding, so the whole stack composes without circular dependencies or hidden coupling. So this layering is not extra structure for its own sake; it is the *Domain-neutral core* + *Complexity lives in core, domain modules stay simple* principles applied, and it is the same boundary MoonLight needs to be a well-formed library. -A true property of that boundary, worth stating: because the core (MoonLive) knows only the *language* and the *ISA* — never LEDs, buffers, or projectMM — the same front-end + IR + backends would serve a **wholly different host**: a different output device, or a different application entirely (a script that drives a display, reads a keypad, computes a result). Such a host writes its own thin binding against the same public API + platform seam; nothing in the core changes. The IR seam is what makes that portable, too — the host targets whatever chip it likes by writing one backend behind the unchanged IR. This is a *consequence* of building the core domain-neutral for projectMM, not a goal we design toward — but it is real, and it is the mark of a well-factored core: it doesn't care what you point it at. +A true property of that boundary, worth stating: because the core (MoonLive) knows only the *language* and the *ISA* — never LEDs, buffers, or MoonLight — the same front-end + IR + backends would serve a **wholly different host**: a different output device, or a different application entirely (a script that drives a display, reads a keypad, computes a result). Such a host writes its own thin binding against the same public API + platform seam; nothing in the core changes. The IR seam is what makes that portable, too — the host targets whatever chip it likes by writing one backend behind the unchanged IR. This is a *consequence* of building the core domain-neutral for MoonLight, not a goal we design toward — but it is real, and it is the mark of a well-factored core: it doesn't care what you point it at. -**Hard constraint: the layering is justified entirely by projectMM's optimality — never compromised for it.** The clean engine/binding split is adopted only because it makes projectMM architecturally sound, fast, and CLAUDE.md/architecture.md-compliant (domain-neutral core, data-over-objects on the hot path, the platform boundary). If any separability would cost projectMM's optimality — a slower hot path, a heavier binding, an abstraction the engine doesn't need — it is **not** done. The binding stays thin and the core stays neutral *because that is the optimal projectMM design*, full stop; nothing in the layering is bent toward a use beyond projectMM. +**Hard constraint: the layering is justified entirely by MoonLight's optimality — never compromised for it.** The clean engine/binding split is adopted only because it makes MoonLight architecturally sound, fast, and CLAUDE.md/architecture.md-compliant (domain-neutral core, data-over-objects on the hot path, the platform boundary). If any separability would cost MoonLight's optimality — a slower hot path, a heavier binding, an abstraction the engine doesn't need — it is **not** done. The binding stays thin and the core stays neutral *because that is the optimal MoonLight design*, full stop; nothing in the layering is bent toward a use beyond MoonLight. ## 4. Safety — staged (decision 6) @@ -311,7 +311,7 @@ The diff is exactly the ceremony: gone are `class`/`override`/`onBuildControls`/ ## 6. Testing — the engine's biggest structural advantage -A live-script engine is one of the **most testable things projectMM can build**, and projectMM's two-tier test framework ([testing.md](../../reference/testing.md): doctest unit tests + JSON scenarios, each run in-process *and* live) maps onto it almost perfectly. This is a genuine edge: the bottom-up's structural note that ESPLiveScript ships `.ino` examples and no unit suite isn't a knock on it — it's the gap our framework closes. **Every live-script feature gets pinned by a test, back to back**, because two properties make a compiler exceptionally test-friendly: +A live-script engine is one of the **most testable things MoonLight can build**, and MoonLight's two-tier test framework ([testing.md](../../reference/testing.md): doctest unit tests + JSON scenarios, each run in-process *and* live) maps onto it almost perfectly. This is a genuine edge: the bottom-up's structural note that ESPLiveScript ships `.ino` examples and no unit suite isn't a knock on it — it's the gap our framework closes. **Every live-script feature gets pinned by a test, back to back**, because two properties make a compiler exceptionally test-friendly: 1. **Every compiler stage is a pure input→output function.** Lex, parse, IR-lower, codegen each take a known input and produce a deterministic output — the easiest thing in the world to unit-test, with no hardware and no flakiness. 2. **A script's *result* is deterministic and exactly assertable.** A known script over a known grid at a known `elapsed()` produces an exact buffer — byte-for-byte checkable. There is no "looks about right"; there is a golden buffer. @@ -331,7 +331,7 @@ Scenarios exercise a scripted module *as a wired `MoonModule`* — the integrati - **The MoonModule binding** — `add_module MoonLiveEffect`, `set_control source=