diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 902ed172a..53ae79f16 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -10,6 +10,8 @@ reviews: enabled: false drafts: false base_branches: ["main"] + # Never pause: every review is asked for by hand, so a pause only swallows the next request. + auto_pause_after_reviewed_commits: 0 path_filters: - "!**/build/**" diff --git a/.github/workflows/generated-files.yml b/.github/workflows/generated-files.yml new file mode 100644 index 000000000..cbf742865 --- /dev/null +++ b/.github/workflows/generated-files.yml @@ -0,0 +1,28 @@ +name: Generated files + +# A contributor's pull request leaves the generated files out: the metrics and a scenario's observation blocks are measured on the contributor's machine, and the merge regenerates them on ours. A warning rather than a failure, since a pull request can mean to change one, a new scenario for one. + +on: + pull_request: + paths: + - 'docs/reference/metrics/**' + - 'test/scenarios/**' + +jobs: + warn: + # Pull requests from a fork only: on ours, the merge is where the files are regenerated. + if: github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Name each generated file the pull request changes + env: + BASE: ${{ github.event.pull_request.base.sha }} + run: | + # Metrics are generated whole; a scenario only when it already existed, since a new one is written by hand. + { git diff --name-only "$BASE"...HEAD -- docs/reference/metrics + git diff --name-only --diff-filter=M "$BASE"...HEAD -- test/scenarios; } | while read -r f; do + echo "::warning file=$f::Generated on your machine: the merge regenerates it on ours, so leave it out of the pull request unless you mean to change it." + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f111b2947..7bcae95a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,9 @@ name: Release -# Tag-triggered release pipeline. Builds 4 ESP32 firmwares + 2 desktop binaries -# in parallel, generates 4 ESP Web Tools manifests, attaches everything to the -# GitHub release, and deploys the installer page to GitHub Pages. +# The release pipeline. Builds every shipped ESP32 firmware (the matrix from mooninstaller/firmwares.json), the Teensy firmware and the desktop packages (macOS arm64, Windows x64, Linux x64 and arm64) in parallel, writes an ESP Web Tools manifest per firmware, attaches everything to the GitHub release, publishes the container image, and deploys the installer page to GitHub Pages. # # Two triggers produce a release: -# - push to main (src/esp32/CMakeLists.txt changed) → tags/publishes as `latest` -# (a moving prerelease, always the newest build from main) +# - push to main touching a path below → tags/publishes as `latest` (a moving prerelease, always the newest build from main) # - push of a vX.Y.Z tag → stable/RC release # # Manual replay: workflow_dispatch with an existing tag, useful for retries @@ -179,15 +176,7 @@ jobs: - name: Build firmware uses: espressif/esp-idf-ci-action@v1 with: - # Match the local development IDF (v6.1-rc1, commit 44f0c59f). - # The project uses v6-era APIs — `esp_eth_phy_new_generic`, the new - # mDNS component manager, `int clock_gpio` in the EMAC config (v5.x - # had a strongly-typed enum). v5.4 fails to compile platform_esp32.cpp. - # `v6.1-rc1` is the espressif/idf Docker tag pinned to the same - # tagged pre-release the local pin uses — fixed and signed (not the - # rolling `release-v6.1` branch tag), so CI and local build the exact - # same tree. This tracks the v6.1 line toward GA; re-pin to the `v6.1` - # tag once it ships (a deliberate re-test pass, see docs/how-to/building.md). + # The espressif/idf Docker tag of the local pin (PINNED_IDF_COMMIT in build_esp32.py), a fixed release tag rather than the rolling `release-v6.1`, so CI and local build the same tree; v5.x lacks the v6 APIs platform_esp32.cpp uses, and moving the pin is a re-test pass (the bump-idf process in moonwalk/09-plan-setup.yaml). esp_idf_version: v6.1 # The IDF target is the firmware's declared chip, carried by the matrix from firmwares.json. target: ${{ matrix.chip }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1935ccebd..00c082365 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,6 @@ name: Test -# Fast host-side unit tests for the Python (MoonDeck / build scripts) and JS -# (web installer) code that the C++ ctest/scenario suites can't reach. Runs on -# every PR and on pushes to main/next-iteration. Scoped to the paths these tests -# cover so a docs-only or pure-firmware change doesn't spend a runner here. +# Fast host-side unit tests for the Python (MoonDeck / build scripts) and JS (web installer) code that the C++ unit and scenario suites can't reach. Runs on every PR and on pushes to main, scoped to the paths these tests cover so a docs-only or pure-firmware change doesn't spend a runner here. # # Today this pins the Improv frame wire format (test/python + test/js assert a # shared golden vector so the device C++, Python, and JS builders can't drift). @@ -30,6 +27,11 @@ on: # the "this is a subset of C++" claim needs re-checking. - 'moonlive/**' - 'src/light/moonlive/MoonLiveBuiltins_light.h' + # MoonWalk's stage files, the map's renderer and what the generator checks them against. + - 'moonwalk/**' + - 'docs/assets/moonwalk.js' + - 'docs/help/help.json' + - 'docs/explanation/architecture/index.md' # The sanitizer jobs below compile the WHOLE C++ suite, so they have to run when any of it # changes. Without these, a C++ change could break them with no run at all, and the fix could # not prove itself either: exactly what happened to a missing in a unit test. @@ -72,7 +74,7 @@ jobs: # `pytest test/python` here would compile all 34 scripts a second time. - name: pytest run: > - uv run --with pytest --with pyserial --with markdown --with wled + uv run --with pytest --with pyserial --with markdown --with wled --with pyyaml --with jsonschema pytest test/python -q --ignore=test/python/test_scripts_are_cpp.py js: diff --git a/.gitignore b/.gitignore index 371b6d0a6..0d28116fe 100644 --- a/.gitignore +++ b/.gitignore @@ -108,10 +108,6 @@ moonbase/dependencies.lock moonbase/build/ esp32/monitor.log -# Generated live-scenario baseline cache (regenerated by run_live_scenario.py -# --update-baseline; per-target observations live in the scenario JSONs instead). -test/scenario-baseline.json - # IDE .vscode/ .idea/ diff --git a/.vale.ini b/.vale.ini index 23ad6aee6..508637760 100644 --- a/.vale.ini +++ b/.vale.ini @@ -35,6 +35,10 @@ BasedOnStyles = MoonLight [*.py] BasedOnStyles = MoonLight +# MoonWalk's stage files: a process's lines, intro and detail are the pages the docs build generates from them, so they keep the pages' rules. +[moonwalk/*.yaml] +BasedOnStyles = MoonLight + # Upstream code, vendored rather than written here: not our prose to rule on. [src/platform/desktop/vendor/**] BasedOnStyles = @@ -61,7 +65,7 @@ BasedOnStyles = # suggestion becomes an error there, so a regression fails the gate instead of annotating a PR. # 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] +[docs/how-to/installing-on-linux.md] BasedOnStyles = MoonLight MoonLight.SentenceLength = error MoonLight.Weasel = error diff --git a/CLAUDE.md b/CLAUDE.md index e8419f070..439205526 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,9 +132,11 @@ Keep each commit under ~100 changed files: CodeRabbit reviews the files a pushed **A contributed pull request is one topic, max 400 changed lines and ten files**, tests included. Generated files (metrics, images, scenario observations) are a category of their own, outside the count. Past a few hundred lines a review stops catching defects (the SmartBear and Cisco review study; Google's guide on small changes), and the product owner reviews every line. Every pull request is based on main. The one exception is a step of a larger topic, which lands as its own pull request and may build on the step below it. A stacked pull request from a fork shows its parents, so it is reviewed once the one below it merges. +**A contribution is reviewed and finished as [Review and finish a contribution](docs/moonwalk.md#review-contribution) describes**. The contributor fixes their own code first and the maintainer finishes, case by case. The merge keeps their commits and authorship. + ### Build and test -Implement against [the architecture](docs/explanation/architecture/index.md) and [coding-standards.md](docs/contributing/coding-standards.md). Everything build, flash, run and monitor: [building.md](docs/how-to/building.md). Per-script reference: [MoonDeck.md](moondeck/MoonDeck.md). +Implement against [the architecture](docs/explanation/architecture/index.md) and [coding-standards.md](docs/contributing/coding-standards.md). Everything build, flash, run and monitor: [the contributor processes in MoonWalk](docs/moonwalk.md#desktop-build-test). Per-script reference: [MoonDeck.md](moondeck/MoonDeck.md). ```mermaid flowchart LR diff --git a/README.md b/README.md index 5799fe8fa..3d5286f1b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Drive large LED installations and DMX fixtures. One source tree drives ESP32, Te ![A 128x128 light wall rendering the ColorTrails effect](docs/assets/light/effects/ColorTrailsEffect.gif) -**Flash an ESP32 from your browser and see lights in under a minute:** open the [web installer](https://moonmodules.org/MoonLight/install/) in Chrome or Edge, plug in your device, and follow it. No toolchain, no recompile. The [Getting started guide](docs/gettingstarted.md) takes you from a blank board to a running light show. +**Flash an ESP32 from your browser and see lights in under a minute:** open the [web installer](https://moonmodules.org/MoonLight/install/) in Chrome or Edge, plug in your device, and follow it. No toolchain, no recompile. [Install MoonLight](docs/moonwalk.md#install) takes you from a blank board to a running device, and [Show an effect on your lights](docs/moonwalk.md#first-light-show) from there to a light show. No hardware handy? The [desktop build](https://github.com/MoonModules/MoonLight/releases/latest) runs the same UI and effect pipeline on macOS, Windows and Linux, driving fixtures over Art-Net, DDP or E1.31. @@ -136,7 +136,7 @@ The browser installer: it picks your device, flashes the matching firmware, and ![The installer picking a device](docs/assets/ui/installer.png) -Architecture: [MoonInstaller](docs/explanation/architecture/mooninstaller.md) · Use it: [Web installer](https://moonmodules.org/MoonLight/install/) · [Getting started](docs/gettingstarted.md) · [source](mooninstaller/README.md) +Architecture: [MoonInstaller](docs/explanation/architecture/mooninstaller.md) · Use it: [Web installer](https://moonmodules.org/MoonLight/install/) · [Install MoonLight](docs/moonwalk.md#install) · [source](mooninstaller/README.md) ### MoonDeck @@ -144,7 +144,7 @@ The developer console: one page that builds, flashes, runs, tests and monitors a ![MoonDeck, the dev console](docs/assets/ui/moondeck_desktop.png) -Architecture: [MoonDeck](docs/explanation/architecture/moondeck.md) · Use it: [MoonDeck](moondeck/MoonDeck.md) · [building.md](docs/how-to/building.md) +Architecture: [MoonDeck](docs/explanation/architecture/moondeck.md) · Use it: [MoonDeck](moondeck/MoonDeck.md) · [Build processes](docs/moonwalk.md#desktop-build-test) ## Performance @@ -164,11 +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/MoonLight/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/MoonLight/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. -- **Linux x64 and arm64**: `.tar.gz`, or `.deb` on Debian, Ubuntu and Raspberry Pi OS. The arm64 build covers a Raspberry Pi, a NanoPi and most 64-bit boards. +**Desktop**: download your build from the [releases page](https://github.com/MoonModules/MoonLight/releases), then open `http://localhost:8080/`. Step by step for macOS, Windows, Linux and Docker, with screenshots: [Install MoonLight](https://moonmodules.org/MoonLight/moonwalk.html#install), with your platform picked. **From source**: you need [uv](https://docs.astral.sh/uv/), CMake 3.20+ and a C++20 compiler, plus ESP-IDF v6.x for ESP32. Then launch [MoonDeck](#moondeck): @@ -176,15 +172,15 @@ Per-grid and per-device tables, free-heap figures, and why WiFi costs what it do uv run moondeck/moondeck.py ``` -Open `http://localhost:8420` to build, run, test, flash and discover devices. Full setup and every target: [building.md](docs/how-to/building.md). +Open `http://localhost:8420` to build, run, test, flash and discover devices. Full setup and every target: [the build processes](docs/moonwalk.md#build-setup). ## Documentation | Document | What's in it | |----------|--------------| -| [Getting started](docs/gettingstarted.md) | Blank board to running light show | +| [Install MoonLight](docs/moonwalk.md#install) | Blank board to a running device | | [the architecture](docs/explanation/architecture/index.md) | How the system is put together | -| [building.md](docs/how-to/building.md) | Build and flash for every target | +| [MoonWalk](docs/moonwalk.md#build-setup) | Build and flash for every target, step by step | | [moonmodules/](docs/moonmodules/) | One page per module: [core](docs/moonmodules/core/) and [light](docs/moonmodules/light/) | | [performance.md](docs/reference/performance.md) | Timing and memory per platform | | [testing.md](docs/reference/testing.md) | What the tests cover | diff --git a/docker-compose.yml b/docker-compose.yml index 4c7276dfa..48b43fea1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,13 +11,7 @@ services: moonlight: build: . - # The released binary is amd64 only, so this line is REQUIRED on an arm64 host (an Apple-silicon - # Mac, an ARM server). Without it compose builds for the host's own architecture and the - # container dies with "rosetta error: failed to open elf" the moment it starts. It is harmless - # on an amd64 host, where it is what would have happened anyway. It goes when the release grows - # an arm64 Linux build. - platform: linux/amd64 - # Or, once images are published, drop `build:` and use one of these instead: + # Or drop `build:` and use a published image, which carries amd64 and arm64: # image: ghcr.io/moonmodules/moonlight:latest # the rolling prerelease # image: ghcr.io/moonmodules/moonlight:6.0.0 # a fixed version container_name: moonlight diff --git a/docs/assets/extra.css b/docs/assets/extra.css index e2fce221e..e4c9222d8 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -142,6 +142,9 @@ } .mm-deck-spread .mm-pcard:hover .mm-pcard-face { border-color: var(--md-accent-fg-color); } /* A compact card has room for two lines of its description, ended with an ellipsis. */ +/* A heading screen readers and the outline keep and the eye skips, so MoonWalk's map starts under the tabs: the standard visually-hidden pattern. */ +.mm-visually-hidden { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } +.mm-walk-js .mm-walk-text { display: none; } /* MoonWalk's plain text, there for search and readers without JavaScript, gives way to the live view */ .mm-deck-compact .md-typeset .mm-pcard p, .md-typeset .mm-deck-compact .mm-pcard p { -webkit-line-clamp: 2; } /* A screenshot of a module's controls shows whole, scaled into the frame; an animation fills it. */ .mm-pcard-front img[src$=".png"] { object-fit: contain; } diff --git a/docs/assets/help-deck.js b/docs/assets/help-deck.js index 3c2822114..f3355928a 100644 --- a/docs/assets/help-deck.js +++ b/docs/assets/help-deck.js @@ -1,9 +1,11 @@ // The card decks: a catalog's modules drawn from help.json by the app's own help.js, so a card reads as the module's help does on a device, and the attribution catalog drawn from its own table. // Mounted on every page that holds a `.mm-deck`, through Material's document$ so instant navigation mounts it too. A deck with a `data-page` holds that page's modules; one without holds the table inside it, which stays what search indexes and a reader without JavaScript sees. -import { helpMarkdown, safeUrl, moduleHelp, controlHelp, emojiTagsFor, emojiChipRow, chipsMatch, typeDisplayName, EMOJI_LABEL, graphemes } from "./help.js"; +import { helpMarkdown, safeUrl, moduleHelp, controlHelp, walkLinks, emojiTagsFor, emojiChipRow, chipsMatch, typeDisplayName, EMOJI_LABEL, graphemes } from "./help.js"; const HELP = new URL("../help/help.json", import.meta.url); +const WALK = new URL("../moonwalk/moonwalk.json", import.meta.url); +const WALK_PAGE = new URL("../moonwalk.html", import.meta.url).href; // The script is running, so the plain list under each deck can hide until its deck mounts. document.documentElement.classList.add("mm-deck-js"); @@ -100,13 +102,13 @@ function playingCard({id, name, emoji, labels, image, alt, line, back, text}) { } // A module's card: its help on the back, the line first as plain text rather than the app's disclosure, so a tap on it turns the card rather than folding the help. -function moduleCard(type, help) { +function moduleCard(type, help, walk) { const name = typeDisplayName(type); const line = document.createElement("p"); line.className = "mm-pcard-line"; line.innerHTML = helpMarkdown(help.line || ""); const back = [line]; - const box = moduleHelp({...help, image: null}, help.doc); + const box = moduleHelp({...help, image: null}, help.doc, walkLinks(walk, type, null, WALK_PAGE)); const summary = box.querySelector(".help-summary"); if (summary) back.push(summary); back.push(box.querySelector(".card-helpbox-body")); @@ -117,7 +119,7 @@ function moduleCard(type, help) { label.className = "control-label"; label.textContent = ctrl; row.appendChild(label); - controlHelp(row, ch); + controlHelp(row, ch, walkLinks(walk, type, ctrl, WALK_PAGE)); back.push(row); } return playingCard({ @@ -363,6 +365,8 @@ async function mount(host) { return; } let modules; + // MoonWalk's index gives the cards their links into it; without it they show the help alone. + const walk = fetch(WALK).then(r => r.ok ? r.json() : null).catch(() => null); try { modules = (await (await fetch(HELP)).json()).modules; } catch { @@ -373,7 +377,8 @@ async function mount(host) { const onPage = (doc) => doc === page || doc.startsWith(page + "#"); // a registry link may name the page without a card const types = Object.keys(modules).filter(t => onPage(modules[t].doc || "")) .sort((a, b) => typeDisplayName(a).localeCompare(typeDisplayName(b))); - mountDeck(host, types.map(t => moduleCard(t, modules[t])), "modules"); + const index = await walk; + mountDeck(host, types.map(t => moduleCard(t, modules[t], index)), "modules"); } // One resize listener for every deck: instant navigation swaps pages without reloading, so a listener per mount would pile up over detached decks. diff --git a/docs/assets/moonwalk.js b/docs/assets/moonwalk.js new file mode 100644 index 000000000..ed35e70c0 --- /dev/null +++ b/docs/assets/moonwalk.js @@ -0,0 +1,871 @@ +// MoonWalk's journey map: every process from moonwalk/*.yaml, drawn from moonwalk.json as a story map, stages (or systems) as columns and each process's steps inside it. The model is docs/work/future/moonwalk.md. +// Two hosts: on the docs site it adds a map button to every page's header and draws the map on the MoonWalk page, through Material's document$ so instant navigation mounts it too; the device's app imports it from the site and calls drawMap with its own state, so a device shows what it has done and what comes next. + +const ROOT = new URL("../", import.meta.url); +const MODEL = new URL("moonwalk/moonwalk.json", ROOT); +// Cytoscape.js, the graph library: compound nodes give the three levels, and it never enters the firmware. +const CYTOSCAPE = "https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.4/cytoscape.min.js"; +const LS_DONE = "mm_moonwalk_done"; // the processes this reader marked done, which the state cannot show on a website +const LS_VIEW = "mm_moonwalk_view"; // the reader's view of the map: actor, columns and hide done, kept per host +const LS_SPLIT = "mm_moonwalk_split"; // the reader's share for the text beside the map, once they drag it +const LS_INPUTS = "mm_moonwalk_inputs"; // the reader's choices, such as their platform, kept per input name + +// The reader's platform, the likely answer to a "which computer" choice, from the browser they read on. +function ownPlatform(options) { + const ua = (navigator.userAgentData?.platform || navigator.platform || navigator.userAgent || "").toLowerCase(); + const guess = ua.includes("win") ? "windows" : ua.includes("mac") ? "macos" : ua.includes("linux") ? "linux" : ""; + return options.find((o) => o.toLowerCase() === guess); +} +function readInputs() { + try { return JSON.parse(localStorage.getItem(LS_INPUTS) || "{}"); } catch { return {}; } +} +function keepInput(name, value) { + try { localStorage.setItem(LS_INPUTS, JSON.stringify({ ...readInputs(), [name]: value })); } catch { /* kept for this view only */ } +} + +// A process's choices as the reader sees them: their own pick first, then what the host knows (the installer knows it installs a board), then the reader's operating system. +function chosenOf(p, presets = {}) { + const chosen = {}; + const inputs = Object.entries(p.inputs?.properties || {}); + const keptAll = inputs.length ? readInputs() : {}; // read once per call, since the map asks for every step on each layout + for (const [name, spec] of inputs) { + const options = spec.enum || []; + const kept = keptAll[name]; + chosen[name] = options.includes(kept) ? kept : options.includes(presets[name]) ? presets[name] : ownPlatform(options) || options[0]; + } + return chosen; +} + +// Whether a step applies to the reader's choices: every `given` naming an input must match its chosen option, one (`equals`) or one of several (`in`). +export const applies = (s, chosen) => (s.given || []).every((g) => !g.input || (g.in ? g.in.includes(chosen[g.input]) : chosen[g.input] === g.equals)); +const ACTOR_COLOR = { user: "#2e7d32", contributor: "#1565c0", "product-owner": "#6a1b9a", agent: "#ef6c00", ci: "#546e7a" }; // who does a step, one color on the map and in the text +const ACTORS = Object.keys(ACTOR_COLOR); +const COL = 340, PROC_H = 64, STEP_H = 36, GAP = 28; + +// The map's styles, injected once, since the app has no copy of the site's stylesheet. Its colors are the docs theme's variables, which a host that is not the site maps its own colors onto, else fixed ones. +const STYLES = ` +:where(.mm-walk-dialog) .mm-map { --mw-panel: 16rem; } /* the text's share in a dialog; :where keeps it below any host's own rule, whatever the order the styles load in */ +.mm-map { --mw-line: var(--md-default-fg-color--lightest, #8884); --mw-bg: var(--md-default-bg-color, transparent); --mw-muted: var(--md-default-fg-color--light, #888); --mw-accent: var(--md-accent-fg-color, #526cfe); + display: grid; grid-template-columns: minmax(0, 1fr) 8px var(--mw-panel, 20rem); grid-template-rows: auto 75vh; gap: 0.5rem; } +.mm-map-bar { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; font-size: 0.75rem; } +.mm-map-bar input[type="search"] { flex: 1 1 12rem; } +.mm-map-bar input[type="search"], .mm-map-bar select, .mm-map-bar button, .mm-map-chip { border: 1px solid var(--mw-line); background: var(--mw-bg); color: inherit; border-radius: 4px; padding: 4px 8px; font: inherit; } +.mm-map-canvas, .mm-map-panel { border: 1px solid var(--mw-line); border-radius: 6px; min-height: 0; } +.mm-map-panel { overflow: auto; font-size: 0.75rem; padding: 0 0.75rem; } +.mm-map-panel h3 { margin-top: 0.75rem; color: var(--mw-accent); } /* the process's title, in the accent color */ +.mm-map-meta { color: var(--mw-muted); } +.mm-map-chips { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 4px; } +.mm-map-chip { border-radius: 999px; padding: 2px 10px; cursor: pointer; } +.mm-map-chip.on { border-color: var(--mw-accent); } +.mm-map-next { cursor: pointer; text-decoration: underline; } +.mm-map.mm-map-reading { --mw-panel: 60%; } +.mm-map-split { cursor: col-resize; border-radius: 4px; background: var(--mw-line); touch-action: none; } +.mm-map-split:hover, .mm-map-split:focus-visible { background: var(--mw-accent); outline: none; } +.mm-map-view { line-height: 1.5; } /* the panel's size, the host's secondary text */ +.mm-map-view h4 { margin: 1.2em 0 0.4em; } +.mm-map-view video, .mm-map-view img { max-width: 100%; border-radius: 6px; } +.mm-map-panel a { color: var(--mw-accent); text-decoration: underline; } /* a link reads as one in every host, whose own styles may reset it */ +.mm-map-step { margin: 0.8em 0 0.3em; padding: 0.3em 0.6em; font-weight: bold; border: 2px solid var(--mw-actor, var(--mw-muted)); border-radius: 6px; background: color-mix(in srgb, currentColor 6%, transparent); } /* the line a step on the map scrolls to, drawn as its box there */ +.mm-map-off { opacity: 0.45; } +.mm-map-call { display: flex; gap: 0.5em; align-items: center; margin: 0.2em 0 0.4em; } +.mm-map-call code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; font-size: 0.9em; padding: 2px 6px; border-radius: 4px; background: var(--mw-line); } +.mm-map-note { font-weight: normal; color: var(--mw-muted); font-size: 0.9em; } +.mm-map-show { margin-left: 0.5em; font-weight: normal; border: 1px solid var(--mw-line); background: var(--mw-bg); color: inherit; border-radius: 4px; padding: 1px 8px; font-size: 0.75rem; cursor: pointer; } +.mm-walk-bar { --mw-line: var(--md-default-fg-color--lightest, #8884); --mw-accent: var(--md-accent-fg-color, #526cfe); + position: fixed; left: 50%; bottom: 1rem; transform: translateX(-50%); z-index: 1000; display: flex; gap: 0.5rem; align-items: center; max-width: calc(100vw - 2rem); + padding: 0.5rem 0.75rem; border: 1px solid var(--mw-accent); border-radius: 8px; background: var(--md-default-bg-color, #fff); color: var(--md-default-fg-color, #000); box-shadow: 0 4px 16px #0004; font-size: 0.85rem; } +.mm-walk-bar.mm-walk-anchored { bottom: auto; transform: none; } +.mm-walk-bar .mm-walk-say { cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.mm-walk-bar button { border: 1px solid var(--mw-line); background: transparent; color: inherit; border-radius: 4px; padding: 2px 8px; font: inherit; cursor: pointer; } +.mm-walk-dialog { width: min(960px, 92vw); height: min(640px, 85vh); min-width: min(420px, 92vw); min-height: min(360px, 96vh); max-width: 98vw; max-height: 96vh; margin: auto; + resize: both; overflow: hidden; padding: 12px 14px; border: 1px solid var(--md-default-fg-color--lightest, #8884); border-radius: 10px; background: var(--md-default-bg-color, #fff); color: var(--md-default-fg-color, #000); } +.mm-walk-dialog[open] { display: flex; flex-direction: column; } +.mm-walk-dialog::backdrop { background: rgba(0,0,0,0.5); } +.mm-walk-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } +.mm-walk-head h3 { margin: 0; font-size: 1rem; } +.mm-walk-body { flex: 1; min-height: 0; } +.mm-walk-dialog .mm-map { height: 100%; grid-template-rows: auto 1fr; } +@media (max-width: 760px) { .mm-map { grid-template-columns: 1fr; grid-template-rows: auto 60vh auto; } .mm-map-split { display: none; } }`; + +function ensureStyles() { + if (document.getElementById("mm-map-styles")) return; + const el = document.createElement("style"); + el.id = "mm-map-styles"; + el.textContent = STYLES; + document.head.appendChild(el); +} + +function readDone() { + try { return new Set(JSON.parse(localStorage.getItem(LS_DONE) || "[]")); } catch { return new Set(); } +} +function writeDone(done) { + try { localStorage.setItem(LS_DONE, JSON.stringify([...done])); } catch { /* a private window keeps marks for the visit only */ } +} + +// A host vouching for processes it saw done, such as the installer after an install: kept as marks on this origin, where the map reads them. +export function markDone(ids) { + const done = readDone(); + for (const id of ids || []) if (typeof id === "string" && /^[a-z0-9-]+$/.test(id)) done.add(id); // a process id, whoever hands it over + writeDone(done); +} + +// The page's address under the site root, the key moonwalk.json's byPage index uses. +function here() { + const path = location.href.split(/[?#]/)[0]; + return path.startsWith(ROOT.href) ? decodeURIComponent(path.slice(ROOT.href.length)) || "index.html" : ""; +} + +function mapButton() { + const header = document.querySelector(".md-header__inner"); + if (!header) return; + let a = header.querySelector(".mm-map-link"); + if (!a) { + a = document.createElement("a"); + a.className = "md-header__button mm-map-link"; + a.title = "This page on MoonWalk, the journey map"; + a.setAttribute("aria-label", a.title); + a.textContent = "\u{1F5FA}️"; + header.insertBefore(a, header.querySelector(".md-search") || null); + } + a.href = new URL(`moonwalk.html?focus=${encodeURIComponent(here())}`, ROOT).href; +} + +function loadScript(src) { + if (window.cytoscape) return Promise.resolve(); + return new Promise((resolve, reject) => { + const s = document.createElement("script"); + s.src = src; + s.onload = resolve; + s.onerror = () => reject(new Error(`could not load ${src}`)); + document.head.appendChild(s); + }); +} + +// The map's columns: the journey's stages, or the architecture's systems, where a process touching several stands in each of their columns. +function columns(model, by) { + if (by !== "system") return model.stages.map((s) => ({ id: s.id, title: s.title, processes: s.processes })); + const cols = model.systems.map((name) => ({ id: name, title: name, processes: [] })); + const none = { id: "none", title: "No system", processes: [] }; + for (const stage of model.stages) + for (const pid of stage.processes) { + const named = model.processes[pid].systems || []; + if (!named.length) none.processes.push(pid); + for (const name of named) cols.find((c) => c.id === name).processes.push(pid); + } + return [...cols, none].filter((c) => c.processes.length); +} + +// The story map's grid: a column each, its processes stacked, a process's steps inside it, one below the other. Every node carries its process id, since in the system view one process has several. +// The map's nodes and their places. `keep(pid, step)` leaves a step out, and the steps below move up, so hiding leaves no hole. +function elements(model, by, keep = () => true) { + const out = []; + columns(model, by).forEach((column, x) => { + out.push({ data: { id: `col:${column.id}`, label: column.title, kind: "stage" } }); + let y = 0; + for (const pid of column.processes) { + const p = model.processes[pid]; + const steps = (p.steps || []).filter((s) => keep(pid, s)); + const nid = `${column.id}::${pid}`; + out.push({ data: { id: nid, pid, parent: `col:${column.id}`, label: p.title, kind: "process", actor: p.actor, stepped: steps.length > 0 }, + position: { x: x * COL, y } }); + steps.forEach((s, k) => { + out.push({ data: { id: `${nid}/${s.id}`, pid, parent: nid, label: s.say, kind: "step", actor: s.actor }, + position: { x: x * COL, y: y + (k + 1) * STEP_H } }); + if (k) out.push({ data: { id: `next:${nid}/${k}`, source: `${nid}/${steps[k - 1].id}`, target: `${nid}/${s.id}`, kind: "next" } }); + }); + y += steps.length ? (steps.length + 1) * STEP_H + GAP : PROC_H; + } + }); + // Dependencies are the journey's order, so only the stage view draws them. + if (by !== "system") + for (const [pid, p] of Object.entries(model.processes)) + for (const dep of p.dependsOn || []) + out.push({ data: { id: `dep:${dep}>${pid}`, source: `${model.processes[dep].stage}::${dep}`, target: `${p.stage}::${pid}`, kind: "dep" } }); + return out; +} + +// A theme color as rgb(), since Material writes hsla() with degrees, which Cytoscape does not parse; the browser resolves it. Probed inside the map, where a host defines the variables. +function themeColor(root, variable, fallback) { + const probe = document.createElement("span"); + probe.style.color = `var(${variable}, ${fallback})`; + root.appendChild(probe); + const rgb = getComputedStyle(probe).color; + probe.remove(); + return rgb; +} + +function style(root) { + const fg = themeColor(root, "--md-default-fg-color", "#222"); + const bg = themeColor(root, "--md-default-bg-color", "#fff"); + const accent = themeColor(root, "--md-accent-fg-color", "#526cfe"); + return [ + { selector: "node", style: { label: "data(label)", color: fg, "font-size": 12, "text-wrap": "wrap", "text-valign": "center", "text-halign": "center", "min-zoomed-font-size": 7 } }, + { selector: "node[kind='stage']", style: { shape: "round-rectangle", "background-color": fg, "background-opacity": 0.04, "border-width": 1, "border-color": fg, "border-opacity": 0.2, "text-valign": "top", "font-size": 22, "font-weight": "bold", padding: 24, "min-zoomed-font-size": 4 } }, + { selector: "node[kind='process']", style: { shape: "round-rectangle", width: 280, height: 44, "text-max-width": 260, "background-color": bg, "border-width": 3, "border-color": (n) => ACTOR_COLOR[n.data("actor")] || fg } }, + { selector: "node[kind='process'][?stepped]", style: { "text-valign": "top", "font-weight": "bold", padding: 10 } }, + { selector: "node[kind='step']", style: { shape: "round-rectangle", width: 260, height: 28, "text-max-width": 250, "font-size": 10, "background-color": fg, "background-opacity": 0.06, "border-width": 2, "border-color": (n) => ACTOR_COLOR[n.data("actor")] || fg, "min-zoomed-font-size": 9 } }, + { selector: "edge", style: { width: 1.5, "line-color": fg, "line-opacity": 0.35, "curve-style": "bezier", "target-arrow-shape": "triangle", "target-arrow-color": fg, "arrow-scale": 0.8 } }, + { selector: "edge[kind='next']", style: { width: 1, "line-style": "dotted", "target-arrow-shape": "none" } }, + { selector: "node.mm-off, node.mm-skip", style: { opacity: 0.25 } }, + { selector: "node.mm-here", style: { "border-color": accent, "border-width": 3, "background-color": accent, "background-opacity": 0.25 } }, + { selector: ".done", style: { opacity: 0.4 } }, + { selector: ".faded", style: { opacity: 0.08 } }, + { selector: "node:selected", style: { "border-color": accent, "border-width": 5 } }, + ]; +} + +// A step's expectation against a device's state: a control's value or a module that exists. A step names a module by type, so it holds when any module of that type meets it. An HTTP status or a file the state cannot answer, so those give undefined. +function holds(exp, ofType) { + const mods = ofType(exp.module) || []; + if ("exists" in exp) return (mods.length > 0) === exp.exists; + if (!("control" in exp)) return undefined; + return mods.some((m) => { + const c = m.controls?.find((c) => c.name === exp.control); + if (!c) return false; + // A list counts as its number of rows, so "a network is known" is `notEquals: 0`; equals and notEquals compare as text, since a document's number and the file's number or string mean the same value. + const value = Array.isArray(c.value) ? c.value.length : c.value; + if ("equals" in exp) return String(value) === String(exp.equals); + if ("notEquals" in exp) return String(value) !== String(exp.notEquals); + // A range compares as numbers, each bound inclusive and either one open. + const n = Number(value); + return !Number.isNaN(n) && !(n < exp.minimum) && !(n > exp.maximum); + }); +} + +// A device's modules by type, the key a step names them by. +function typesOf(stateDoc) { + const byType = new Map(); + const walk = (mods) => { for (const m of mods || []) { if (!byType.has(m.type)) byType.set(m.type, []); byType.get(m.type).push(m); walk(m.children); } }; + walk(stateDoc?.modules); + return byType; +} + +// Whether the state shows a step done: every expectation it can answer holds, and there is at least one; undefined when it can answer none, which only the reader can mark. +export function stepHolds(step, stateDoc, byType = typesOf(stateDoc)) { + const answers = (step.then || []).map((e) => holds(e, (t) => byType.get(t))).filter((v) => v !== undefined); + return answers.length ? answers.every(Boolean) : undefined; +} + +// Where a step points in the app: the module type its action names, and the control it sets. +export function targetOf(step) { + const arg = step.when && Object.values(step.when)[0]; + return arg && typeof arg === "object" && arg.module ? { type: arg.module, control: arg.control } : null; +} + +// The step a followed process stands at: from `i`, past every step the state shows done. +export function advance(steps, i, stateDoc) { + const byType = typesOf(stateDoc); // once per state change, which comes every second + while (i < steps.length && stepHolds(steps[i], stateDoc, byType) === true) i++; + return i; +} + +// The processes a device's state shows done: every expectation the state can answer holds, and there is at least one. Observed rather than recorded, so a step done by hand, by the installer or through MoonWalk counts the same. +export function doneOnDevice(model, stateDoc) { + const byType = typesOf(stateDoc); + const done = new Set(); + for (const [pid, p] of Object.entries(model.processes)) { + const answers = (p.steps || []).flatMap((s) => s.then || []).map((e) => holds(e, (t) => byType.get(t))).filter((v) => v !== undefined); + if (answers.length && answers.every(Boolean)) done.add(pid); + } + return done; +} + +// The processes a contributor's runs show done: every step the change triggers has a pass that still holds, and there is at least one. `steps` is what MoonDeck's /api/gates reports per process, each step triggered or not and fresh or not. +export function doneBySteps(steps) { + const done = new Set(); + for (const [pid, list] of Object.entries(steps || {})) { + const needed = list.filter((s) => s.triggered); + if (needed.length && needed.every((s) => s.fresh)) done.add(pid); + } + return done; +} + +// A run's duration as a reader takes it in: `42s`, `3m05s`. +export const took = (seconds) => { + if (seconds == null) return ""; + const s = Math.round(seconds); // rounded first, so 119.6 reads 2m00s rather than 1m60s + return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`; +}; + +// A process followed in the host while the user performs it, the onboarding checklist's construct: one bar holding the current step, which the host's state moves on and a tap turns back into the map. The bar floats under the step's target as a coach mark does, above it when there is no room below, and at the bottom of the screen when the step has none in view. The host passes `show(type, control, reveal)`, which returns the target's element and with `reveal` brings it into view first, and `reopen(at)`, which opens the map at `process/step`. Returns `update(stateDoc)`, which the host calls on each state change, and `stop()`. +export function follow(model, pid, stepId, { show, reopen, choices }) { + ensureStyles(); + const p = model.processes[pid]; + const steps = (p?.steps || []).filter((s) => applies(s, chosenOf(p, choices))); // the reader's choices as the panel shows them, so the bar counts the steps it numbered + let i = Math.max(0, steps.findIndex((s) => s.id === stepId)); + const bar = control("div", { className: "mm-walk-bar", role: "status" }); + const say = control("span", { className: "mm-walk-say", title: "Open MoonWalk at this step" }); + const next = control("button", { type: "button" }); + const close = control("button", { type: "button", title: "Stop following" }, "\u2715"); + bar.append(say, next, close); + document.body.append(bar); + let stopped = false, anchor = null, frame = 0, shownAt = 0; + // Under the target, else above it, kept on screen; a target out of view, or none, leaves the bar at the bottom. + const place = () => { + frame = 0; + const t = steps[i] && targetOf(steps[i]); + // The host redrew its cards: find the target again, and show it again while it is new, since opening a page redraws it once more when that page's state arrives. + if (t && !anchor?.isConnected) anchor = show(t.type, t.control, Date.now() - shownAt < 3000) || null; + const r = anchor?.getBoundingClientRect(); + const inView = r && r.bottom > 0 && r.top < innerHeight; + bar.classList.toggle("mm-walk-anchored", Boolean(inView)); + if (!inView) { bar.style.top = bar.style.left = ""; return; } + const h = bar.offsetHeight, w = bar.offsetWidth; + bar.style.top = `${r.bottom + 8 + h <= innerHeight ? r.bottom + 8 : Math.max(8, r.top - 8 - h)}px`; + bar.style.left = `${Math.min(Math.max(8, r.left), innerWidth - w - 8)}px`; + }; + const later = () => { if (!frame) frame = requestAnimationFrame(place); }; + addEventListener("scroll", later, { capture: true, passive: true }); + addEventListener("resize", later); + const stop = () => { stopped = true; bar.remove(); removeEventListener("scroll", later, { capture: true }); removeEventListener("resize", later); }; + const render = () => { + if (i >= steps.length) { say.textContent = `\u2714 ${p.title}: done`; next.hidden = true; return; } + const s = steps[i]; + say.textContent = `Step ${i + 1} of ${steps.length}: ${s.say}`; + // A step the state can answer moves on by itself, and Skip passes it; one it cannot is the reader's to mark Done. + next.hidden = false; + next.textContent = (s.then || []).some((e) => "module" in e) ? "Skip" : "Done"; + }; + const go = (n) => { + i = n; + render(); + const t = steps[i] && targetOf(steps[i]); + anchor = t ? show(t.type, t.control, true) || null : null; + shownAt = Date.now(); + later(); + }; + say.addEventListener("click", () => { stop(); reopen(`${pid}/${steps[Math.min(i, steps.length - 1)]?.id || ""}`); }); + next.addEventListener("click", () => go(i + 1)); + close.addEventListener("click", stop); + go(i); + return { + update(stateDoc) { + if (stopped || i >= steps.length) return; // a finished process has nothing left to watch for + const n = advance(steps, i, stateDoc); + if (n !== i) go(n); + else later(); // a redraw can move the target + }, + stop, + }; +} + +// What comes next: the processes not done whose dependencies all are, in the journey's order, the frontier of the dependency graph. +export function nextOf(model, done, actor) { + return model.stages.flatMap((s) => s.processes).filter((pid) => { + const p = model.processes[pid]; + return !done.has(pid) && (!actor || p.actor === actor) && (p.dependsOn || []).every((d) => done.has(d)); + }); +} + +function control(tag, attrs, text) { + const el = document.createElement(tag); + Object.assign(el, attrs); + if (text) el.textContent = text; + return el; +} + +// Where a step takes the reader, a page or an address, as a link in the text under its instruction; another site opens beside the map. An address reads as itself, a page as "Read the page". +function linkOf(s, host) { + if (!s.url || /^[a-z][\w+.-]*:/i.test(s.url) && !/^https?:/.test(s.url)) return null; // a page of the site or a web address, never another scheme + const web = /^https?:/.test(s.url); + const p = control("p"); + p.append(control("a", { href: new URL(s.url, web ? ROOT : host.site || ROOT).href, target: host.newTab || web ? "_blank" : "", rel: "noopener" }, web ? s.url.replace(/^https?:\/\//, "") : "Read the page")); + return p; +} + +// What the panel adds after a step: who does it when that is not the process's actor, and what it waits on. +function stepNote(s, p) { + const notes = []; + if (s.actor && s.actor !== p.actor) notes.push(s.actor); + for (const g of s.given || []) { + if (g.input) notes.push(`${g.input}: ${g.in ? g.in.join(", ") : g.equals}`); // one option, or the several a step serves + else if (g.changed) notes.push(`changes ${g.changed.join(", ")}`); + } + return notes.length ? ` (${notes.join("; ")})` : ""; +} + +// A step's call as a reader types it: a MoonDeck script with its arguments, or a command; null for a step that runs nothing. +export function commandOf(step) { + const r = step.when?.run; + if (!r) return null; + return r.script ? `uv run ${r.script}${r.args ? ` ${r.args}` : ""}` : r.command || null; +} + +// The call as code with a button that copies it. Where the browser keeps the clipboard from an insecure page, as on a device's own http address, the button selects the text for a manual copy. +function callLine(cmd) { + const row = control("div", { className: "mm-map-call" }); + const code = control("code", {}, cmd); + const copy = control("button", { type: "button", className: "mm-map-show", title: "Copy the command" }, "Copy"); + copy.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(cmd); + copy.textContent = "Copied"; + setTimeout(() => { copy.textContent = "Copy"; }, 1500); + } catch { + const range = document.createRange(); + range.selectNodeContents(code); + getSelection().removeAllRanges(); + getSelection().addRange(range); + } + }); + row.append(code, copy); + return row; +} + +// What to do for a step: its call to copy, or the manual step's own words. +function howTo(s) { + const cmd = commandOf(s); + if (cmd) return callLine(cmd); + return s.when?.manual ? control("p", { className: "mm-map-note" }, s.when.manual) : null; +} + +// Whether a process carries its own prose, so the view shows it whole rather than linking a page: in the map already, or marked `text` and fetched when the view opens. +const hasProse = (p) => Boolean(p.text || p.intro || p.outro || (p.steps || []).some((s) => s.detail)); + +// A process's prose, its intro, outro and step details, fetched once when its view first opens and merged into the map's process, so the map a page loads stays small. +// The fetch is shared by every map drawn on the page, and each merges it into its own model, since a map opened again draws from a model fetched again. +const texts = new Map(); +function withText(pid, p) { + if (!texts.has(pid)) { + texts.set(pid, fetch(new URL(`moonwalk/text/${pid}.json`, ROOT)).then((r) => { + if (!r.ok) throw new Error(`answered ${r.status}`); + return r.json(); + }).catch((e) => { texts.delete(pid); throw e; })); // a failed fetch is tried again on the next open + } + return texts.get(pid).then((t) => { + Object.assign(p, { intro: t.intro, outro: t.outro }); + for (const s of p.steps || []) if (t.details?.[s.id]) s.detail = t.details[s.id]; + delete p.text; + }); +} + +// The process view: a process with its prose reads whole in the panel, widened for reading, its intro, its steps in their numbered sections, each with its detail, and its outro; one without prose shows its steps and links its page. +function showPanel(panel, model, pid, done, onDone, host = {}) { + const p = model.processes[pid]; + if (p?.text) { + // Drawn once its prose has arrived, unless the reader opened another process meanwhile. + panel.dataset.opening = pid; + return withText(pid, p).then(() => { if (panel.dataset.opening === pid) showPanel(panel, model, pid, done, onDone, host); }, + (e) => { if (panel.dataset.opening === pid) panel.replaceChildren(control("p", { className: "mm-map-note" }, `${p.title}: its text could not load (${e.message}).`)); }); + } + panel.dataset.opening = pid; + panel.replaceChildren(); + host.root?.classList.toggle("mm-map-reading", Boolean(p && hasProse(p))); + if (!p) return; + if (host.back) { + const back = control("p", { className: "mm-map-next" }, "\u2190 All choices"); + back.addEventListener("click", host.back); + panel.append(back); + } + const meta = [model.stages.find((s) => s.id === p.stage)?.title, p.actor, ...(p.systems || [])].join(" · "); + panel.append(control("h3", {}, p.title), control("p", { className: "mm-map-meta" }, meta), control("p", {}, p.goal)); + if (hasProse(p)) { + const view = control("div", { className: "mm-map-view" }); + const md = (text) => { const d = control("div"); d.innerHTML = host.helpMarkdown(text); view.append(d); }; + if (p.intro) md(p.intro); // the intro first, which says what the choice below is for + // A choice the process offers, such as the platform, as a row of options above the steps: only the chosen option's steps show. + const chosen = chosenOf(p, host.choices); + for (const [name, spec] of Object.entries(p.inputs?.properties || {})) { + const options = spec.enum || []; + const row = control("div", { className: "mm-map-chips" }); + row.append(control("span", { className: "mm-map-meta" }, (spec.description || name) + " ")); + for (const o of options) { + const b = control("button", { type: "button", className: "mm-map-chip" + (o === chosen[name] ? " on" : "") }, o); + b.addEventListener("click", () => { keepInput(name, o); if (host.hideOff) host.relayout?.(); showPanel(panel, model, pid, done, onDone, host); }); + row.append(b); + } + view.append(row); + } + host.offChoice?.(p, chosen); // the map grays out the steps of the options not chosen + // A step that does not apply, for another choice or not needed for this change, is grayed as on the map, or left out when the reader hides them. + const steps = p.steps || []; + const runOf = (s) => host.steps?.[pid]?.find((r) => r.id === s.id); + const off = steps.map((s) => !applies(s, chosen) || runOf(s)?.triggered === false); + let current = null; + const sectionOf = steps.map((s) => (current = s.section || current)); + const live = new Set(sectionOf.filter((sec, i) => sec && !off[i])); + let chapter = 0, section = null; + steps.forEach((s, i) => { + if (off[i] && host.hideOff) return; + // A heading once per section, numbered by the sections with a step that applies; one whose steps are all grayed is grayed with them. + if (sectionOf[i] && sectionOf[i] !== section) { + section = sectionOf[i]; + view.append(live.has(section) ? control("h4", {}, `${++chapter}. ${section}`) : control("h4", { className: "mm-map-off" }, section)); + } + // A step and its detail are one block, the part of the text that belongs to the step on the map. + const block = control("div"); + block.dataset.step = s.id; + if (off[i]) block.classList.add("mm-map-off"); + // The instruction in its box from the map, bordered in its actor's color, what it waits on as a quiet aside after it. + const line = control("p", { className: "mm-map-step" }); + line.append(s.say); + if (ACTOR_COLOR[s.actor]) line.style.setProperty("--mw-actor", ACTOR_COLOR[s.actor]); + if (stepNote(s, p)) line.append(control("span", { className: "mm-map-note" }, stepNote(s, p))); + // In a host that can show a card, a step with a target follows from here: the map gives way to the app and the bar. + // A step the device already shows done is ticked, as a checklist ticks a finished item, and has nothing to show. + const run = runOf(s); + if (host.types && stepHolds(s, host.state, host.types) === true) line.append(control("span", { className: "mm-map-meta" }, " \u2714 done")); + else if (run) { + // A script step: not needed for this change, passed and still holding, or to run, with how long it took last time. + if (!run.triggered) line.append(control("span", { className: "mm-map-note" }, " not needed for this change")); + else if (run.fresh) line.append(control("span", { className: "mm-map-meta" }, " \u2714 passed")); + else if (run.running) line.append(control("span", { className: "mm-map-meta" }, " running")); + else if (host.run && run.card) { + const b = control("button", { type: "button", className: "mm-map-show", title: run.seconds == null ? "Not measured yet" : `Up to ${took(run.seconds)} in its recent runs${run.slow ? ", slow: running it is a choice" : ""}` }, run.seconds == null ? "Run" : `Run \u00b7 ${run.slow ? "\u{1F40C} " : ""}${took(run.seconds)}`); + b.addEventListener("click", () => host.run(run)); + line.append(b); + } + } else if (host.follow && targetOf(s)) { + const b = control("button", { type: "button", className: "mm-map-show" }, "Show me"); + b.addEventListener("click", () => host.follow(pid, s.id)); + line.append(b); + } + block.append(line); + const how = howTo(s) || linkOf(s, host); + if (how) block.append(how); + if (s.detail) { const d = control("div"); d.innerHTML = host.helpMarkdown(s.detail); block.append(d); } + view.append(block); + }); + if (p.outro) md(p.outro); + panel.append(view); + } else { + if (p.url) { + const line = control("p"); + line.append(control("a", { href: new URL(p.url, host.site || ROOT).href, target: host.newTab ? "_blank" : "", rel: "noopener" }, "Read the page")); // a host serving no pages names the site, as linkOf does + panel.append(line); + } + if (p.steps?.length) { + const ol = control("ol"); + for (const s of p.steps) { + const li = control("li"); + li.append(s.say, stepNote(s, p)); + li.dataset.step = s.id; + const how = howTo(s) || linkOf(s, host); + if (how) li.append(how); + ol.append(li); + } + panel.append(ol); + } + } + host.current = pid; + host.spy?.(panel); + // Done on the device is what the state shows, so it has no checkbox to undo; what the state cannot show is the reader's own mark. + if (host.device?.has(pid)) { panel.append(control("p", { className: "mm-map-done" }, `\u2714 ${host.doneBy}`)); return; } + const label = control("label", { className: "mm-map-done" }); + const box = control("input", { type: "checkbox", checked: done.has(pid) }); + box.addEventListener("change", () => onDone(pid, box.checked)); + label.append(box, " Done"); + panel.append(label); +} + +// What the reader can do from where they are: the processes here (touching the card in view, or the page they came from) and the next ones to do, neither showing what is done. Returns them, so the map can fit to the same set. +export function choicesOf(model, done, actor, here) { + const open = (pid) => !done.has(pid) && (!actor || model.processes[pid].actor === actor); + const hereOpen = here.filter(open).slice(0, 6); + const next = nextOf(model, done, actor).filter((pid) => !hereOpen.includes(pid)).slice(0, 5); + return { here: hereOpen, next }; +} + +// The panel before a process is picked: the choices, each a link to its place on the map. +function showChoices(panel, model, choices, onPick) { + panel.replaceChildren(); + const list = (title, pids, empty) => { + panel.append(control("h3", {}, title)); + if (!pids.length) { panel.append(control("p", { className: "mm-map-meta" }, empty)); return; } + const ol = control("ol"); + for (const pid of pids) { + const li = control("li", { className: "mm-map-next" }, model.processes[pid].title); + li.addEventListener("click", () => onPick(pid)); + ol.append(li); + } + panel.append(ol); + }; + if (choices.hereAsked) list("Here", choices.here, "Everything here is done."); + list("Next", choices.next, "Nothing left on this path."); + panel.append(control("p", { className: "mm-map-meta" }, "Pick one, or click a process on the map, for its goal, its steps and its page.")); +} + +// The handle between the map and the text: dragged, or moved by the arrow keys, it sets the text's share, kept for this reader. A share rather than pixels, so it holds when the window or the dialog is resized. +function dragSplit(host, split, panel) { + const share = (px) => { + const w = host.getBoundingClientRect().width; + const value = `${Math.round(Math.min(Math.max(px, 160), w - 160) / w * 100)}%`; + host.style.setProperty("--mw-panel", value); + try { localStorage.setItem(LS_SPLIT, value); } catch { /* kept for this visit only */ } + }; + try { const kept = localStorage.getItem(LS_SPLIT); if (kept) host.style.setProperty("--mw-panel", kept); } catch { /* the default share */ } + split.addEventListener("pointerdown", (down) => { + split.setPointerCapture(down.pointerId); + const move = (ev) => share(host.getBoundingClientRect().right - ev.clientX); + split.addEventListener("pointermove", move); + split.addEventListener("pointerup", () => split.removeEventListener("pointermove", move), { once: true }); + }); + split.addEventListener("keydown", (ev) => { + if (ev.key !== "ArrowLeft" && ev.key !== "ArrowRight") return; + ev.preventDefault(); + const step = host.getBoundingClientRect().width * 0.05; + share(panel.getBoundingClientRect().width + (ev.key === "ArrowLeft" ? step : -step)); + }); +} + +// The map in a host's own dialog, resized from its corner, for the hosts that are not the docs page. `draw(body, close)` draws into the dialog's body, usually through drawMap, and returns the map's teardown, which runs when the dialog closes; `closeClass` styles the Close button as the host's own. Returns the teardown, or nothing when the map did not draw. +export async function openMap(draw, { closeClass = "" } = {}) { + ensureStyles(); + const dlg = control("dialog", { className: "mm-walk-dialog" }); + const head = control("div", { className: "mm-walk-head" }); + const close = control("button", { type: "button", className: closeClass }, "Close"); + close.addEventListener("click", () => dlg.close()); + head.append(control("h3", {}, "MoonWalk"), close); + const body = control("div", { className: "mm-walk-body" }); + dlg.append(head, body); + let stop = null; + dlg.addEventListener("close", () => { stop?.(); dlg.remove(); }, { once: true }); + document.body.append(dlg); + dlg.showModal(); + try { + stop = await draw(body, () => dlg.close()); + if (!dlg.open) stop?.(); // closed while it was still loading + } catch (e) { + body.textContent = `MoonWalk could not load: ${e.message}`; // offline, as on a device's own access point + } + return stop; +} + +// Draw the map into `host`, and return its teardown. A host passes what it knows: `site` (where the docs pages live, for a host that does not serve them), `model` (moonwalk.json when it holds it already), `ownsAddress` (the docs page, whose address is the open process), `state` (a device's /api/state document), `steps` (MoonDeck's /api/gates per process: each script step triggered, fresh, its seconds and its card), `run` (MoonDeck's: runs one such step), `modules` (the types of the cards in view), `here` (the processes or stages it is about, by id), `open` (a process or `process/step` to open at), `show`, `reopen` and `onFollow` (a host that can show a card: Show me hands it the followed process, see follow), `choices` (a choice's option the host knows, by input name), `done` (processes it vouches for, kept as marks), `actor` (the reader it starts filtered to) and `newTab` (pages open beside the host rather than over it). +export async function drawMap(host, opts = {}) { + if (host.dataset.mounted) return; + host.dataset.mounted = "1"; + host.classList.add("mm-map"); + ensureStyles(); + const bar = control("div", { className: "mm-map-bar" }); + const search = control("input", { type: "search", placeholder: "Search processes, steps, modules" }); + const actor = control("select"); + actor.append(control("option", { value: "" }, "Every actor"), ...ACTORS.map((a) => control("option", { value: a }, a))); + // The reader's own view comes back as they left it; the host's actor is the first visit's. + let view = {}; + try { view = JSON.parse(localStorage.getItem(LS_VIEW) || "{}") || {}; } catch { /* the defaults */ } + const keepView = () => { try { localStorage.setItem(LS_VIEW, JSON.stringify({ actor: actor.value, by: by.value, hide: hide.checked, hideOff: hideOff.checked })); } catch { /* kept for this visit only */ } }; + actor.value = "actor" in view && ["", ...ACTORS].includes(view.actor) ? view.actor : opts.actor || ""; + const hide = control("input", { type: "checkbox" }); + hide.checked = Boolean(view.hide); + // Steps for another choice, or not needed for this change: grayed in the text as on the map, or left out of the text. + const hideOff = control("input", { type: "checkbox" }); + hideOff.checked = Boolean(view.hideOff); + const hideOffLabel = control("label", { title: "Steps for another choice, or not needed for this change" }); + hideOffLabel.append(hideOff, " Hide steps that don't apply"); + const hideLabel = control("label"); + hideLabel.append(hide, " Hide done"); + const by = control("select", { title: "Columns" }); + by.append(control("option", { value: "stage" }, "Columns: stage"), control("option", { value: "system" }, "Columns: system")); + if (view.by === "system") by.value = "system"; + for (const c of [actor, by, hide, hideOff]) c.addEventListener("change", keepView); + const all = control("button", { type: "button", title: "Zoom to the processes the filters show" }, "Fit selection"); + const chips = control("div", { className: "mm-map-chips" }); + bar.append(search, actor, by, hideLabel, hideOffLabel, all, chips); + const canvas = control("div", { className: "mm-map-canvas" }); + const panel = control("aside", { className: "mm-map-panel" }); + const split = control("div", { className: "mm-map-split", tabIndex: 0, title: "Drag to share the space between the map and the text" }); + split.setAttribute("role", "separator"); + split.setAttribute("aria-orientation", "vertical"); + host.append(bar, canvas, split, panel); + dragSplit(host, split, panel); + + let model; + try { + await loadScript(CYTOSCAPE); + if (opts.model) model = structuredClone(opts.model); // the host's copy, which the map's prose is merged into + else { + const r = await fetch(MODEL); + if (!r.ok) throw new Error(`${MODEL.href} answered ${r.status}`); + model = await r.json(); + } + } catch (e) { + canvas.textContent = `The map could not load: ${e.message}`; + return; + } + // The help's renderer, so a process reads as a module's help does; imported here rather than at the top, so a test reads this module's rules without a served site. + const { helpMarkdown } = await import("./help.js"); + // Done is the device's state and the reader's marks together; the device's part is not the reader's to unmark. + // What the host can see done: a device's state, or a contributor's passing runs in MoonDeck. + const device = new Set([...(opts.state ? doneOnDevice(model, opts.state) : []), ...doneBySteps(opts.steps)]); + if (opts.done?.length) markDone(opts.done); + const marks = readDone(); + const done = new Set([...marks, ...device]); + const host_ = { device, newTab: opts.newTab, site: opts.site, root: host, helpMarkdown, choices: opts.choices, doneBy: opts.state ? "Done on this device" : "Done: its scripts last passed" }; + host_.state = opts.state; + host_.hideOff = hideOff.checked; + hideOff.addEventListener("change", () => { host_.hideOff = hideOff.checked; host_.relayout?.(); if (host_.current) showPanel(panel, model, host_.current, done, mark, host_); }); + host_.steps = opts.steps; + if (opts.run) host_.run = opts.run; + host_.types = opts.state ? typesOf(opts.state) : null; // indexed once, for every step the panel ticks + if (opts.show) host_.follow = (pid, stepId) => opts.onFollow?.(follow(model, pid, stepId, opts)); + // On the site the open process is the page's address, so a link, a search result or a bookmark opens it; every other host's address is its own. + const remember = (pid) => { if (opts.ownsAddress) history.replaceState(null, "", pid ? `#${pid}` : location.pathname + location.search); }; + const picked = new Set(); // the system chips switched on; none means every system + // A chip for each system some process touches, so none filters to an empty map. + const used = new Set(Object.values(model.processes).flatMap((p) => p.systems || [])); + for (const name of model.systems.filter((n) => used.has(n))) { + const chip = control("button", { type: "button", className: "mm-map-chip" }, name); + chip.addEventListener("click", () => { + picked.has(name) ? picked.delete(name) : picked.add(name); + chip.classList.toggle("on", picked.has(name)); + fit(picked.size ? apply() : (apply(), cy.collection())); + }); + chips.append(chip); + } + // A step that does not apply, for the reader's choice or not needed for this change, leaves the layout when the reader hides them. + const keepStep = (pid, s) => !hideOff.checked || (applies(s, chosenOf(model.processes[pid], opts.choices)) && opts.steps?.[pid]?.find((r) => r.id === s.id)?.triggered !== false); + const cy = window.cytoscape({ container: canvas, elements: elements(model, by.value, keepStep), style: style(host), layout: { name: "preset" }, minZoom: 0.05, maxZoom: 3, autoungrabify: true }); // the layout is the model's order, so a block taps but never drags + const nodesOf = (pid) => cy.nodes().filter((n) => n.data("pid") === pid); + // A drag that starts on a block pans the map, as one on the background does; a tap without moving still opens the block. + let dragFrom = null, dragged = 0; + cy.on("tapstart", "node", (ev) => { dragFrom = ev.renderedPosition; dragged = 0; }); + cy.on("tapdrag", (ev) => { + if (!dragFrom) return; + const at = ev.renderedPosition; + dragged += Math.abs(at.x - dragFrom.x) + Math.abs(at.y - dragFrom.y); + cy.panBy({ x: at.x - dragFrom.x, y: at.y - dragFrom.y }); + dragFrom = at; + }); + const tapped = () => dragged < 6; // a pan that ends on a block is no tap on it + cy.on("tapend", () => { dragFrom = null; }); + // The canvas follows its host, a resized dialog in the app or a narrower window on the site. + const resize = new ResizeObserver(() => cy.resize()); + resize.observe(canvas); + + function apply() { + const q = search.value.trim().toLowerCase(); + const visible = cy.collection(); + cy.batch(() => { + for (const [pid, p] of Object.entries(model.processes)) { + const nodes = nodesOf(pid); + const text = [p.title, p.goal, ...(p.systems || []), ...(p.steps || []).map((s) => `${s.say} ${JSON.stringify(s.when)}`)].join(" ").toLowerCase(); + const show = (!actor.value || p.actor === actor.value) && !(hide.checked && done.has(pid)) && (!q || text.includes(q)) + && (!picked.size || (p.systems || []).some((name) => picked.has(name))); + nodes.toggleClass("faded", !show).toggleClass("done", done.has(pid)); + if (show) visible.merge(nodes.filter((n) => n.data("kind") === "process")); + } + }); + // A script step the change does not need grays out, and one whose pass still holds is done, over its process's own mark. + cy.nodes("[kind='step']").forEach((n) => { + const r = opts.steps?.[n.data("pid")]?.find((x) => n.id().endsWith(`/${x.id}`)); + if (r) n.toggleClass("mm-skip", !r.triggered).toggleClass("done", r.fresh); + }); + return visible; + } + // Fit to a set, but never closer than 1.2, and keep the set centered when the cap applies. + const fit = (eles) => { + const target = eles.length ? eles : cy.elements(); + cy.fit(target, 40); + if (cy.zoom() > 1.2) { cy.zoom(1.2); cy.center(target); } + }; + + search.addEventListener("input", () => fit(search.value.trim() ? apply() : (apply(), cy.collection()))); + hide.addEventListener("change", () => apply()); + all.addEventListener("click", () => fit(apply())); // what the actor, the chips, the search and hide done leave; nothing left fits the whole map + // The layout again, for the columns, the reader's choices or the steps they hide. + const relayout = () => { + cy.elements().remove(); + cy.add(elements(model, by.value, keepStep)); + const shown = apply(); + // The open process stays in view; otherwise what the filters leave, or the whole map. + const open = host_.current ? nodesOf(host_.current).filter((n) => n.data("kind") === "process") : cy.collection(); + if (open.length) { open.select(); fit(open.union(open.children())); } else fit(picked.size || search.value.trim() ? shown : cy.collection()); + }; + host_.relayout = relayout; + by.addEventListener("change", relayout); + // The step being read: the first step block in the text's view lights its node on the map, and the map follows it when it is off screen. + const reading = (pid, stepId) => { + cy.nodes(".mm-here").removeClass("mm-here"); + const node = nodesOf(pid).filter((n) => n.data("kind") === "step" && n.id().endsWith(`/${stepId}`)); + node.addClass("mm-here"); + const box = node.boundingBox(), view = cy.extent(); + if (node.length && (box.y1 < view.y1 || box.y2 > view.y2 || box.x1 < view.x1 || box.x2 > view.x2)) cy.animate({ center: { eles: node } }, { duration: 200 }); + }; + host_.spy = (root) => { + host_.observer?.disconnect(); + const blocks = [...root.querySelectorAll("[data-step]")]; + if (!blocks.length) return; + const seen = new Set(); + host_.observer = new IntersectionObserver((entries) => { + for (const e of entries) e.isIntersecting ? seen.add(e.target) : seen.delete(e.target); + const first = blocks.find((b) => seen.has(b)); + if (first) reading(host_.current, first.dataset.step); + }, { root, rootMargin: "0px 0px -60% 0px" }); + blocks.forEach((b) => host_.observer.observe(b)); + }; + // A step for an option the reader did not choose grays out on the map, as it is left out of the text. + host_.offChoice = (p, chosen) => nodesOf(p.id).forEach((n) => { + const s = n.data("kind") === "step" && (p.steps || []).find((st) => n.id().endsWith(`/${st.id}`)); + n.toggleClass("mm-off", Boolean(s) && !applies(s, chosen)); + }); + // A step tapped on the map opens its process and scrolls the text to the step. + const toStep = async (pid, stepId) => { + if (host_.current !== pid) await showPanel(panel, model, pid, done, mark, host_); // a process's text may still be on its way + // The text alone scrolls, since scrollIntoView would move the page around the map as well. + const block = panel.querySelector(`[data-step="${CSS.escape(stepId)}"]`); + if (block) panel.scrollTo({ top: block.getBoundingClientRect().top - panel.getBoundingClientRect().top + panel.scrollTop, behavior: "smooth" }); + }; + const mark = (id, on) => { + on ? marks.add(id) : marks.delete(id); + writeDone(marks); + on || device.has(id) ? done.add(id) : done.delete(id); + apply(); + }; + const open = (pid) => { + remember(pid); + cy.elements().unselect(); + const target = nodesOf(pid).filter((n) => n.data("kind") === "process"); + target.select(); + showPanel(panel, model, pid, done, mark, host_); + fit(target.union(target.children())); + }; + cy.on("tap", "node[kind='process']", (ev) => { if (!tapped()) return; remember(ev.target.data("pid")); showPanel(panel, model, ev.target.data("pid"), done, mark, host_); }); + cy.on("tap", "node[kind='step']", (ev) => { if (!tapped()) return; const pid = ev.target.data("pid"); remember(pid); toStep(pid, ev.target.id().split("/").pop()); }); + + // Where the reader is: the processes touching the cards in view, or the page they came from. A link naming one process opens it; anything else opens on the choices. + const focus = opts.ownsAddress ? new URLSearchParams(location.search).get("focus") || "" : ""; + const touching = (opts.modules || []).flatMap((m) => (model.byModule[m] || []).map((ref) => ref.split("/")[0])); + // What the host names comes first: a process by id, or a stage standing for its processes. + const named = (opts.here || []).flatMap((id) => model.processes[id] ? [id] : model.stages.find((s) => s.id === id)?.processes || []); + const here = [...new Set(named.length ? named : touching.length ? touching : model.byPage[focus] || [])]; + const choose = () => { + remember(null); + host_.observer?.disconnect(); + host_.current = null; + cy.nodes(".mm-here").removeClass("mm-here"); + host.classList.remove("mm-map-reading"); + const choices = { ...choicesOf(model, done, actor.value, here), hereAsked: here.length > 0 }; + showChoices(panel, model, choices, open); + const shown = [...choices.here, ...choices.next]; + cy.elements().unselect(); + const target = cy.collection(shown.map((id) => nodesOf(id))).filter((n) => n.data("kind") === "process"); + target.select(); + // Where the host or the page names processes, the map zooms to them; otherwise it shows the whole journey, as Fit selection does. + fit(here.length && target.length ? target : apply()); + }; + host_.back = choose; + // A new actor refilters the map; the choices follow unless a process is open for reading. + actor.addEventListener("change", () => { apply(); if (!host_.current) choose(); }); + apply(); + // What the host or the address names, a process or one of its steps, opens; anything else opens on the choices. + // A redirect from a page MoonWalk took over keeps that page's anchor after the process's, so only the first part counts. + const [asked, step] = (opts.open || (opts.ownsAddress ? decodeURIComponent(location.hash.slice(1)).split("#")[0] : "") || focus).split("/"); + if (model.processes[asked]) { open(asked); if (step) toStep(asked, step); } + else choose(); + // What a host calls when it closes the map: Cytoscape's window listeners and both observers go with it. + return () => { cy.destroy(); resize.disconnect(); host_.observer?.disconnect(); }; +} + +function mount() { + mapButton(); + const host = document.getElementById("mm-map"); + if (!host) return; + document.documentElement.classList.add("mm-walk-js"); // the page's plain text gives way to the live view + // The page owns its address, so the open process is its hash. A map that cannot draw returns no teardown, and the plain text comes back. + const plain = () => document.documentElement.classList.remove("mm-walk-js"); + drawMap(host, { ownsAddress: true }).then((stop) => { if (!stop) plain(); }, plain); +} + +// Mounted where there is a page to mount on; a test imports the module for its rules alone. +if (globalThis.document$) globalThis.document$.subscribe(mount); else if (globalThis.document) mount(); diff --git a/docs/contributing/coding-standards.md b/docs/contributing/coding-standards.md index daa6eb019..a6e2ced32 100644 --- a/docs/contributing/coding-standards.md +++ b/docs/contributing/coding-standards.md @@ -2,7 +2,7 @@ How code in this repo is written. Hard rules and process live in [CLAUDE.md](../CLAUDE.md). -How to build and run lives in [building.md](../how-to/building.md), and what is tested in [testing.md](../reference/testing.md). Design rationale lives in [the architecture](../explanation/architecture/index.md). +How to build and run lives in [MoonWalk's contributor processes](../moonwalk.md#desktop-build-test), and what is tested in [testing.md](../reference/testing.md). Design rationale lives in [the architecture](../explanation/architecture/index.md). It runs from the smallest scale outward: the line you are typing, then naming and when to add a file, then values and the hot path they run in. After that the shape of a whole module, then the tooling that checks all of it, and last the method for when something breaks. @@ -165,7 +165,7 @@ All targets build warnings-as-errors: `-Wall -Wextra -Werror` on Clang/GCC (macO - In a comparison, keep both sides the same signedness, don't mix a signed expression with an unsigned literal (`== 1`, not `== 1u`, when the other side is signed). Watch `& `, `%`, and subtraction results, which carry the signedness of their operands. - A change that only built+passed on macOS/Linux is **not** verified for Windows. The Windows CI job (`release.yml`) is the real gate for MSVC-only warnings; let it run before considering a `src/`-touching change done, or build with MSVC locally if you have it. -Diagnostics as you type come from clangd, using the same `.clang-tidy` CI runs; once-per-machine setup is in [building.md, editor setup](../how-to/building.md#editor-setup-clangd). +Diagnostics as you type come from clangd, using the same `.clang-tidy` CI runs; once-per-machine setup is [Set up the editor](../moonwalk.md#setup-editor). ### Static checks diff --git a/docs/contributing/documentation-standards.md b/docs/contributing/documentation-standards.md index 34232c00e..a096b2958 100644 --- a/docs/contributing/documentation-standards.md +++ b/docs/contributing/documentation-standards.md @@ -14,7 +14,7 @@ A reader enters at the top and stops as soon as they have enough. A writer puts flowchart TB entry["README.md · index.md
what it is, what to do next"] - tut["tutorial
gettingstarted.md · tutorials/
a lesson to follow"] + tut["tutorial
tutorials/
a lesson to follow"] how["how-to
how-to/
one task you already have"] exp["explanation
explanation/
why it is shaped this way"] ref["reference
reference/ · moonmodules/
facts, fast"] @@ -119,10 +119,10 @@ Every page serves one of four reader needs, and only one. This is [Diátaxis](ht | | Doing | Understanding | |---|---|---| -| **Learning** | **Tutorial**: a lesson to follow. `gettingstarted.md`, `tutorials/`. | **Explanation**: why it is shaped this way. `explanation/`. | +| **Learning** | **Tutorial**: a lesson to follow. `tutorials/`, and MoonTube's clips. | **Explanation**: why it is shaped this way. `explanation/`. | | **Working** | **How-to**: one task you already have. `how-to/`. | **Reference**: facts, fast. `reference/`, the catalog rows, the headers. | -**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 folder under `docs/` is the type.** A page's path says which cell it sits in. So `how-to/installing-on-linux.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 2d7e6b8ee..a6c09f6e2 100644 --- a/docs/explanation/architecture/index.md +++ b/docs/explanation/architecture/index.md @@ -2,7 +2,7 @@ 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). +Coding conventions live in [coding-standards.md](../../contributing/coding-standards.md); how to build and run lives in [MoonWalk's contributor processes](../../moonwalk.md#desktop-build-test); what is tested lives in [testing.md](../../reference/testing.md). ## The problem @@ -30,11 +30,15 @@ flowchart LR inst["MoonInstaller · firmware, deviceModel, board"] cloud["MoonCloud · opt-in stats and talk"] deck["MoonDeck · the developer console"] + tube["MoonTube · clips that are UI tests"] + walk["MoonWalk · every process on one map"] core -.-> base core -.-> inst core -.-> cloud core -.-> deck + core -.-> tube + core -.-> walk classDef po fill:#2d3561,stroke:#7b88c9,color:#fff classDef agent fill:#3d2d61,stroke:#a07bc9,color:#fff @@ -43,7 +47,7 @@ flowchart LR class mm po class core,light agent class live check - class base,inst,cloud,deck gate + class base,inst,cloud,deck,tube,walk gate ``` | Part | What it decides | @@ -56,6 +60,8 @@ flowchart LR | [MoonInstaller](mooninstaller.md) | Firmware, deviceModel and board: three words for three different things | | [MoonCloud](mooncloud.md) | The opt-in server side, and the only server a device talks to | | [MoonDeck](moondeck.md) | One script per task, two front ends | +| [MoonTube](../../../moondeck/moontube/moontube.md) | One run file per clip: performed as a UI test, filmed as a documentation video | +| [MoonWalk](../../moonwalk.md) | Every process, from a first reader to a release, described once and drawn on one map | ## Core and light domain diff --git a/docs/explanation/architecture/moondeck.md b/docs/explanation/architecture/moondeck.md index 4d12803b4..0814bf9a0 100644 --- a/docs/explanation/architecture/moondeck.md +++ b/docs/explanation/architecture/moondeck.md @@ -2,7 +2,7 @@ The developer console: one page that builds, flashes, runs, tests and monitors across every target, and discovers devices on the network. The per-script reference is [MoonDeck.md](../../moondeck/MoonDeck.md). What follows is why it is shaped this way. -The shape comes first, then why the scripts are ours, then where the state lives. +The shape comes first, then how it pairs with MoonWalk, why the scripts are ours, and where the state lives. ## One script per task, two front ends @@ -27,19 +27,27 @@ flowchart LR Every action the console offers is a thin wrapper around a script, so `uv run moondeck/build/build_desktop.py` and the button run the same code. That is the whole design: one implementation, two ways in, and no path where a human and an agent measure something differently. + + ![MoonDeck, one page per task](../../assets/ui/moondeck_desktop.png) **The script is the contract.** It picks the right per-host build directory, applies the flags the gate expects, and tees its output where the report reads it. Reaching past it to `cmake` or `idf.py` produces a number measured differently, or a stale binary the script would have rebuilt. +## Two menus, one kitchen + +MoonDeck is the à la carte menu: every script on its page, in any order, for a reader who knows what they want. [MoonWalk](../../moonwalk.md), behind MoonDeck's 🗺️ button, is the set menu: the courses in order for one occasion, a first build or a commit. In MoonDeck it ticks off what was served, read from MoonDeck's run records. One kitchen cooks both, since a step names a script and MoonDeck runs it. So a script has one way to run, one log and one result, whichever menu ordered it. + +The docs site shows the same set menu as a reference to read, for a contributor deciding what to run or running it from a terminal. A web page runs inside the browser rather than on the reader's machine. The kitchen is MoonDeck, a server on the reader's own machine: the one place a script starts, logs and is timed. + ## Why our own scripts -The firmware builds vendor-native against pinned ESP-IDF versions, and the tooling covers far more than compile-and-flash: desktop builds, unit and scenario runs, spec and boundary checks, KPI collection, provisioning, multi-board bench orchestration. A wrapper toolchain would cover one of those and still need the scripts around it. The full reasoning is in [building.md](../../how-to/building.md#moondeck-the-dev-console). +The ESP32 build is ESP-IDF-native. MoonLight tracks the IDF against a pinned commit for chips like the P4 and the S31, a control PlatformIO's packaged platforms do not offer. The hot-path drivers (LCD_CAM, parlio, GDMA) use the vendor APIs directly rather than through an Arduino core. The tooling also covers far more than compile, upload and monitor. It builds the desktop, runs unit tests and scenarios, checks specs and boundaries, measures, serves the web installer, provisions boards and drives bench runs. A wrapper toolchain covers one of those and still needs the scripts around it. One script per task with two front ends keeps people, agents and CI on the same path. ## State, and where it lives Script definitions are committed in `moondeck/moondeck_config.json`. Runtime state, meaning the selected network, the known devices and the last-used serial port, lives in `moondeck/moondeck.json` and is gitignored: it describes one developer's bench rather than the project. -**Networks are the unit of bench state.** Each holds its own device list, serial port and WiFi credentials, and the console auto-selects the network whose subnet matches the host, so moving a laptop between networks usually needs no clicks. +**Networks are the unit of bench state.** Each holds its own device list, serial port and WiFi credentials. The console selects the network whose subnet matches the host, so moving a laptop between networks usually needs no clicks. **The device-model picker reads the installer's catalog.** The same `deviceModels.json` the web installer uses, so a device configured through either route ends up with the same module tree. Selecting a model pushes its full config: add the modules, then set their controls. diff --git a/docs/explanation/architecture/mooninstaller.md b/docs/explanation/architecture/mooninstaller.md index 06993b898..34cb23e9e 100644 --- a/docs/explanation/architecture/mooninstaller.md +++ b/docs/explanation/architecture/mooninstaller.md @@ -54,6 +54,8 @@ Detection follows the same split: a serial port with PJRC's USB ids names itself A board without a serial port, held in its bootloader by its program button, is reached from a canceled port pick, the one moment it is needed. Each next platform adds its writer here and nothing else, as the UF2 boards will. +**The installer owns the serial port** from the flash through the reboot, Improv provisioning and the push of the device model's settings, so one orchestrator, `install-orchestrator.js`, drives all of it. Each pushed document waits for the board's acknowledgement and is sent again when refused. The release keeps the last five stable and five prerelease builds on the installer's own site. Web Serial flashes an image the page reads from its own origin, and GitHub's release downloads allow no cross-origin read. GitHub Pages publishes it from the release workflow alone, with the repository's Pages source set to GitHub Actions. + The device UI updates a device the same two ways: it hands the release URL to the device, and a device without TLS answers that route with 501. The browser then downloads the image from the release's copy on the installer's site, which allows a cross-origin read, and uploads it. @@ -68,4 +70,4 @@ So the Ethernet pins live at **both** levels, and that's not a contradiction: th **The deviceModel is one level, there is no separate per-unit provenance level.** Whether a control is PCB-fixed or user-wired is not a taxonomy the code tracks; it falls out of what the catalog entry lists. A bare dev kit lists few controls (the user wires the rest, so those stay unset); a finished product lists more (its wiring is fixed). Same kind of entry, different completeness, no `kind:` flag. -**The governing rule is "default only where the hardware fixes it"**, the [Defaults rule](../../contributing/coding-standards.md#defaults) applied to pin provenance. An entry defaults a control by *including* it and leaves a user-wired control unset by *omitting* it, so the data carries the rule with no level-tagging. It covers settings as well as pins. `txPowerSetting` is set per entry because whether a rig sustains full-power WiFi TX is a brownout property of the assembly and its power supply rather than the chip. The catalog pins `Network.txPowerSetting: 8` for the ESP32-S3 N16R8 Dev, which browns out at full power on typical USB. The catalog is [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json) (schema in the [installer README](../../mooninstaller/README.md)). +**The governing rule is "default only where the hardware fixes it"**, the [Defaults rule](../../contributing/coding-standards.md#defaults) applied to pin provenance. An entry defaults a control by *including* it and leaves a user-wired control unset by *omitting* it, so the data carries the rule with no level-tagging. It covers settings as well as pins. `txPowerSetting` is set per entry because whether a rig sustains full-power WiFi TX is a brownout property of the assembly and its power supply rather than the chip. The catalog pins `Network.txPowerSetting: 8` for the ESP32-S3 N16R8 Dev, which browns out at full power on typical USB. The catalog is [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json) (its fields in [`deviceModels.schema.json`](../../mooninstaller/deviceModels.schema.json), its rules in the [installer README](../../mooninstaller/README.md#the-device-catalog)). diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md deleted file mode 100644 index 8d9867f33..000000000 --- a/docs/gettingstarted.md +++ /dev/null @@ -1,282 +0,0 @@ -# Getting started - -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. - -**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 MoonLight is first? See the [project overview](../README.md). - -MoonLight is provided as is, without warranty: like any software it has bugs, and you use it at your own risk. - ---- - -## Chapter 1: install MoonLight - -### 1. Open the installer and plug in - -Open the **[web installer](https://moonmodules.org/MoonLight/install/)** in Chrome or Edge, then plug your ESP32 into a USB port. - -![The web installer](assets/gettingstarted/01-01-installer-start.png) - - - -### 2. Pick the USB port - -Click **USB Port → Pick a port…**. Your browser shows a small list of connected devices: choose the one that appeared when you plugged in the ESP32. (Not sure which? Unplug, look at the list, plug back in: the new entry is your device.) - -![Selecting the USB port](assets/gettingstarted/01-02-select-port.png) - -**Windows users, dialog says "No serial ports available"?** Windows doesn't ship drivers for the USB-serial chips most ESP32 boards use (WCH CH340, Silicon Labs CP2102). -A one-time install fixes it for every future flash: the steps and the download link are in [building.md § Windows: USB-serial drivers](how-to/building.md#windows-usb-serial-drivers). -macOS and Linux ship these drivers built in. - -**Mac users, no ESP32 in the list?** A recent Mac asks whether a new USB accessory may connect, and a board that was refused stays blocked. -In System Settings, Privacy & Security, set "Allow accessories to connect" to ask for new accessories, then plug the board in again and allow it. - -Once a port is chosen, the installer recognizes the chip and tells you how many devices match it, so you know you're on the right track. - -![Port selected, chip detected](assets/gettingstarted/01-03-port-selected.png) - -### 3. Pick your device - -Choose your device from the **Device** picker. Each card shows a picture, the chip, and what the device can do (LEDs, WiFi, a button, a microphone…). -Click **details** on any card to see exactly what it is and a link to its product page. - -![Picking a device](assets/gettingstarted/01-04-pick-device.png) - -![A device card with its details](assets/gettingstarted/01-05-device-details.png) - -The little colored pills are the device's capabilities, and the color tells you how ready each one is: - -- 🟢 **Green**, set up and working the moment you install. This capability is supported *and* already wired into the device's configuration. -- 🟡 **Yellow**, the firmware supports it, but it isn't pre-configured. It works once you add and set up the matching module yourself in the UI (Chapter 2). -- 🟠 **Amber**, planned. The hardware has it, but there's no module for it yet: it's on the to-do list. Want to help? Building one is our usual loop: read the product page and datasheet, pin the behavior as tests, then write the code to pass them ([see how we work](../CLAUDE.md#principles)). - -So a green pill works out of the box, a yellow one works with a bit of setup, and an amber one is coming later. - -The setup panel then shows how your device is configured out of the box, the modules and settings applied automatically when you install. - -![Device setup](assets/gettingstarted/01-06-device-setup.png) - -Nothing is locked in: once the device is running you can change any of it later in the UI (that's what Chapter 2 is all about). - -Leave **Release** and **Firmware** at their suggested values: the newest stable build, and the firmware that matches your device. -Tick **Erase chip first** only to start clean or to switch firmware. -It is also needed when a 4 MB classic board (esp32 / wrover / eth) runs a release before v4.0 and you update it. -That last update must erase, since its partition layout changed ([MIGRATING](reference/MIGRATING.md)). -If the device already holds config you care about, back it up first ("Back up a device's config first" on the installer page). -Erasing wipes WiFi credentials and all settings, and the backup brings them back after the flash; its report lists anything it could not carry. - -### 4. Click Install - -The installer erases (if you asked it to) and writes the firmware. It takes under a minute. - -![Erasing](assets/gettingstarted/01-07-erasing.png) -![Installing](assets/gettingstarted/01-08-installing.png) - -### 5. Get it on your network - -What happens next depends on your device: - -- **WiFi:** enter your network name and password when prompted, then **Connect**. - Or click **Skip** and set WiFi up from the device itself. It opens its own `MM-XXXX` network, and joining that with a phone or laptop opens a sign-in screen on its WiFi card. Pick your network there. - Restoring a config backup? Skip this step too. Join the `MM-XXXX` network: the device's UI opens by itself as a sign-in screen, or at `http://4.3.2.1`. Restore the backup in the File Manager (⟲), and take the offered restart. The bundle carries the WiFi credentials, so the device joins your network by itself. - - ![Entering WiFi credentials](assets/gettingstarted/01-09-wifi-credentials.png) - -- **Ethernet:** plug in the cable: it connects on its own, no password needed. - -### 6. Open your device - -When it's online, the installer shows a link: your device's address on your network. Click it. - -![Device is online over WiFi](assets/gettingstarted/01-10-online-wifi.png) - -You'll see this same "Device is online!" box however your device connected: over Ethernet, or when it rejoins a network it already knows. - -![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: 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 MoonLight interface - -Everything below runs **in your browser, live from the device**. There's no app, no account and no cloud: the ESP32 itself serves the interface, 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 what a device adds to the interface, on a board that was flashed a moment earlier: - - - -### The layout: list, preview, controls - -![The full MoonLight interface](assets/gettingstarted/02-01-UI-large.png) - -Three regions, left to right: - -- **The module list** (left): every part of your device, from system info at the top to your light setup at the bottom. Click a name to jump to it. -- **The 3D preview** (center): a live picture of your lights in their real shape, updating as the effects run. This is what your physical LEDs are doing, right now. -- **The controls** (right): the settings for each module. Drag a slider or pick an option and the lights react instantly. - -Every module header carries a **⏻ power button**, which turns that module on or off. Bright (accent-colored) means on; dimmed means off. -A switched-off module stops running and stays in place with all its settings, so flicking it back on picks up right where it left off. It's the quick way to mute an effect or an output for a moment without deleting anything. - -You'll also spot two little read-outs in each header. **🕒** is how fast that module runs: its loop speed, which a click flips between fps and microseconds. **🧠** is how much memory it uses. They let you see at a glance what each part is costing. - -The interface adapts to your window. On a narrower screen the controls take the full width and the preview tucks into a floating thumbnail you can move around: - -![Medium width, preview as a floating thumbnail](assets/gettingstarted/02-02-UI-mid.png) - -Narrower still, it stacks into a single scrollable column, so it works on a phone, standing next to your lights: - -![Small width, single column](assets/gettingstarted/02-03-UI-small.png) - -### The 3D preview - -![The 3D preview, lights numbered](assets/gettingstarted/02-04-UI-Preview.png) - -Drag to rotate, scroll to zoom. Each dot is one light at its real position, lit with the color it's showing this instant. Turn on the numbers to see each light's index, handy when you're wiring or mapping a layout. -The preview is a *view* of the device: it never slows the lights down, and on a slow connection it eases off (fewer updates, then fewer points) rather than stalling. - -> More on how the preview streams from the device: [PreviewDriver](moonmodules/light/drivers.md#preview). - -### 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, and ? opens that module's page in the documentation. { } 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. - -**System**: who this device is and how it's doing: its name, the device model, uptime, frame rate, and live memory / storage bars. -You may also see an **Audio** module here. Devices with a built-in mic come with it set up, and on any device you can add it yourself. It's how audio-reactive effects hear the music. -Audio is the first of many: any sensor or input, from hardware or over the network, lives here as its own module, and we're adding more all the time. - -![The System module](assets/gettingstarted/02-05-UI-System.png) - -> [SystemModule](moonmodules/core/system.md#system) · [Audio](moonmodules/core/services.md#audio) - -**Firmware**: which build you're running, and where you update it. The **Install** button here does an over-the-air update straight from the device, no USB cable needed once it's on your network. - -![The Firmware module](assets/gettingstarted/02-06-UI-Firmware.png) - -**Updating from an older build?** Skim the [migration notes](reference/MIGRATING.md) first. Most updates need nothing, since the device keeps your settings. -A breaking change is listed there with the one action it costs you, usually re-setting or re-adding a control. - -> [FirmwareUpdateModule](moonmodules/core/system.md#firmware-update) - -**Network**: your connection, WiFi or Ethernet, its signal strength, and the 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) - -> [NetworkModule](moonmodules/core/system.md#network) · [DevicesModule](moonmodules/core/system.md#devices) - -> **Lights are one use among many.** Everything above (the modules, the live controls, the 3D view, the web UI, the networking) is a general-purpose engine that knows nothing 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 MoonLight ships as a reusable library). - -### Control it from your phone with WLED Native - -The device's own web UI works on a phone. 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 on the network as a WLED device does. So the app finds your MoonLight controllers automatically, with no setup and 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. - -![MoonLight devices discovered in WLED Native](assets/core/WLED%20Native%20discovers%20MoonLight.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 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? 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. - -![MoonLight devices as lights in a Home Assistant dashboard](assets/core/ha-integration.png){ width="600" } - -There are two ways in: zeroconf, where HA finds the device on its own, or MQTT auto-discovery, for a broker-only or cross-subnet setup. 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). - -### Building a light show: layouts → layers → drivers - -The bottom three modules are where the fun is. They form a simple pipeline: a **layout** says where your lights are, **layers** decide what colors play on them, and **drivers** send the result out to the real world. Add modules with the dashed **+ add module** button under each one. - -**Layouts**: the shape of your lights. The default **Grid** is a width × height (× depth) of pixels; change the numbers and the preview reshapes instantly. Turn on **serpentine** if your strip zig-zags back and forth. - -![The Layouts module](assets/gettingstarted/02-08-UI-Layouts.png) - -> [Layouts](moonmodules/light/supporting.md) - -**Effects**: what plays on the lights. Add an **effect** (a moving pattern), stack several to blend them, and reshape them with **modifiers** (mirror, rotate, and more). Each effect has its own controls, such as speed and color mode, which you tweak live. - -![The Effects module](assets/gettingstarted/02-09-UI-Layers.png) - -> [Effects](moonmodules/light/supporting.md) · [Layer](moonmodules/light/supporting.md) - -**Drivers**: where the colors go. Set overall **brightness** and color order, then add an output. That's real LED strips on a pin, or the frame sent over the network (ArtNet, E1.31/sACN, DDP) to other devices or lighting software. - -![The Drivers module](assets/gettingstarted/02-10-UI-Drivers.png) - -> [Drivers](moonmodules/light/supporting.md) · [NetworkSendDriver](moonmodules/light/drivers.md#networksend) - -That's the whole picture: **layout → layers → drivers**, previewed in 3D, all tuned live in your browser. Pick an effect, drag a slider, watch the lights, then keep going. - ---- - -### If your device shows MoonBase - -**MoonBase** is a small recovery image built into your device. If a firmware update is interrupted, or an installed firmware does not start, your device boots MoonBase instead of going dark, and its page offers you three ways out: - -- **Boot the app** puts you straight back if the firmware is still fine. Try this first: it changes nothing on the device. -- **From a file** installs a firmware you have already downloaded. Get the `firmware-...bin` matching your device from the [releases page](https://github.com/MoonModules/MoonLight/releases). -- **From a URL** downloads and installs in one step. The releases page gives you a link to each file; paste it here and your device fetches it directly. - -Installing takes a few minutes, and the page reports its progress as it downloads. Your device reboots into the new firmware on its own when it finishes. - -Two things worth knowing. A failed install **stays** in MoonBase rather than pretending to have worked, so you can try again. -And you cannot break a device this way: an update never overwrites MoonBase. It is still there for the next attempt, including after a power cut in the middle of one. - -Off your network, MoonBase opens the device's own `MM-XXXX` access point. Joining it opens MoonBase's page by itself as a sign-in screen, or at **4.3.2.1** there. Once it knows your WiFi, the access point carries your WiFi's password. - -MoonBase shows its own version on its page, and your device's Firmware card shows which MoonBase it carries. When it does not match the app, the card warns, and its MoonBase tab installs the matching one over the network, so keeping the recovery image current needs no cable. - -That update runs from the app, because only the running app can write the partition MoonBase lives in. So it is a way to keep MoonBase fresh, not a way back from a device that will not start. If the app cannot run, or MoonBase itself will not boot, that still takes a cable. -Your device checks the image first, refusing anything whose magic bytes, chip or description say it is not a MoonBase image for this chip. - ---- - -### 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 the frame and sends it over the network instead of driving a strip. It also has the memory for a far larger grid than a board does. - - - -It's useful for trying an idea before you wire anything, or for developing effects without a flash cycle. A machine with real processing power can also drive a board over the network. - -### Where to go next - -- **Understand the pipeline**, how layouts, layers, effects, modifiers and drivers fit together: [architecture overview](explanation/architecture/moonlight.md#the-pipeline). -- **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). - -Stuck, or something didn't work? Open an [issue](https://github.com/MoonModules/MoonLight/issues), and tell us what device you used and where it stopped. diff --git a/docs/help/help.json b/docs/help/help.json index d16312029..8a5d8dbf9 100644 --- a/docs/help/help.json +++ b/docs/help/help.json @@ -122,7 +122,7 @@ "dim": 0, "doc": "light/supporting.md#fixtureprofiles", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/FixtureProfilesModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FixtureProfilesModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#fixtureprofilesmodule" }, "CarLightsLayout": { "line": "A pair of car headlights: concentric rings of lights placed like a car's front lamps.", @@ -229,7 +229,7 @@ "dim": 0, "doc": "light/layouts.md#moonlive", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/moonlive/MoonLiveLayout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_MoonLiveLayout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#moonlivelayout" }, "PanelsLayout": { "line": "An M × N grid of matrix panels, tiled into one large display for a video wall.", @@ -323,7 +323,7 @@ "dim": 3, "doc": "light/layouts.md#grid", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/layouts/GridLayout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_GridLayout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#gridlayout" }, "GridBlacksLayout": { "line": "A grid with dark columns held black in every row, for a sealed panel that must stay dark down a strip, or a slat wall.", @@ -355,7 +355,7 @@ "dim": 3, "doc": "light/layouts.md#gridblacks", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/layouts/GridBlacksLayout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_GridBlacksLayout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#gridblackslayout" }, "PanelLayout": { "line": "A 2D matrix panel with full wiring control: the axis order, the direction per axis, and serpentine.", @@ -443,7 +443,7 @@ "dim": 2, "doc": "light/layouts.md#rings241", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/layouts/Rings241Layout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_Rings241Layout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#rings241layout" }, "SingleColumnLayout": { "line": "A vertical line of lights at a fixed x, the 1D column.", @@ -468,7 +468,7 @@ "dim": 1, "doc": "light/layouts.md#singlecolumn", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/layouts/SingleColumnLayout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_SingleColumnLayout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#singlecolumnlayout" }, "SingleRowLayout": { "line": "A horizontal line of lights at a fixed y: the 1D row, a bare strip laid straight.", @@ -507,7 +507,7 @@ "dim": 3, "doc": "light/layouts.md#sphere", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/layouts/SphereLayout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_SphereLayout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#spherelayout" }, "SpiralLayout": { "line": "A conical spiral: lights winding up a cone from a wide base to a point.", @@ -570,7 +570,7 @@ "dim": 2, "doc": "light/layouts.md#wheel", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/layouts/WheelLayout.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_WheelLayout.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#wheellayout" }, "AudioSpectrumEffect": { "line": "The 16 mic frequency bands spread across X, each column lit bottom-up by its magnitude.", @@ -661,7 +661,7 @@ "dim": 2, "doc": "light/effects.md#beatripples", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/BeatRipplesEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_BeatRipples.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#beatrippleseffect" }, "BlurzEffect": { "line": "Audio-reactive blurred dots: one frequency band per frame lights a dot, then the whole frame is blurred for soft trails.", @@ -687,7 +687,7 @@ "dim": 2, "doc": "light/effects.md#blurz", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/BlurzEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_BlurzEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#blurzeffect" }, "BouncingBallsEffect": { "line": "A row of balls per column bounce under gravity, each losing energy on impact and relaunching when it stops.", @@ -707,7 +707,7 @@ "dim": 2, "doc": "light/effects.md#bouncingballs", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/BouncingBallsEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_BouncingBallsEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#bouncingballseffect" }, "DemoReelEffect": { "line": "Plays every other registered effect in turn, auto-advancing on a timer, so one Layer cycles the whole library hands-free.", @@ -733,7 +733,7 @@ "dim": 3, "doc": "light/effects.md#demoreel", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/DemoReelEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_DemoReelEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#demoreeleffect" }, "DistortionWavesEffect": { "line": "Two interfering sine waves beat against each other into a moire color field.", @@ -756,7 +756,7 @@ "dim": 2, "doc": "light/effects.md#distortionwaves", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/DistortionWavesEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_DistortionWavesEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#distortionwaveseffect" }, "FireEffect": { "line": "A Fire2012-style heat field: sparks at the base rise and cool through the active palette, coldest at its low end.", @@ -776,7 +776,7 @@ "dim": 2, "doc": "light/effects.md#fire", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FireEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FireEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#fireeffect" }, "FixedRectangleEffect": { "line": "A solid color filling a positioned box within the grid, with an optional alternating-white checker on the box's pixels.", @@ -823,7 +823,7 @@ "dim": 3, "doc": "light/effects.md#fixedrectangle", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FixedRectangleEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FixedRectangleEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#fixedrectangleeffect" }, "FreqMatrixEffect": { "line": "A 1D scrolling frequency display: each frame shifts the strip and injects a new pixel at one end.", @@ -855,7 +855,7 @@ "dim": 1, "doc": "light/effects.md#freqmatrix", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FreqMatrixEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FreqMatrixEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#freqmatrixeffect" }, "FreqSawsEffect": { "line": "Audio-reactive sawtooth waves: each column maps to a frequency band whose loudness drives a per-band oscillator speed.", @@ -890,7 +890,7 @@ "dim": 2, "doc": "light/effects.md#freqsaws", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FreqSawsEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FreqSawsEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#freqsawseffect" }, "GameOfLifeEffect": { "line": "Conway's cellular automaton generalized to 2D and 3D, with selectable rulesets and a custom `B#/S#` rule.", @@ -953,7 +953,7 @@ "dim": 3, "doc": "light/effects.md#gameoflife", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/GameOfLifeEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_GameOfLifeEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#gameoflifeeffect" }, "GEQEffect": { "line": "A flat graphic equalizer: the 16 audio bands rise as vertical bars from the bottom.", @@ -979,7 +979,7 @@ "dim": 2, "doc": "light/effects.md#geq", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/GEQEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_GEQEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#geqeffect" }, "GEQ3DEffect": { "line": "A 3D-perspective graphic equalizer: audio bands rise as bars with faked depth.", @@ -1011,7 +1011,7 @@ "dim": 2, "doc": "light/effects.md#geq3d", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/GEQ3DEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_GEQ3DEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#geq3deffect" }, "LavaLampEffect": { "line": "Three slow metaball blobs that rise and merge, colored by the active palette.", @@ -1083,7 +1083,7 @@ "dim": 2, "doc": "light/effects.md#lissajous", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/LissajousEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_LissajousEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#lissajouseffect" }, "MetaballsEffect": { "line": "Blobs orbit and merge smoothly, where the summed field of all balls picks each pixel's color and brightness.", @@ -1109,7 +1109,7 @@ "dim": 2, "doc": "light/effects.md#metaballs", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/MetaballsEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_MetaballsEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#metaballseffect" }, "MoonLiveEffect": { "line": "An effect written as text on the running device, compiled to native code on the next tick.", @@ -1155,7 +1155,7 @@ "dim": 3, "doc": "light/effects.md#networkreceive", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/NetworkReceiveEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_NetworkReceiveEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#networkreceiveeffect" }, "NoiseEffect": { "line": "A gradient-noise field indexed straight into the palette, the plainest way to turn noise into light.", @@ -1179,7 +1179,7 @@ "dim": 3, "doc": "light/effects.md#noise", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/NoiseEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_NoiseEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#noiseeffect" }, "NoiseMeterEffect": { "line": "An audio VU meter rendered as a noise bar, where the volume sets how many rows light from the bottom.", @@ -1199,7 +1199,7 @@ "dim": 1, "doc": "light/effects.md#noisemeter", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/NoiseMeterEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_NoiseMeterEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#noisemetereffect" }, "PaintBrushEffect": { "line": "Audio-reactive brush strokes: lines whose 3D endpoints oscillate on the beat over a fading field.", @@ -1231,7 +1231,7 @@ "dim": 3, "doc": "light/effects.md#paintbrush", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/PaintBrushEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PaintBrushEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#paintbrusheffect" }, "ParticlesEffect": { "line": "A swarm of drifting particles with persistent fading trails.", @@ -1257,7 +1257,7 @@ "dim": 2, "doc": "light/effects.md#particles", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/ParticlesEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_ParticlesEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#particleseffect" }, "FseqPlayerEffect": { "line": "An FSEQ show played from a file: the frame-by-frame channel data xLights and Falcon Player write.", @@ -1281,7 +1281,7 @@ "dim": 3, "doc": "light/effects.md#fseqplayer", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FseqPlayerEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FseqPlayerEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#fseqplayereffect" }, "PictureEffect": { "line": "The active [Picture service](https://moonmodules.org/MoonLight/moonmodules/core/services.html#picture)'s picture on the layer, so a picture of any size shows on a layout of any size.", @@ -1302,7 +1302,7 @@ "dim": 2, "doc": "light/effects.md#picture", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/PictureEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PictureEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#pictureeffect" }, "PlasmaEffect": { "line": "Summed sine waves on orthogonal and diagonal axes make large rolling blobs, in 3D on volumetric layouts.", @@ -1328,7 +1328,7 @@ "dim": 3, "doc": "light/effects.md#plasma", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/PlasmaEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PlasmaEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#plasmaeffect" }, "PraxisEffect": { "line": "An algorithmic palette pattern driven by two beat oscillators, a macro and a micro mutator, whose frequencies and ranges reshape the hue field over time.", @@ -1363,7 +1363,7 @@ "dim": 2, "doc": "light/effects.md#praxis", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/PraxisEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PraxisEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#praxiseffect" }, "PulseEffect": { "line": "Shells expand from a drifting origin, one per beat: a pair of fronts on a strip, a ring on a panel, a sphere in a volume.", @@ -1413,7 +1413,7 @@ "dim": 2, "doc": "light/effects.md#rainbow", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/RainbowEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RainbowEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#rainboweffect" }, "RandomEffect": { "line": "Lights one random light per frame in a random palette color over a fading background: a sparse, palette-tinted sparkle.", @@ -1429,7 +1429,7 @@ "dim": 3, "doc": "light/effects.md#random", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/RandomEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RandomEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#randomeffect" }, "RingsEffect": { "line": "Expanding concentric rings from random centers, with additive overlap and calm defaults.", @@ -1499,7 +1499,7 @@ "dim": 3, "doc": "light/effects.md#rubikscube", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/RubiksCubeEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RubiksCubeEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#rubikscubeeffect" }, "SineEffect": { "line": "Red, green and blue each follow a sine along one axis at a third of a turn apart: a glowing, scrolling color box.", @@ -1521,7 +1521,7 @@ "dim": 3, "doc": "light/effects.md#sine", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/SineEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_SineEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#sineeffect" }, "SolidEffect": { "line": "A flat fill with five color modes: a plain RGB(W) color, the palette spread across the lights, one RMS-averaged palette color, or the palette banded along rows or columns.", @@ -1558,7 +1558,7 @@ "dim": 3, "doc": "light/effects.md#solid", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/SolidEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_SolidEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#solideffect" }, "SdfShapesEffect": { "line": "A circle and a box orbit and melt into each other, drawn as signed distance fields rather than outlines.", @@ -1627,7 +1627,7 @@ "dim": 3, "doc": "light/effects.md#aurora", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/AuroraEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_AuroraEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#auroraeffect" }, "PolarNoiseEffect": { "line": "A warped noise field addressed by angle and radius, folded into a kaleidoscope.", @@ -1734,7 +1734,7 @@ "dim": 3, "doc": "light/effects.md#fluid", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FluidEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_Fluid.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#fluid" }, "NebulaEffect": { "line": "A noise field decides where light is born, a curl flow decides where it goes, and the cloud keeps folding into itself.", @@ -1803,7 +1803,7 @@ "dim": 3, "doc": "light/effects.md#trails", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/TrailsEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_TrailsEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#trailseffect" }, "ColorTrailsEffect": { "line": "Emitters pouring color into a flow that carries and folds it.", @@ -1844,7 +1844,7 @@ "dim": 3, "doc": "light/effects.md#colortrails", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/ColorTrailsEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_ColorTrailsEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#colortrailseffect" }, "TunnelEffect": { "line": "A texture mapped onto the inside of an infinite tube, so the viewer appears to fly down it forever.", @@ -2080,7 +2080,7 @@ "dim": 2, "doc": "light/effects.md#fixedpoint", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/FixedPointEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_FixedPointEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#fixedpointeffect" }, "MovingHeadEffect": { "line": "Aims a rig of moving heads as one instrument, where `formation` decides how the heads relate.", @@ -2129,7 +2129,7 @@ "dim": 3, "doc": "light/effects.md#movinghead", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/MovingHeadEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_MovingHeadEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#movingheadeffect" }, "FlyingToastersEffect": { "line": "The classic screensaver on a light wall: chrome toasters with flapping wings and slices of toast drift diagonally across the dark, forever.", @@ -2198,7 +2198,7 @@ "dim": 2, "doc": "light/effects.md#spaceinvaders", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/SpaceInvadersEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_SpaceInvadersEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#spaceinvaderseffect" }, "SpriteFountainEffect": { "line": "A fountain that throws the project's whole sprite cast, launched from the floor on a sweeping nozzle and falling back under gravity.", @@ -2269,7 +2269,7 @@ "dim": 2, "doc": "light/effects.md#pong", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/PongEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PongEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#pongeffect" }, "BreakoutEffect": { "line": "A paddle knocking down a wall of bricks, the attract mode of the 1976 original where the machine plays itself.", @@ -2304,7 +2304,7 @@ "dim": 2, "doc": "light/effects.md#breakout", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/BreakoutEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_BreakoutEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#breakouteffect" }, "BallpitEffect": { "line": "Falling balls that pile up and shove each other aside.", @@ -2444,7 +2444,7 @@ "dim": 3, "doc": "light/effects.md#spheremove", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/SphereMoveEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_SphereMoveEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#spheremoveeffect" }, "SpiralEffect": { "line": "A rotating spiral built from each pixel's angle and distance from the center.", @@ -2492,7 +2492,7 @@ "dim": 2, "doc": "light/effects.md#starfield", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/StarFieldEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_StarFieldEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#starfieldeffect" }, "StarSkyEffect": { "line": "Twinkling stars at random light positions, each fading in and out independently over a dark background.", @@ -2515,7 +2515,7 @@ "dim": 3, "doc": "light/effects.md#starsky", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/StarSkyEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_StarSkyEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#starskyeffect" }, "TetrixEffect": { "line": "Falling Tetris-style blocks: each column drops a brick that lands on the growing stack, fills the column, then clears and restarts.", @@ -2538,7 +2538,7 @@ "dim": 2, "doc": "light/effects.md#tetrix", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/TetrixEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_TetrixEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#tetrixeffect" }, "TextEffect": { "line": "Renders a multi-line string in a bitmap font, static by default or as a wrapping marquee.", @@ -2566,7 +2566,7 @@ "dim": 2, "doc": "light/effects.md#text", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/TextEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_TextEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#texteffect" }, "WaveEffect": { "line": "An oscilloscope waveform scrolls across the grid with a fading trail.", @@ -2588,7 +2588,7 @@ "dim": 2, "doc": "light/effects.md#wave", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/effects/WaveEffect.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_WaveEffect.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#waveeffect" }, "BlockModifier": { "line": "Expands a 1D effect into concentric square rings: its position along the strip becomes the ring, so a gradient draws nested squares.", @@ -2601,7 +2601,7 @@ "kind": "static", "doc": "light/modifiers.md#block", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/BlockModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_BlockModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#blockmodifier" }, "CheckerboardModifier": { "line": "Masks the layer in a checkerboard: the lights on the \"off\" squares stay dark, the rest show the effect unchanged.", @@ -2621,7 +2621,7 @@ "kind": "static", "doc": "light/modifiers.md#checkerboard", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/CheckerboardModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_CheckerboardModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#checkerboardmodifier" }, "MoonLiveModifier": { "line": "The coordinate transform written as text on the running device: mirror the pattern, shift it, swap its axes.", @@ -2652,7 +2652,7 @@ "kind": "static", "doc": "light/modifiers.md#moonlive", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/moonlive/MoonLiveModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_MoonLiveModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#moonlivemodifier" }, "CircleModifier": { "line": "Expands a 1D effect into concentric circular rings: its position along the strip becomes the radius, so a gradient draws nested circles.", @@ -2665,7 +2665,7 @@ "kind": "static", "doc": "light/modifiers.md#circle", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/CircleModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_CircleModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#circlemodifier" }, "MirrorModifier": { "line": "Folds the far half of the box back onto the near half, mirroring the image across its center on each axis you choose.", @@ -2688,7 +2688,7 @@ "kind": "static", "doc": "light/modifiers.md#mirror", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/MirrorModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_MirrorModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#mirrormodifier" }, "MultiplyModifier": { "line": "Tiles the image across the box, a number of times per axis, and can mirror every other tile into a kaleidoscope.", @@ -2720,7 +2720,7 @@ "kind": "static", "doc": "light/modifiers.md#multiply", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/MultiplyModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_MultiplyModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#multiplymodifier" }, "PinwheelModifier": { "line": "Remaps the grid into petals around the center, so a line or a 2D effect turns into a rotating flower or spokes.", @@ -2750,7 +2750,7 @@ "kind": "static", "doc": "light/modifiers.md#pinwheel", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/PinwheelModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PinwheelModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#pinwheelmodifier" }, "RandomMapModifier": { "line": "Moves every light to another, each one exactly once, and reshuffles on a beat: the arrangement scrambles while the content stays.", @@ -2767,7 +2767,7 @@ "kind": "animated", "doc": "light/modifiers.md#randommap", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/RandomMapModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RandomMapModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#randommapmodifier" }, "RegionModifier": { "line": "Crops the layer to a rectangle given in percentages of each axis, so it holds when the layout is resized; outside it is dark.", @@ -2799,7 +2799,7 @@ "kind": "static", "doc": "light/modifiers.md#region", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/RegionModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RegionModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#regionmodifier" }, "RippleXZModifier": { "line": "Collapses an axis to a single plane, so an effect drawn along the other axes ripples across the whole box.", @@ -2822,7 +2822,7 @@ "kind": "static", "doc": "light/modifiers.md#ripplexz", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/RippleXZModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RippleXZModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#ripplexzmodifier" }, "RotateModifier": { "line": "Turns the 2D image around its center, continuously, at the speed you set.", @@ -2839,7 +2839,7 @@ "kind": "dynamic", "doc": "light/modifiers.md#rotate", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/RotateModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_RotateModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#rotatemodifier" }, "TransposeModifier": { "line": "Swaps a pair of axes and can flip each one, so the image rotates or mirrors without the effect redrawing.", @@ -2871,7 +2871,7 @@ "kind": "static", "doc": "light/modifiers.md#transpose", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/modifiers/TransposeModifier.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_TransposeModifier.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#transposemodifier" }, "HueDriver": { "line": "Drives Philips Hue bulbs as pixels: each color bulb in the driver's window becomes one pixel, pushed to the bridge over its HTTP API.", @@ -2906,7 +2906,7 @@ "dim": 0, "doc": "light/drivers.md#hue", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/HueDriver.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_HueDriver.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#huedriver" }, "NetworkSendDriver": { "line": "Streams the buffer over UDP as Art-Net, E1.31 or DDP, its packets paced so receivers keep up, to Falcon and Advatek controllers, xLights and LedFx.", @@ -3000,7 +3000,7 @@ "dim": 0, "doc": "light/drivers.md#preview", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/PreviewDriver.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PreviewDriver.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#previewdriver" }, "NdiDriver": { "line": "Publishes the layer as an NDI video source that OBS, Resolume, TouchDesigner or any NDI receiver finds by name on the network.", @@ -3050,7 +3050,7 @@ "dim": 0, "doc": "light/drivers.md#ndi", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/NdiDriver.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_NdiDriver.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#ndidriver" }, "HlsDriver": { "line": "Streams the layer as H.264 over HLS from the device's own HTTP server, which a TV, VLC or a browser can play.", @@ -3105,7 +3105,7 @@ "dim": 0, "doc": "light/drivers.md#hls", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/HlsDriver.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_HlsDriver.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#hlsdriver" }, "RtspDriver": { "line": "Serves the layer as an H.264 stream a player pulls over RTSP, with a fraction of the delay HLS carries.", @@ -3214,7 +3214,7 @@ "dim": 0, "doc": "light/drivers.md#panelcard", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/PanelCardDriver.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PanelCardDriver.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#panelcarddriver" }, "Hub75Driver": { "line": "Drives HUB75 LED panels straight from the board's own pins, with no receiving card in between.", @@ -3380,7 +3380,7 @@ "dim": 0, "doc": "light/drivers.md#pwm-light", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/light/drivers/PwmLightDriver.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_PwmLightDriver.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#pwmlightdriver" }, "ParallelLedDriver": { "line": "WS2812B-class LEDs on up to 16 strands clocked out at once by a DMA peripheral, which makes a large wall possible.", @@ -3562,7 +3562,7 @@ "dim": 0, "doc": "core/system.md#system", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/SystemModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_SystemModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#systemmodule" }, "ControlModule": { "line": "A control desk of switches, encoders, faders and preset pads that puts the device into a named state.", @@ -3611,7 +3611,7 @@ "dim": 0, "doc": "core/system.md#control", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/ControlModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_ControlModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#controlmodule" }, "Services": { "line": "The container for the user-added Service modules, the capability bridges a device provides or consumes.", @@ -3620,7 +3620,7 @@ "dim": 0, "doc": "core/services.md", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/Services.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_Services.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#services" }, "AudioService": { "line": "The audio source the audio-reactive effects read: a level, sixteen bands and the peak.", @@ -3713,7 +3713,7 @@ "dim": 0, "doc": "core/services.md#audio", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/AudioService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/light/unit_AudioService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#audioservice" }, "PictureService": { "line": "The picture the Picture effect samples onto the lights, from one source at a time.", @@ -3735,7 +3735,7 @@ "dim": 0, "doc": "core/services.md#picture", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/PictureService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_PictureService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#pictureservice" }, "OscService": { "line": "Receives OSC over the network and writes it onto this device's controls, so a fader in Resolume, TouchDesigner or TouchOSC drives MoonLight.", @@ -3783,7 +3783,7 @@ "dim": 0, "doc": "core/services.md#osc", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/OscService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_OscService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#oscservice" }, "I2cBusModule": { "line": "The board's I2C bus: its two pins, opened once and shared by every device on it, with a scan that detects them.", @@ -3813,7 +3813,7 @@ "dim": 0, "doc": "core/system.md#i2c-bus", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/I2cBusModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_I2cBusModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#i2cbusmodule" }, "TasksModule": { "line": "A read-only diagnostic showing what runs where: the tasks, and the modules inside each.", @@ -3840,7 +3840,7 @@ "dim": 0, "doc": "core/system.md#tasks", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/TasksModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_TasksModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#tasksmodule" }, "PinsModule": { "line": "A read-only map of which module owns each GPIO, for what role, and whether the pin is safe for it.", @@ -3861,7 +3861,7 @@ "dim": 0, "doc": "core/system.md#pins", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/PinsModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_PinsModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#pinsmodule" }, "ButtonService": { "line": "A list of push buttons, each on its own GPIO, each driving a control.", @@ -3889,7 +3889,7 @@ "dim": 0, "doc": "core/services.md#button", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/ButtonService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_ButtonService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#buttonservice" }, "GamepadService": { "line": "A gamepad's buttons and sticks, each driving a control through the same rows every input service uses.", @@ -3919,7 +3919,7 @@ "dim": 0, "doc": "core/services.md#gamepad", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/GamepadService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_GamepadService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#gamepadservice" }, "MidiService": { "line": "A MIDI control desk driving the control surface, in the Mackie Control or the Akai APC40 mkII layout.", @@ -4001,7 +4001,7 @@ "dim": 0, "doc": "core/services.md#analog", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/AnalogService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_AnalogService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#analogservice" }, "InfraredService": { "line": "An infrared receiver whose learned remote keys drive other modules' controls.", @@ -4031,7 +4031,7 @@ "dim": 0, "doc": "core/services.md#infrared", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/InfraredService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_InfraredService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#infraredservice" }, "MoonLiveService": { "line": "A MoonLive script that reads hardware and drives controls, the input twin of a scripted effect.", @@ -4053,7 +4053,7 @@ "dim": 0, "doc": "core/services.md#moonliveservice", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/MoonLiveService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_MoonLiveService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#moonliveservice" }, "FileManagerModule": { "line": "Browse and manage the device filesystem: a folder tree with an inline text editor.", @@ -4088,7 +4088,7 @@ "dim": 0, "doc": "core/system.md#file-manager", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/FileManagerModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_FileManagerModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#filemanagermodule" }, "SdCardService": { "line": "A board's microSD slot, where one is wired: its wiring, a usage gauge and a format button.", @@ -4149,7 +4149,7 @@ "dim": 0, "doc": "core/services.md#sd-card", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/services/SdCardService.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_SdCardService.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#sdcardservice" }, "FirmwareUpdateModule": { "line": "Over-the-air firmware flashing: what is installed, and how an install goes.", @@ -4189,7 +4189,7 @@ "dim": 0, "doc": "core/system.md#firmware-update", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/FirmwareUpdateModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_FirmwareUpdateModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#firmwareupdatemodule" }, "MoonCloudModule": { "line": "The container for everything MoonLight does with a server MoonModules runs.", @@ -4207,7 +4207,7 @@ "dim": 0, "doc": "core/system.md#mooncloud", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/MoonCloudModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_MoonCloudModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#mooncloudmodule" }, "MoonStatsModule": { "line": "One opt-in report about this install, sent once per install or upgrade, so development effort goes where the users are.", @@ -4240,7 +4240,7 @@ "dim": 0, "doc": "core/system.md#stats", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/MoonStatsModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_MoonStatsModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#moonstatsmodule" }, "MoonTalkModule": { "line": "A public message board between MoonLight devices, off until you turn it on.", @@ -4274,7 +4274,7 @@ "dim": 0, "doc": "core/system.md#talk", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/MoonTalkModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_MoonTalkModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#moontalkmodule" }, "ImprovProvisioningModule": { "line": "Browser-driven WiFi provisioning over serial: the web installer hands the WiFi credentials to a fresh device.", @@ -4337,7 +4337,7 @@ "dim": 0, "doc": "core/system.md#mqtt", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/MqttModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_MqttModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#mqttmodule" }, "DevicesModule": { "line": "Discovers other devices on the LAN, MoonLight and WLED among them, and lists them, this one included and marked as such.", @@ -4385,7 +4385,7 @@ "dim": 0, "doc": "core/system.md#network", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/NetworkModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_NetworkModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#networkmodule" }, "EthernetModule": { "line": "The wired interface: which board's wiring it uses, and how it gets an address.", @@ -4500,7 +4500,7 @@ "dim": 0, "doc": "core/system.md#wifi", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/WiFiModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_WiFiModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#wifimodule" }, "AccessPointModule": { "line": "The device's own WiFi network, at `4.3.2.1`: joining it opens the UI by itself, as a sign-in screen.", @@ -4533,7 +4533,7 @@ "dim": 0, "doc": "core/system.md#access-point", "source": "https://github.com/MoonModules/MoonLight/blob/main/src/core/system/AccessPointModule.h", - "tests": "https://github.com/MoonModules/MoonLight/blob/main/test/unit/core/unit_AccessPointModule.cpp" + "tests": "https://moonmodules.org/MoonLight/reference/tests/unit-tests.html#accesspointmodule" }, "FilesystemModule": { "line": "The persistence engine: it writes control values to `/.config/*.json` and restores them on boot, so settings survive a reboot.", diff --git a/docs/how-to/adding-a-stream-source.md b/docs/how-to/adding-a-stream-source.md index 23b975caa..64caeacf8 100644 --- a/docs/how-to/adding-a-stream-source.md +++ b/docs/how-to/adding-a-stream-source.md @@ -4,7 +4,7 @@ A new way for frames to reach the lights, from a capture device, a file format o Every source is one of two families, and each family has one place where its frames land. So a source is small: it fetches frames and hands them over, and everything after that is already written and tested. -> This assumes the desktop build and tests run: see [Building, running, flashing](building.md). A source is proven on the desktop before a board sees it. +The steps are the process [Add a picture or channel-data source](../moonwalk.md#add-stream-source). What follows is the reference behind it: the families, where their frames land, and the contract of each. --- @@ -86,10 +86,3 @@ A source writing from a task of its own calls `write` there, which waits out at - unit tests that write frames and check each light against what was sent, with `moondeck/run/fseq_play.py` writing reference FSEQ files and streaming them over DDP. The sink's placement on serpentine, 3D, mirrored and transposed layouts is pinned by Network Receive's tests. - ---- - -## Before the pull request - -One topic, sized and based as [the process](../contributing/principles-and-process.md) sets. -The desktop tests pass, the source works on the board it is for, and a board without the device builds and shows a status rather than failing. diff --git a/docs/how-to/backup-and-restore.md b/docs/how-to/backup-and-restore.md index 40263c9d4..ccc249c3a 100644 --- a/docs/how-to/backup-and-restore.md +++ b/docs/how-to/backup-and-restore.md @@ -1,26 +1,16 @@ -# Back up and restore a device +# Backup and restore -Every setting a device holds, saved to one file on your computer, and put back on the same device or a different one. +A backup is every setting a device holds in one `.json` bundle: the configuration of every module, your [presets](presets.md) and your MoonLive scripts. Taking one and putting it back, on the same device or another: [Back up and restore a device](../moonwalk.md#backup-restore). -Worth doing before a firmware update, before rewiring, and once a rig you care about is working the way you want. +## What a backup holds -## Take a backup - -Open the **File Manager** panel and press **Backup (⤓)**. - -The browser downloads one `.json` bundle named for the device and the date. It walks the whole filesystem, so it holds the configuration of every module, your saved [presets](presets.md), and your MoonLive scripts. - -**Keep the file private. It contains the WiFi password.** Through the access point the configuration files are left out of the bundle. - -Every file is byte-verified against the directory listing as it is read, with three outcomes. A file shorter than listed aborts the backup, because silent truncation is worse than no backup at all. A file longer than listed is binary rather than text, so it is skipped and named. A file that cannot be read is skipped and named too. +The browser walks the whole filesystem. Every file is byte-verified against the directory listing as it is read, with three outcomes. A file shorter than listed aborts the backup, because silent truncation is worse than no backup at all. A file longer than listed is binary rather than text, so it is skipped and named. A file that cannot be read is skipped and named too. One thing is left out on purpose: `/.hls`, the streaming encoder's scratch output, which is rewritten every second and would fill the report with skipped binaries. -## Put it back +## What a restore reports -Press **Restore (⟲)**, choose the bundle, and confirm twice. The second press is deliberate: restoring overwrites the device's files. - -Then read the report. It lists what needed an eye: +The report lists what needed an eye: - **Renamed and mapped**: a file, module type or control that changed name since the backup was taken, and what it became. - **Values to review**: something the mapping could not decide on its own, usually because it depends on the chip or the wiring. @@ -38,19 +28,3 @@ The device never runs migration code. Its loader is robust by design: an absent So the rename maps live in the browser, in [migrate.js](https://github.com/MoonModules/MoonLight/blob/main/src/ui/migrate.js), and apply to the bundle *before* it is uploaded. Each firmware release embeds the map as of that release, which is what lets any older backup restore correctly onto it with no version stamps anywhere. The breaks a map cannot express are written down instead, in [MIGRATING](../reference/MIGRATING.md). - -## Restoring onto a blank device - -A backup restores onto a freshly erased device, which is what makes an erase safe to do. - -Join the device's `MM-XXXX` access point: its UI opens by itself as a sign-in screen, or at `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 by itself. - -## From firmware older than the Backup button - -The [installer page](https://moonmodules.org/MoonLight/install/) offers the same backup as a bookmarklet, so a device too old to have the button can still be captured before you update it. - -## Moving a setup to another device - -A restore is how you clone a rig. Take a backup from the working device, restore onto the new one, and read the report. - -One thing to check afterwards: anything that names a pin. The configuration carries the pins the old board used, and if the new device is wired differently, the lights will be wrong in a way that looks like a broken effect. The [device name](../explanation/architecture/mooncore.md#device-name-one-identity-every-network-name-derives-from-it) comes across too, so give the clone its own. diff --git a/docs/how-to/building.md b/docs/how-to/building.md deleted file mode 100644 index 1209a510a..000000000 --- a/docs/how-to/building.md +++ /dev/null @@ -1,367 +0,0 @@ -# Building, running, flashing - -How to get the system running on a desktop, an ESP32, a Teensy, or a Raspberry Pi. Design rationale for the choices below lives in [the architecture](../explanation/architecture/index.md); coding conventions in [coding-standards.md](../contributing/coding-standards.md); what is tested in [testing.md](../reference/testing.md). - -## MoonDeck — the dev console - -Everything that builds, flashes, runs, tests, monitors, or checks the project, for every target, lives as a script under `moondeck/`. The full per-script reference is [moondeck/MoonDeck.md](../../moondeck/MoonDeck.md). - - - -The scripts have two front ends with the same code and arguments: - -- **CLI** — `uv run moondeck//.py`. What agents use; what CI uses. Composes with shell, captures exit codes, parses output. -- **MoonDeck** — `uv run moondeck/moondeck.py`, then open `http://localhost:8420`. A browser dev console wrapping the same scripts: status dots, run/stop toggles for long-running processes, grouped tabs, output panes. The human control deck. - -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 — 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: - -- **Desktop** — build, run, test. Fast iteration. -- **ESP32** — chip type and USB port selection. Build, flash, monitor. -- **Live** — device discovery and monitoring against running devices on the network. - -Script definitions and configuration live in `moondeck/moondeck_config.json` (committed). Script documentation lives in `moondeck/MoonDeck.md`, one section per script. Runtime state (selected devices, ports) persists in `moondeck/moondeck.json` (gitignored). - -## Desktop / Raspberry Pi - -Desktop and RPi both build with the root `CMakeLists.txt`. RPi can cross-compile against the same tree or build natively on the device — same source. - -```sh -uv run moondeck/build/build_desktop.py # build -uv run moondeck/run/run_desktop.py # run as detached background process -uv run moondeck/test/test_desktop.py # unit tests -``` - -Or use MoonDeck's Desktop tab for the same operations with a status dot per card. The desktop run detaches and outlives the launching script — the same model as flashing an ESP32, where the device runs independently afterwards. - -![MoonDeck Desktop tab](../assets/ui/moondeck_desktop.png) - -Each host writes into its own build dir: `build/macos/`, `build/linux/`, `build/windows/`. The per-host layout mirrors the ESP32 side's `build/esp32-/` shape — one directory per target, no cross-target clobbering on a multi-host dev machine. - -### Where the desktop keeps its settings - -A **source checkout writes to `build/fs/`** (its config under `build/fs/.config/`), recognized by `CMakeLists.txt` and `moondeck/` both being in the working directory, so a development tree stays self-contained and gitignored. The device's filesystem is that one subdirectory rather than the whole build tree, so the File Manager shows what a board shows instead of build output. Anywhere else, an installed or unzipped binary writes to the OS per-user data directory: - -| Platform | Directory | -|---|---| -| Windows | `%LOCALAPPDATA%\MoonLight` | -| macOS | `~/Library/Application Support/MoonLight` | -| Linux | `$XDG_DATA_HOME/MoonLight`, else `~/.local/share/MoonLight` | - -`MM_DATA_DIR` overrides both, which is how the test suite pins its root into the build tree rather than touching a developer's real settings. - -The distinction matters because a shipped binary is launched from a download folder or a Start-menu shortcut, where a path relative to the working directory is either unwritable or belongs to that folder rather than to the user. The root is created when the filesystem mounts, and a location that cannot be written to fails the mount and is reported once, rather than surfacing as a failed save on every change. - -### Editor setup (clangd) - -Diagnostics appear **as you type**, from the same [`.clang-tidy`](../../.clang-tidy) config CI uses. -A finding shows up while the code is fresh in your head, not ten minutes later in a pipeline. - -Once per machine: install the **clangd** extension (`llvm-vs-code-extensions.vscode-clangd`) and **disable Microsoft's C/C++ IntelliSense**, running both produces duplicated and contradictory diagnostics. Nothing else to configure: [`.clangd`](../../.clangd) at the repo root points at the compilation database, and `CMAKE_EXPORT_COMPILE_COMMANDS` (set in `CMakeLists.txt`) means any normal build refreshes it. - -Two things worth knowing: - -- **If every file reports `'cstdint' file not found`**: the build directory was configured - with a different compiler than clangd is. `.clangd`'s `--query-driver` handles the usual cases; if a new toolchain appears, add it there. This failure is loud and total, real diagnostics disappear behind it, so it is worth recognizing on sight. -- **clangd runs a subset of the CI check set**: skipping checks it considers slow (>10% - AST-build cost). That is deliberate and means the same config file is safe to share: CI remains the authority. - -### 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\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. - -Each packager skips its platform-specific format when the tool is missing (`dpkg-deb`, `makensis`) on a dev machine, and fails outright under CI, where a missing artifact would otherwise fail the release with an error naming a glob rather than the absent tool. - -### Prerequisites - -Every host needs [uv](https://docs.astral.sh/uv/), CMake 3.20+, and a C++20 compiler. - -- **macOS:** `xcode-select --install` for Clang, `brew install cmake uv`. -- **Linux:** distro packages for `cmake`, GCC 12+ / Clang 15+, and `uv` from astral's installer. -- **Windows:** Visual Studio 2022 Build Tools with the **MSVC v143** workload and **Windows 11 SDK**, plus CMake. Quickest install (run in an elevated terminal): - - ```powershell - winget install Kitware.CMake - winget install Microsoft.VisualStudio.2022.BuildTools --override "--passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" - ``` - - Build and test from a **Developer PowerShell for VS 2022** (Start Menu → "x64 Native Tools…") so `cl.exe` and the SDK paths are on `PATH`. The default CMake generator on Windows is Visual Studio multi-config, so `MoonLight.exe` lands at `build/windows/Release/MoonLight.exe` and `mm_scenarios.exe` at `build/windows/test/Release/`. `build_desktop.py` and `run_scenario.py` look in both the `Release/` subdir and the build root, so Ninja (single-config) also works if preferred. - -### Docker - -The desktop build runs in a container, which is the whole system without an ESP32: same effect pipeline, same web UI, same driver stack, driving real fixtures over Art-Net, DDP and E1.31. - -```sh -docker compose up -d # then open http://localhost:8081/ -docker compose logs -f -docker compose down # stops it; the volume, and your config, survive -``` - -Or from the published image, one per release: - -```sh -docker run -d --name moonlight -p 8081:8080 -v moonlight:/data \ - ghcr.io/moonmodules/moonlight:latest --no-browser -``` - -`--no-browser` because a container has no browser to open: without it the start prints a line saying it could not open one, which is noise rather than a failure. The Compose service passes it already. - -`:latest` follows the rolling prerelease, the same build the installer page offers; a version tag such as `:6.0.0` pins one. Images are published by the release workflow from the same `.deb` that release ships, so the image and the binary are the same build. - -**Upgrading preserves everything.** The image holds only the binary and all state lives in the -volume, so an upgrade keeps settings, presets, scripts and the device's identity. Which command depends on which source the compose file names: `docker compose pull && docker compose up -d` for a published `image:`, and `docker compose build --pull && docker compose up -d` for the shipped `build: .`, where `pull` alone would fetch nothing and `up` would rebuild from the checkout. Only `docker compose down -v` wipes it, and only a mounted volume is preserved at all: a bare `docker run` with no `-v` loses its state when the container goes. - -| | | -|---|---| -| **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 | -| **Capabilities** | none; it binds its port as an ordinary process | - -**When host networking is needed.** Unicast output to a fixture works over ordinary bridge -networking. Discovery and the broadcast or multicast output modes do not cross a bridge, so those want `network_mode: host` (or an L2 CNI on Kubernetes). With host networking there is no port mapping, so pass `--port 8081` in `command:` to stay clear of anything already on 8080. - -**Several instances** run side by side with no conflict: each container has its own port space, so -they all listen on 8080 internally with different published ports, and each generates its own identity so they are distinguishable on the network. CPU is the practical limit rather than memory (measured at ~5 MB and about one core each, since the desktop build renders as fast as it is allowed); cap it with `cpus:` in compose when running a fleet. - -**amd64 only** for now: the release ships no arm64 Linux binary. On an Apple-silicon Mac or an ARM -server the compose file's `platform: linux/amd64` runs it under emulation, which works but is slower than native. - -## ESP32 - -The ESP32 target uses ESP-IDF directly, not the Arduino framework. - -**Tested IDF version:** **v6.1** (commit `fff9895c`). CI builds against the `v6.1` Docker tag and local builds should match (clone command below). The why, the alternatives, and how to check for a newer one are in [ESP-IDF version](#esp-idf-version) below. - -### Prerequisites - -You need [uv](https://docs.astral.sh/uv/) (Python launcher), CMake 3.20+, and a C++20 compiler. Clone ESP-IDF (~2 GB) into the expected location for your OS — the build scripts search this path first via `Path.home() / "esp" / "esp-idf"`: - -**macOS / Linux:** - -```sh -git clone --depth 1 --branch v6.1 https://github.com/espressif/esp-idf.git ~/esp/esp-idf -``` - -**Windows** (PowerShell — run once with admin to enable long paths if you haven't already): - -```powershell -# IDF and its tooling have deeply nested paths; without longpaths the clone -# trips MAX_PATH (260 chars) inside the v6.1 tree. -git config --global core.longpaths true -git clone --depth 1 --branch v6.1 https://github.com/espressif/esp-idf.git "$env:USERPROFILE\esp\esp-idf" -``` - -Then run the one-time Python environment setup — either open MoonDeck (`uv run moondeck/moondeck.py`), go to the ESP32 tab, and click **Setup ESP-IDF**, or run it directly: - -```sh -uv run moondeck/build/setup_esp_idf.py # one-time -uv run moondeck/build/build_esp32.py --firmware esp32 # WiFi-only -uv run moondeck/build/flash_esp32.py --firmware esp32 --port /dev/tty.usbserial-XXXX -uv run moondeck/run/monitor_esp32.py --port /dev/tty.usbserial-XXXX -``` - -On the variants that opt into it (`esp32`, `esp32-16mb`, `esp32-wrover`, `esp32-eth`, `esp32s3-zero`, and `qemu`, which is emulated rather than installable) the build also produces -**MoonBase**, the second boot image ([MoonBase](../explanation/architecture/moonbase.md)), -and `flash_esp32.py` writes the corrected layout in one pass: app in the big `ota_0` slot, MoonBase in `factory`, and an otadata that boots the app directly. A device on the older dual-OTA table adopts this layout only through such a full serial flash, OTA never rewrites the partition table. - -`setup_esp_idf.py` runs the upstream installer for the host: `install.sh` on macOS/Linux, `install.bat` on Windows. Both create the same `~/.espressif/python_env/...` venv and download the same toolchains (~1.5 GB more) — only the wrapper differs. The Windows installer needs roughly 5 minutes on a fast link. It also offers to move a drifted checkout onto the pinned commit (see [ESP-IDF version](#esp-idf-version)); pass `--no-checkout` to keep it warn-only. - -**Building for the ESP32-S31** (a RISC-V *preview* target in v6.1) needs its toolchain fetched once — the default install only pulls the classic-`esp32` toolchains: - -```sh -(cd ~/esp/esp-idf && ./install.sh esp32s31) # one-time, adds the S31 RISC-V toolchain -``` - -Flash the S31 from the web installer or with the CLI (`flash_esp32.py --firmware esp32s31 --port `), whichever suits. Browser flashing works from esptool-js 0.7.0, which identifies the chip by its id rather than by a ROM magic value the S31 shares with the classic ESP32. - -On Windows, the `--port` argument is a `COM*` name (e.g. `COM3`) instead of `/dev/tty.usbserial-XXXX`. MoonDeck's port picker enumerates `COM*` automatically. - -The ESP32 tab in MoonDeck wraps the same steps as cards (Setup → Firmware → Build → Port → Flash → Run). The Network bar at the top is the same one shown on the Live tab — it remembers which serial port and WiFi credentials belong to the current LAN, so moving the laptop between networks doesn't require re-picking. - -![MoonDeck ESP32 tab](../assets/ui/moondeck_esp32.png) - -### 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 MoonLight bug; it's the OS. - -**How to tell what you're dealing with:** - -- **MoonDeck ESP32 tab** — click Refresh; no `COM*` in the dropdown after plugging in a board. -- **Chrome web installer** — the "Select a serial port" browser dialog says *No serial ports available*. -- **Device Manager** — the board shows under *Other devices* (yellow triangle) with the chip name (e.g. *CP2102N USB to UART Bridge Controller*) and status *Error*. -- **PowerShell** — `Get-PnpDevice | Where-Object InstanceId -match "VID_10C4|VID_1A86"` shows the device with `Status: Error` and empty `SERIALCOMM` registry (`Get-ItemProperty "HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM"`). - -**Which driver you need**, by the vendor ID (VID) in the InstanceId: - -| VID | Chip | Common boards | Driver | -|---|---|---|---| -| `1A86` | WCH CH340 / CH341 | LOLIN D32, cheap NodeMCU-ESP32 clones, some Olimex | wch.cn official CH341SER (Windows Update usually pulls it — if not, try `pnputil /scan-devices` from an elevated PowerShell first). | -| `10C4` | Silicon Labs CP2102 / CP2102N | ESP32-S3 DevKitC, ESP32-S31 CoreBoard, many newer dev kits | Silicon Labs Universal driver — [zip download](https://www.silabs.com/documents/public/software/CP210x_Universal_Windows_Driver.zip). Windows Update rarely has this one, so grab it directly. | -| `0403` | FTDI FT2232 / FT230X | Some Olimex Gateways, older ESP32-WROVER-KIT | Windows Update usually installs FTDI VCP automatically. | - -**Fastest headless install** (works for both CH340 and CP210x — Silicon Labs' zip contains an `.inf` `pnputil` can install directly): - -```powershell -# 1) Download + extract the Silicon Labs Universal driver (CP210x). -$dst = "$env:TEMP\cp210x_driver" -Invoke-WebRequest "https://www.silabs.com/documents/public/software/CP210x_Universal_Windows_Driver.zip" ` - -OutFile "$dst.zip" -Expand-Archive -Path "$dst.zip" -DestinationPath $dst -Force - -# 2) Install (elevated — accept the UAC prompt). -Start-Process powershell -Verb RunAs -ArgumentList ` - "pnputil /add-driver $dst\silabser.inf /install" -``` - -After the driver installs and Windows finishes binding (a few seconds), the boards appear as `Silicon Labs CP210x USB to UART Bridge (COM*)` with `Status: OK`. Both MoonDeck's port list (stateless registry read, no need to restart the server) and the web installer's Chrome picker populate immediately. - -**Related Windows serial gotchas — same class of issue, same page:** - -- **Stale "ghost" COM ports** from previous plug attempts (visible in Device Manager under *Show hidden devices*) reserve entries in the `ComDB` registry and can block fresh COM allocations even after replug. Clean them with `pnputil /remove-device "USB\VID_…"` (also elevated) — then replug. -- **Wedged CH340 driver from an earlier install** (`Status: Unknown` even though the driver is installed) — same pattern: `pnputil /delete-driver oem*.inf /uninstall /force` for the wch.cn driver, replug, let Windows Update supply a fresh one. -- **Cable / port** — some phone-charging cables carry only VBUS + GND (no data lines) and enumerate as briefly-then-vanish. Try a known-good cable and a rear USB-2 port before spending more time on drivers. - -### ESP-IDF version - -**Pinned to `v6.1`** (commit `fff9895c`, the GA tag). `setup_esp_idf.py` holds the exact commit in `PINNED_IDF_VERSION`, warns loudly when the installed tree differs, and by default offers to check the pin out so a stray `git pull` or a fresh shallow clone landing on a newer commit converges back rather than silently building against the wrong tree (`--no-checkout` keeps it warn-only). Minimum is ESP-IDF v5.1 (C++20 needs GCC 12+); the project uses v6.x APIs (`esp_eth_phy_new_generic`, the component manager for mDNS, the modern RMT/parlio/LCD drivers) so v5.x would need adjustments. - -**Why v6.1 and not v6.0.** The v6.x line is: **v6.0** (GA 2026-02-27) and **v6.1** (GA 2026-08-25), which is what we pin. v6.1 carries driver fixes for the newer SoCs (P4 parlio, RMT v2 on every chip) and is the earliest line carrying the `esp32s31` preview target, so v6.0 is not an option for that board. The pin is a fixed, signed tag rather than the rolling `release/v6.1` branch, so a `git pull` cannot move it under a build. Each pin move is a deliberate re-test pass, not a routine pull: the move to GA was validated by building all four ESP32 variants and running the full device scenario set on a classic, an S3, an S31 and a P4 (2026-09-29). Being on a GA tag also starts the 30-month support clock, where a pre-release carried no support guarantee at all. Tracked in [backlog](../work/future/index.md). - -**v6.0 is the floor — don't depend on anything newer than it.** Because **v6.0 stable is our fallback** if the v6.1 line proves troublesome, the firmware and build tooling must stay buildable on v6.0. The rule is generic: **use no IDF API, component, Kconfig symbol, or tool that isn't present in v6.0.** A feature that exists only on the v6.1-dev branch (or arrives in a later minor) is off-limits until v6.0 is no longer the fallback. When adopting anything new from the IDF, confirm it shipped in v6.0 first (check the v6.0 docs / release notes, not `latest`); if it's v6.1-only, it waits. - -**Explicit exceptions are allowed.** The floor is a default, not an absolute. A feature may step below it (depend on something not in v6.0) when the product owner decides so *explicitly* and the reason is documented at the point it's introduced: in the module spec, a code comment at the dependency, and the commit body. The bar is a conscious, recorded decision, not a silent drift: a floor you can consciously waive with a stated reason stays honest, whereas a rule quietly violated does not. Each such exception also narrows the v6.0 fallback (that target now needs the newer dependency too), so it states what the fallback loses. Two exceptions stand today, each scoped to one target and tracked in the [backlog](../work/future/index.md). **P4 WiFi over the C6 co-processor** needs `esp_wifi_remote` / esp-hosted, a managed component outside mainline v6.0. And the **ESP32-S31** exists as a target only from v6.1, so that board has no v6.0 to fall back to at all: the fallback covers the chips v6.0 knows about, which is every other target we ship. - -**v6.0 vs v6.1, and where the real change was.** The earthquake was **v5.x → v6.0**, not v6.0 → v6.1: - -- **v6.0** (vs v5.x): the legacy peripheral drivers, **RMT** among them, were **removed entirely**, so the LED drivers use the RMT v2, parlio and `esp_lcd` APIs (see [RMT LED](../moonmodules/light/drivers.md#rmtled)). **picolibc** replaced newlib as the default C library, and **warnings-as-errors** became the default, as our own `-Werror` is. The `CONFIG_ESP_WIFI_ENABLED` switch was dropped, forced on for WiFi SoCs, hence the `EXCLUDE_COMPONENTS` path under [Firmware variants](#firmware-variants). New are the install manager (EIM), a built-in MCP server, CMake Build System v2 (preview), `network_provisioning`, PSA Crypto, and the C5, C61, H21 and H4 chips. -- **v6.1** (vs v6.0): an ordinary minor, bugfixes and more chip maturity and incremental features on the v6.0 baseline. No second mass-removal, and the feature set froze at GA. - -**Support / EOL policy.** Each *stable* ESP-IDF release is supported for **30 months** from its GA date, split into a Service period (frequent bugfix releases, occasional regulatory features) and a Maintenance period (security and high-severity fixes only). Pre-release and dev snapshots get none of this. So pinning to a GA tag (v6.0 today, or v6.1 after 2026-07-31) is what buys the support window; riding `v6.1-dev` does not. - -**How to check for a newer version.** - -- **Latest stable + all tags:** the [releases page](https://github.com/espressif/esp-idf/releases), or from a clone: `git -C ~/esp/esp-idf fetch --tags && git -C ~/esp/esp-idf tag -l 'v6.*'`. -- **What our tree currently is:** `cat ~/esp/esp-idf/version.txt`, or `git -C ~/esp/esp-idf describe --tags`. `setup_esp_idf.py` prints this and flags drift from the pin. -- **The release schedule + EOL dates:** the upstream [`ROADMAP.md`](https://github.com/espressif/esp-idf/blob/master/ROADMAP.md) (beta/RC/GA dates per minor, and when each older minor reaches end-of-life). - -Moving to a different release is never automatic: bump `PINNED_IDF_COMMIT` / `PINNED_IDF_VERSION` in `setup_esp_idf.py` (and the `esp_idf_version` Docker tag + cache key in `.github/workflows/release.yml`), re-clone or check out the new tag, then run the full ESP32 build + hardware re-test pass before committing the bump. - -#### Adopting the v6.x ecosystem changes - -v6.0 introduced ecosystem-level changes beyond the API surface. The stance, under [§ Principles → Industry standards](../../CLAUDE.md#principles), is to **embrace these as the ESP32 standard**. If the IDF makes something the recognized way to build, install, provision, or ship, that is the path we want rather than a bespoke one we maintain alone. We adopt them **step by step**, each its own commit and hardware re-test, rather than all at once, and only after they clear the **v6.0-floor rule** above. The default is *yes, adopt*, with the burden of argument on *why not*. - -Two guardrails bound the "embrace everything" stance: - -- **Platform-generic stays intact.** These are ESP32-specific gains; none may regress Teensy or the desktop (macOS / Windows / Linux) paths, which don't use ESP-IDF at all. An IDF feature is adopted *inside* the ESP32 platform layer / build tooling, never by leaking an IDF assumption into shared `src/` or the desktop build. If embracing a v6.x feature would touch a cross-platform seam, that seam stays abstracted (the existing platform-boundary rule). -- **The v6.0 floor.** Adopt only what's in v6.0 (see the rule above), so the v6.0 fallback keeps working. - -Where we are on each. The adoption plan and per-item triggers are filed in [backlog-core § Adopting the v6.x ecosystem changes](../work/future/backlog-core.md). - -| Change | Where we are now | -|---|---| -| **EIM** (ESP-IDF Installation Manager) — the new default, cross-platform installer; Espressif says `install.sh` / `idf_tools.py` are "no longer needed" | `setup_esp_idf.py` drives the legacy `install.sh` / `install.bat`. Works, but is the *old* documented path. | -| **PSA Crypto**: legacy mbedTLS crypto APIs deprecated in favor of the PSA API | No direct exposure: we never call mbedTLS ourselves; OTA uses `esp_https_ota` + `esp_crt_bundle_attach` ([platform_esp32_ota.cpp](../../src/platform/esp32/platform_esp32_ota.cpp)), which wrap crypto internally. | -| **`network_provisioning`**: Espressif's Unified Provisioning subsystem, renamed from `wifi_provisioning` in v6.0. Transports: **BLE (GATT)** + **Wi-Fi SoftAP**. Clients: official iOS/Android apps for both, plus `esp_prov` (a Python CLI on Linux/macOS/Windows). Transport-agnostic but ships no web/serial client. | We provision over [Improv](../moonmodules/core/system.md#improv-provisioning) from the **browser** (ESP Web Tools) or a serial CLI, covering no-app onboarding. The IDF-native **phone-app + SoftAP** flow is a coverage gap rather than a duplicate: the two standards meet only on BLE and own different front-ends. | -| **CMake Build System v2** — the named successor to the current build system; technical preview in v6.0/6.1, has its own migration guide | Standard `idf.py` build (v1). Our component is a thin `idf_component_register()` wrapper, so the migration surface is small. | -| **Built-in MCP server** (`idf.py mcp-server`): lets an AI assistant drive build/flash/monitor/debug directly | Not used. Agents and humans both go through the `moondeck//*.py` layer (the uniform interface in [moondeck/MoonDeck.md](../../moondeck/MoonDeck.md)), which wraps pin-drift checks, per-firmware build dirs, and KPI collection. | - -The general rule: **anything already in v6.0 we adopt proactively** (it clears the floor), while **preview / not-yet-in-v6.0 features wait** until they are stable *and* in our floor. Each adoption is its own commit with its own hardware re-test, and none may regress the Teensy / desktop paths. - -### Firmware variants - -`build_esp32.py --firmware` selects one of the shipping variants. The key combines chip name + feature flags + (for SKU-sensitive chips) module. ("Firmware" here is the compiled binary; the physical product (deviceModel) is a separate concept: see [MoonInstaller, firmware vs deviceModel vs board](../explanation/architecture/mooninstaller.md#the-three-words).) `build_esp32.py --help` lists the full set. - -The canonical list is the **firmware registry**, [`moondeck/build/firmwares.py`](../../moondeck/build/firmwares.py). It joins the `FIRMWARES` dicts in [`build_esp32.py`](../../moondeck/build/build_esp32.py) and [`build_teensy.py`](../../moondeck/build/build_teensy.py) and tags every row with its `platform`. An ESP32 slice also keeps its sdkconfig `fragments`. Its machine-readable projection is [`mooninstaller/firmwares.json`](../../mooninstaller/firmwares.json), generated by `generate_firmwares.py` and drift-guarded by `check_firmwares.py`. The CI release matrix, the manifest loops, the Teensy builds and MoonDeck all read it, so the list lives in exactly one place. A row with `ships: false` builds from the CLI but stays out of the release matrix. - -ESP-IDF v6.x has no `CONFIG_ESP_WIFI_ENABLED` switch (the symbol is forced on for WiFi-capable SoCs), so dropping WiFi at compile time happens via `EXCLUDE_COMPONENTS` plus `MM_NO_WIFI` (set when `MM_ETH_ONLY=1`, applied in `esp32/main/CMakeLists.txt`). The `esp32-eth` variant takes this path; the default `esp32` keeps both stacks compiled in and uses the runtime cascade in `NetworkModule` (Ethernet first, WiFi fallback when no PHY responds). - -Each firmware has its own build dir at `build/esp32-/`, so all variants can coexist on disk. `build_esp32.py` points `idf.py -B` at the per-firmware dir; switching firmwares is just a different `--firmware` argument, no clean rebuild penalty. Same-firmware rebuilds stay incremental, as before. Disk usage scales with the number of firmwares built (≈100 MB each), and a future rename would orphan the old dir — clean with `moondeck/build/clean_esp32.py --firmware ` or `--all`. - -If a firmware *key* changes its feature set (e.g. the classic `esp32` collapse turned a WiFi-only key into WiFi+Ethernet), its existing build dir would otherwise keep the old `MM_NO_ETH` / `MM_ETH_ONLY` in `CMakeCache.txt` — CMake `-D` flags are written to the cache, and omitting one on a later configure does *not* clear it, so the stale value would silently build the old feature set (Ethernet stubbed out, no link, no LED, and a flash erase wouldn't help because it's a compile-time define). `build_esp32.py` guards this: before reusing a dir it compares the cached feature flags to what the firmware wants and wipes the dir for a clean reconfigure on a mismatch (printing the reason). Same-feature rebuilds are untouched, so the incremental fast path is preserved. - -Each ESP32-S3 SKU has its own firmware key because the sdkconfig fragment encodes flash size, partition table, and PSRAM mode — flashing an `n16r8` binary onto a different module (e.g. N8R2) either misaligns the partition table (boot loop) or fails PSRAM init. New SKUs become new keys (e.g. `esp32s3-n8r8`); there is no generic `esp32s3` shortcut. - -The Ethernet PHY type and pin map are runtime config, not baked into the build: each firmware carries the driver(s) its chip can host (RMII EMAC for classic/P4, W5500 SPI for S3), and `deviceModels.json` supplies the per-board PHY/pins (pushed into NetworkModule's eth controls at provision). The classic chip default is the common LAN8720 RMII wiring (reset GPIO 5, MDIO addr 0, clock GPIO 17 — e.g. the Olimex ESP32-Gateway), so a board with the same PHY but a different pinout (e.g. WT32-ETH01 with reset on GPIO 16) just needs a different `deviceModels.json` entry — no rebuild. - -`--profile` is accepted one release for migration: `--profile default` → `--firmware esp32`, `--profile eth-only` → `--firmware esp32-eth`. - -### Flashing a running device over the network - -A board already on the network is updated over HTTP, with no cable. Which route to use depends on whether the variant carries [MoonBase](../explanation/architecture/moonbase.md): a board cannot rewrite the partition it is executing from, so on a MoonBase variant the app hands over to MoonBase and MoonBase does the writing. - -**On a MoonBase variant** (`esp32`, `esp32-16mb`, `esp32s3-zero`, and any variant `build_esp32.py` -builds MoonBase alongside), it is two requests: - -```sh -# 1. the app reboots into MoonBase, with nothing staged. Back in ~4 seconds. -curl -X POST http:///api/firmware/moonbase - -# 2. MoonBase writes the app slot and reboots into it -curl --http1.1 -H "Expect:" --data-binary @build/esp32-/.bin \ - http:///api/firmware/upload -``` - -`.bin` is the app image in that build directory; [flash_esp32](../../moondeck/MoonDeck.md#flash_esp32) names it. - -The second request ends with **no HTTP status** (curl reports 000): the device reboots into the new image as the write completes, so the socket closes before a response arrives. That is success, not failure. Confirm by reading the build back: - -```sh -curl -s http:///api/modules/Firmware # the `build` control names the commit and date -``` - -MoonBase serves the same route names as the application, so a page driving an update keeps calling the same paths after the handover. It also installs unattended from a URL, which is what the UI's update button uses: `POST /api/firmware/url` with the URL as the body. `POST /api/firmware/boot-app` returns to the application without installing anything, and only boots an image that validates. - -**Without MoonBase**, the application takes the image directly on the same route, -`POST /api/firmware/upload`. It is one of only two streaming routes (`/api/file` is the other), so the body may exceed the request buffer; every other route rejects an oversized body with 413. - -Two failure modes are worth recognizing, because both look like something else: - -- **413 from `/api/firmware/upload`** on a MoonBase variant means the request reached the - APPLICATION rather than MoonBase, and the app rejected an oversized body on a route it does not stream. The device did not reboot into MoonBase, or booted back before the upload. Check with `GET /moonbase`, which MoonBase answers and the app 404s. -- **`{"error":"incomplete request body"}`** from `/api/firmware/upload` means the body did not - arrive within the read window. Send with `--http1.1 -H "Expect:"` so the transfer starts immediately instead of waiting for a `100 Continue` the device does not send. - -**A partition-table change needs a cable.** OTA writes the app, never the table, so a device on an -older layout adopts a new one only through a full serial flash (see the note under [Firmware variants](#firmware-variants)). On the 4 MB classic that migration also moves the filesystem, so the device comes back unprovisioned. - - -## Teensy - -One firmware runs on a Teensy 4.1, `teensy41`: it uses the board's own Ethernet, or on the 4.1-NE, which has none, a W5500 module, found at boot. -Pins, wiring and power are on [the Teensy 4.1 page](../reference/hardware/teensy-4-1.md). -It builds with CMake over an SDK at pinned versions, and MoonDeck's [Teensy tab](../../moondeck/MoonDeck.md#teensy-tab) wraps every step: - -```bash -uv run moondeck/build/setup_teensy.py # once: the SDK into ~/teensy -uv run moondeck/build/build_teensy.py # build/teensy41/MoonLight.hex -uv run moondeck/build/flash_teensy.py # over USB, no button press -uv run moondeck/run/monitor_teensy.py # USB serial -``` - -Settings live in 1 MB of the program flash and survive a flash and a network update. A running board also updates over the network: upload the `.bin` on the Firmware card, or `curl -H "Expect:" --data-binary @MoonLight.bin http:///api/firmware/upload`. -Each release also carries the image as `firmware-teensy41-v.hex`, for `teensy_loader_cli` or PJRC's Teensy app, and flat as a `.bin` for the network update and the web installer. - -## Pre-compilation steps - -CMake runs these automatically before compilation when their source files change: - -| Step | Source | Generated | Trigger | -|------|--------|-----------|---------| -| `build_info_gen` | `library.json` | `src/core/util/build_info.h` | `library.json` changes | -| `ui_embed` | every file in `embed_ui.cmake`'s `UI_FILES` list | `src/ui/ui_embedded.h` | any UI file changes | - -Both are defined in the root `CMakeLists.txt` (desktop) and `esp32/main/CMakeLists.txt` (ESP32). Generated files are gitignored — rebuilt on every clean build. - -## After it's running - -The system serves the web UI from its embedded HTTP server. Open `http:///` in a browser; on desktop that's typically `http://localhost:8080/`. From the UI you can change effects and modifiers, configure controls, see the 3D preview. The settings persist across reboot. - -To run the test suite or any of the checks (platform boundary, specs, KPI), see the MoonDeck reference linked above. The release-readiness gates that wrap these into a checklist live in [CLAUDE.md § The Process](../../CLAUDE.md#the-process). diff --git a/docs/how-to/control-surface.md b/docs/how-to/control-surface.md index dec68b1a0..951cda6a7 100644 --- a/docs/how-to/control-surface.md +++ b/docs/how-to/control-surface.md @@ -2,7 +2,7 @@ MoonLight's [Control](../moonmodules/core/system.md#control) card is a surface: eight switches, eight encoders, eight faders and a pad grid, each driving whatever you assign it. A control surface puts those controls under your hands. It can be a phone or tablet, a game controller, or a motorized mixing desk whose faders move when something else changes them. -> New here? Start with **[Install & first light](../gettingstarted.md)**. What follows assumes MoonLight is running and you can find it in a browser. +The steps are processes: **[Connect a control surface](../moonwalk.md#control-surface)** for a desk, a gamepad or WLED Native, and **[Control a device from a phone or tablet over OSC](../moonwalk.md#osc-control)**. Below is what they refer to: the routes, the desk behavior, the OSC addresses and troubleshooting. It assumes MoonLight is running and you can find it in a browser. @@ -13,6 +13,7 @@ MoonLight's [Control](../moonmodules/core/system.md#control) card is a surface: | What you have | Route | What it needs | |---|---|---| +| A phone, for power and brightness only | [WLED Native](#a-phone-with-wled-native) | The free WLED Native app on the same network; it drives the device rather than the surface | | A phone, a tablet or another app | [OSC](#a-phone-or-tablet-over-osc) | Open Stage Control, TouchOSC, Resolume, TouchDesigner or QLC+ on the same network | | A MIDI desk (Mackie Control, such as the X-Touch or the QCon) or a gamepad | [The browser](#a-midi-desk-or-a-gamepad-through-the-browser) | The desk or pad plugged into the computer showing MoonLight's page | | One desk for boards elsewhere in the rig | [The desktop app plus OSC](#one-desk-for-several-boards) | MoonLight running on the computer the desk is plugged into | @@ -24,12 +25,7 @@ Every route lands on the same surface, so a desk, a phone and the web UI stay in ## A MIDI desk or a gamepad, through the browser -1. In MoonLight add the service under **Services**: **Midi** for a desk, **Gamepad** for a controller. -2. Plug the desk or pad into the computer showing MoonLight's page, in Chrome, Edge or Firefox. -3. A desk: Chrome asks to use MIDI devices the first time; allow it. A pad: press any button, and the browser lists it. -4. Assign the surface's controls on the Control card, such as `fader2` to a game's paddle. - -A Mackie desk in MC mode needs no setup: faders, knobs, SELECT buttons, touch sensors, motors and lights are mapped. An Akai APC40 mkII needs one choice: set the MIDI service's `profile` to Akai APC40 mkII, and its faders, knobs, activator buttons and clip pads are mapped, with their lights. A gamepad comes with default rows for its sticks and its A button. +The **Midi** service takes a desk and the **Gamepad** service a controller, in Chrome, Edge or Firefox with the device plugged into the computer showing MoonLight's page. A Mackie desk in MC mode needs no setup: faders, knobs, SELECT buttons, touch sensors, motors and lights are mapped. An Akai APC40 mkII needs one choice: set the MIDI service's `profile` to Akai APC40 mkII, and its faders, knobs, activator buttons and clip pads are mapped, with their lights. A gamepad comes with default rows for its sticks and its A button. The Akai APC40 mkII A Microsoft Xbox Series controller @@ -76,16 +72,11 @@ With the MIDI service's `source` on USB, the desk plugs into the board instead o With `source` on network, the desk is an [RTP-MIDI](https://www.rfc-editor.org/rfc/rfc6295) session, the standard Apple's Network MIDI uses. `host` names the other end: a device sharing the desk on its USB port, a desk with a network port, or a computer sharing its desk. -- The service invites the host, and asks again every second until it answers, so either side may start first. -- With `host` empty the service waits to be invited, as a Mac's Audio MIDI Setup does when its Network window connects to the board. Windows does the same with the free rtpMIDI driver. -- The greeting, the surface and the touch handling are those of a desk on the board's own USB port. +The service invites the host and asks again every second until it answers, so either side may start first. With `host` empty the service waits to be invited, as a Mac's Audio MIDI Setup does when its Network window connects to the board. Windows does the same with the free rtpMIDI driver. The greeting, the surface and the touch handling are those of a desk on the board's own USB port. ### Sharing a desk with another board -With `source` on USB and `share` on, the board's USB desk drives nothing here: the board is a network MIDI cable, as an RTP-MIDI interface is. - -1. On the board with the desk, set `source` to USB and turn `share` on. -2. On the board the desk should drive, set `source` to network and put the first board in `host`, by address or by name: `.local`. A board waiting to be invited announces itself over Bonjour as `_apple-midi._udp`, and a Mac lists it in Audio MIDI Setup's Network window the same way. +With `source` on USB and `share` on, the board's USB desk drives nothing here: the board is a network MIDI cable, as an RTP-MIDI interface is. On the board the desk should drive, `source` is network and `host` is the first board, by address or by name: `.local`. A board waiting to be invited announces itself over Bonjour as `_apple-midi._udp`, and a Mac lists it in Audio MIDI Setup's Network window the same way. The sharing board accepts a session only while a desk is plugged in, so a desk plugged in later is greeted: the host invites again until the bridge says yes. One host at a time; a second is refused while the first holds the session. The host's status says why it was refused: `no desk`, `in use`, or `it invites` when the board it invited has a `host` of its own. @@ -95,14 +86,12 @@ Messages pass both ways unchanged. The one exception is a SysEx the desk sends, ## One desk for several boards -The desk plugs into a computer running the MoonLight desktop app, and the app passes its surface on to the boards over OSC. +The desk plugs into a computer running the MoonLight desktop app, with the desk set up on `localhost` as in the browser route, and the app passes its surface on to the boards over OSC. -1. On the desktop app, set up the desk as in the route above, on `localhost`. -2. On the desktop app: **Services → OSC**, turn on `listen` and `feedback`, and set `feedbackPort` to `9000`. -3. Pick how the surface travels: - - **A few known boards:** `addressing` `unicast`, with the boards in `hosts`: `192.168.1.139, MM-testbench-S3.local`. Every board gets its own copy, retried by WiFi. - - **Any number of boards:** `addressing` `multicast`. Every board listening on the default `group` hears it; a second rig on the same network gives its desktop and boards another `group`. -4. On each board: **Services → OSC**, turn on `listen`. +- On the desktop app, **Services → OSC** has `listen` and `feedback` on and `feedbackPort` at `9000`. +- **A few known boards:** `addressing` `unicast`, with the boards in `hosts`: `192.168.1.139, MM-testbench-S3.local`. Every board gets its own copy, retried by WiFi. +- **Any number of boards:** `addressing` `multicast`. Every board listening on the default `group` hears it; a second rig on the same network gives its desktop and boards another `group`. +- On each board, **Services → OSC** has `listen` on. Every change on the desktop's surface now reaches the boards' surfaces, a self-playing game's moving paddle included. @@ -110,66 +99,18 @@ Every change on the desktop's surface now reaches the boards' surfaces, a self-p ## A phone or tablet, over OSC -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, using a free app and one file. - -### The short version - -1. Install **[Open Stage Control](https://openstagecontrol.ammd.net/)** (free; macOS, Windows, Linux) -2. Download **[MoonLight-control-surface.json](https://github.com/MoonModules/MoonLight/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 MoonLight UI moves the faders back. - ---- - -### 1. What this gives you - -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. +Eight switches, eight knobs and eight faders on a touchscreen, moving the device in real time and following it when something else moves it. The steps are in [Control a device from a phone or tablet over OSC](../moonwalk.md#osc-control). -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. +### What this gives you -Two things make this worth the five minutes: +A **control surface** is the Control card's rows of controls on something you can put your hands on. The web UI shows it, but a mouse can only touch one control at a time. 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. - **Several at once.** Ten fingers on a touchscreen, not one mouse pointer. - **It follows the device.** Change brightness in the web UI, or recall a preset, and the fader moves to match. The surface and the device never disagree about a value. -`switch1` drives the master on/off and `fader1` drives the global brightness. The rest are wired and waiting for assignments. From the second slot on, a slot nobody assigned follows the effect enabled last, so switching a look on brings its controls to the surface. On the Control card, right-click an encoder to see what it is bound to. - ---- - -### 2. Find your device's IP address - -The surface sends to an address, so you need the one your device is on. - -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. - ---- - -### 3. Install Open Stage Control - -Download it from **[openstagecontrol.ammd.net](https://openstagecontrol.ammd.net/)**. It is free and open source, and runs on macOS, Windows and Linux. - -**On macOS the first launch needs a right-click → Open**, once. The download is unsigned, so a double-click gets refused with a warning about an unidentified developer. Right-click, choose Open, confirm, and macOS remembers. - ---- - -### 4. Get the session file +`switch1` drives the master on/off and `fader1` drives the global brightness. The rest are wired and waiting for assignments. From the second slot on, a slot nobody assigned follows the effect enabled last, so switching a look on brings its controls to the surface. On the Control card, right-click an encoder to see what it is bound to. The session file always serves the newest layout and sits beside the firmware on the [releases page](https://github.com/MoonModules/MoonLight/releases). -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 MoonLight-control-surface.json](https://github.com/MoonModules/MoonLight/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/MoonLight/releases) if you would rather find it there. - -Save it somewhere you can find again. The file does not contain your device's address, so the same file works for every device you own. - ---- - -### 5. Point it at your device +### The launcher Open Stage Control opens a **launcher** first, a settings window, before it draws anything. Three fields matter: @@ -177,19 +118,17 @@ Open Stage Control opens a **launcher** first, a settings window, before it draw |---|---|---| | `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 | +| `load` | the session file | the layout to draw | So a device at `192.168.1.42` gets `send` = `192.168.1.42:9000`. The two ports are different on purpose and this is the one place people go wrong: `9000` is the device's ear, `9001` is the surface's ear. They are not interchangeable. -> Leave `read-only` **off** if you want to rearrange the layout later. On to keep it as shipped. - -Press the start button. The surface appears. +Leave `read-only` **off** to rearrange the layout later; on keeps it as shipped. --- -### 6. Turn the device's side on +### The device's side In MoonLight: **Services → OSC**. @@ -197,40 +136,28 @@ In MoonLight: **Services → OSC**. |---|---|---| | `listen` | on | accept incoming OSC. Without it the device ignores the surface entirely | | `feedback` | on | answer back, so the faders follow the device | -| `feedbackPort` | `9001` | where to answer. Must match the `osc-port` from step 5 | +| `feedbackPort` | `9001` | where to answer. Must match the `osc-port` in the launcher | | `port` | `9000` | where the device listens. Matches the `send` port | Leave `hosts` empty, with `addressing` on `unicast`. Empty means "answer whoever last wrote to us", which finds your surface on its own. Fill it in only when you want feedback sent somewhere other than the thing driving it. -Move a fader. The device should react immediately. - The shipped Open Stage Control session beside MoonLight's own Control card: eight switches, eight encoders and eight faders in both --- -### 7. Use it from a phone - -This is where it gets good, and it needs no extra setup. - -Open Stage Control also serves the surface as a **web page**. While it is running, look at its console output for a line naming a port (`8080` by default). On any phone or tablet on the same network, browse to: - -```text -http://:8080 -``` +### A phone as the surface -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 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. +Open Stage Control serves the surface as a web page; its console names the port (`8080` by default). A phone or tablet on the same network browses to `http://:8080`. The computer stays the middleman: the phone talks to it, and it talks to the device. If MoonLight's own UI is on port 8080 on that machine, give Open Stage Control a different port in its launcher. --- -### 8. When you see nothing +### When the surface does nothing The surface draws fine but the device does not move, or the faders sit at zero and never follow. In rough order of likelihood: **The device is not listening.** `listen` off is the most common cause, and it is off by default. Services → OSC → `listen` on. -**Wrong IP.** Check the System card again. A device that got a new address from DHCP after a reboot is a classic one: the surface is faithfully sending to nobody. +**Wrong IP.** Check the System card. A device that got a new address from DHCP after a reboot is a classic one: the surface is faithfully sending to nobody. **The two ports are swapped.** `send` must end in `:9000`, `osc-port` must be `9001`. Swapping them produces exactly this symptom: nothing moves, nothing errors. @@ -250,17 +177,13 @@ Brightness should jump. If that works and the surface does not, the problem is o --- -### 9. One command, if you have the repo - -With a checkout, skip steps 3 to 6 entirely: +### One command, from a checkout ```sh uv run moondeck/run/run_open_stage_control.py --host 192.168.1.42 ``` -It finds the app, passes the session, the address and both ports, and runs it headless. Open **http://127.0.0.1:8088**. You still turn `listen` and `feedback` on at the device. - -It runs **headless**, as a web server, so a phone or another laptop reaches the surface at the URLs the launcher prints. On macOS that also sidesteps the quarantine dialog of an unsigned download. The session sends `/mm/hello` when the page loads, so every widget shows the device's real values straight away, and a browser refresh re-reads them. +It finds the app, passes the session, the address and both ports, and runs it headless as a web server, at **http://127.0.0.1:8088**, so a phone or another laptop reaches the surface at the URLs the launcher prints. On macOS that also sidesteps the quarantine dialog of an unsigned download. The session sends `/mm/hello` when the page loads, so every widget shows the device's real values straight away, and a browser refresh re-reads them. | Option | Meaning | |---|---| @@ -311,3 +234,16 @@ A device with `listen` on, on the leader's `feedbackPort`, takes every value the - **[Control card](../moonmodules/core/system.md#control)**: the surface the device owns, and what each control drives - **[MIDI and Gamepad services](../moonmodules/core/services.md#midi)**: the desk profiles, `source` and `share`, and the pad's rows - **[Control surfaces](../reference/hardware/control-surfaces.md)**: the X-Touch and QCon hardware, and Mackie Control on the wire + +--- + +## A phone, with WLED Native + +The steps are in [Connect a control surface](../moonwalk.md#control-surface). For power and brightness from your pocket there is **WLED Native**, the open-source mobile app of the WLED ecosystem. MoonLight speaks the WLED JSON API and announces itself on the network as a WLED device does, so the app finds each MoonLight device with no setup and no pairing. Each shows up as a card with a power toggle and a brightness slider. + +![MoonLight devices discovered in WLED Native](../assets/core/WLED%20Native%20discovers%20MoonLight.jpeg){ width="300" } + +It is free on the [App Store](https://apps.apple.com/us/app/wled-native/id6446207239) for iPhone and iPad, and on [Google Play](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) for Android. + +WLED Native is by **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**, who wrote both the [Android](https://github.com/Moustachauve/WLED-Android) and the [iOS](https://github.com/Moustachauve/WLED-iOS) app. Their open source is what showed exactly what the apps read, so a MoonLight device appears in them without either side knowing about the other. For everything else, the device's own interface stays at `http://.local`. + diff --git a/docs/how-to/faq.md b/docs/how-to/faq.md index 38d861ca0..e94807b5c 100644 --- a/docs/how-to/faq.md +++ b/docs/how-to/faq.md @@ -6,15 +6,15 @@ A symptom rather than a question belongs in [Troubleshooting](troubleshooting.md ## Installing -- **Which firmware does my board take?** The web installer picks it from the board you choose. See [Install and first light](../gettingstarted.md). -- **Can I run it without a board?** Yes, on macOS, Linux and Windows. See [Installing to desktop](installing-to-desktop.md). +- **Which firmware does my board take?** The web installer picks it from the board you choose. See [Install MoonLight](../moonwalk.md#install). +- **Can I run it without a board?** Yes, on macOS, Linux and Windows. See [Install MoonLight](../moonwalk.md#install). - **Which hardware do the heavy effects want?** The ones computing a field per light cost CPU, so a desktop or a small always-on machine runs them where a microcontroller cannot. See [Installing on Linux](installing-on-linux.md). -- **How do I update?** Over the air from the Firmware card, or by URL. See [Updating firmware](updating-firmware.md). +- **How do I update?** Over the air from the Firmware card, or by URL. See [Update MoonLight](../moonwalk.md#update-firmware). ## First light - **Where do I set the number of lights?** On a Layout, not on the driver. See [Layouts](../moonmodules/light/layouts.md). -- **Where do I add an effect?** Expand a Layer under Effects, then **add module**: a layer is what composites effects and holds the blend mode. See [Build your first light show](../tutorials/first-light-show.md). +- **Where do I add an effect?** Expand a Layer under Effects, then **add module**: a layer is what composites effects and holds the blend mode. See [Build your first light show](../moonwalk.md#first-light-show). - **Nothing lights up.** See [Troubleshooting](troubleshooting.md#the-lights-are-dark). ## The interface @@ -44,8 +44,8 @@ A symptom rather than a question belongs in [Troubleshooting](troubleshooting.md ## Developing - **What is MoonDeck, and do I need it?** A browser console that builds, flashes, runs, tests and checks the project, and drives the boards on your bench. Everything it offers is a script under `moondeck/`, so the CLI and the console run the same code and neither is required. See [MoonDeck](../explanation/architecture/moondeck.md). -- **Why not PlatformIO or pioarduino?** The ESP32 build is ESP-IDF-native, tracking IDF pre-releases against a pinned commit for chips like the P4 and S31, and the hot-path drivers use the vendor APIs directly. The tooling also covers far more than compile-upload-monitor. See [Building](building.md#moondeck-the-dev-console). -- **How do I start?** `uv run moondeck/moondeck.py`, then open . Build and run the desktop first: it needs no board and proves the toolchain. See [Building](building.md). +- **Why not PlatformIO or pioarduino?** The ESP32 build is ESP-IDF-native, tracking the IDF against a pinned commit for chips like the P4 and S31. The hot-path drivers use the vendor APIs directly. The tooling also covers far more than compile-upload-monitor. See [why our own scripts](../explanation/architecture/moondeck.md#why-our-own-scripts). +- **How do I start?** `uv run moondeck/moondeck.py`, then open . Build and run the desktop first: it needs no board and proves the toolchain. See [Build, run and test on the desktop](../moonwalk.md#desktop-build-test). ## Still stuck diff --git a/docs/how-to/hold-to-play.md b/docs/how-to/hold-to-play.md index e857e9926..7d2e8b6cd 100644 --- a/docs/how-to/hold-to-play.md +++ b/docs/how-to/hold-to-play.md @@ -3,7 +3,9 @@ A button that shows an effect for as long as it is held, and drops back to a standby effect when it is let go. A magician's quick-change tricks, a foot pedal on a stage, a "press for sparkle" button on a sculpture: each is the same setup, done with configuration only. -> New here? Start with **[Install & first light](../gettingstarted.md)**. What follows assumes MoonLight is running and the buttons are wired to GPIOs. +The steps are the process [Play an effect while a button is held](../moonwalk.md#hold-to-play); below are the mechanism and the same setup as one state document for the API. + +> New here? Start with **[Install MoonLight](../moonwalk.md#install)**. What follows assumes MoonLight is running and the buttons are wired to GPIOs. --- diff --git a/docs/how-to/home-automation.md b/docs/how-to/home-automation.md index 224fc3760..e5dece9fa 100644 --- a/docs/how-to/home-automation.md +++ b/docs/how-to/home-automation.md @@ -1,15 +1,15 @@ # Home automation -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. +Bring a MoonLight device into your smart home, or have it drive your smart lights: the steps are the [Bring the device into a smart home](../moonwalk.md#smart-home) process, and the reference behind them follows: the MQTT topics, the Homebridge accessory, presets in Home Assistant, the infrastructure setup and troubleshooting. The device uses the [MQTT module](../moonmodules/core/system.md#mqtt) (or, for some hubs, the built-in WLED compatibility). It exposes on/off, brightness, and color; a platform adopts it and drives those from its own app, voice assistant, and automations. Integration goes **both directions**: - **Your smart home controlling the device**: a hub such as Home Assistant or Homebridge drives its on/off, brightness and color from the hub's app 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. +The process assumes 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. -- **[Adopt in Home Assistant](#adopt-in-home-assistant)**: point the device at HA's broker; HA auto-creates the entity. Optionally bridge that entity into Apple Home via HA's HomeKit Bridge. +- **[Adopt in Home Assistant](#adopt-in-home-assistant)**: HA discovers the device, or auto-creates the entity from HA's broker. Optionally bridge that entity into Apple Home via HA's HomeKit Bridge. - **[Adopt in Homebridge](#adopt-in-homebridge)**: standalone Apple-Home-only path when HA isn't wanted: add one `mqttthing` accessory, pair in Home. - **[Drive Hue lights](#drive-hue-lights)**: point an effect at your Hue bulbs. - **[Other platforms](#other-platforms)**: Node-RED, openHAB, and other MQTT hubs reach the device through the same topics. @@ -29,7 +29,7 @@ The control surface these integrations drive is documented once: the MQTT contro MoonLight devices as 💫-marked lights in a Home Assistant dashboard -HA discovers a MoonLight device automatically over the WLED path, with no broker and 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. +HA discovers a MoonLight device automatically over the WLED path, with no broker and 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 [How HA creates the entity](#how-ha-creates-the-entity) below. The chain: @@ -38,18 +38,7 @@ Apple Home ──HAP──▶ HA (HomeKit Bridge) ──MQTT──▶ Mosquitto └── HA app / voice / automations ``` -### 1. (MQTT path only) Point the device at the broker - -The default WLED path needs none of this: skip to step 2. Do step 1 **only if you're using the opt-in MQTT path** (a broker-only / cross-subnet setup). In the device's web UI, open the **MQTT** module and set: - -- `broker`: the **HA VM's LAN IP** (not `127.0.0.1`, and not `homeassistant.local` unless the device does mDNS; the IP the HAOS console prints is safer). -- `port`: `1883`. -- `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 → `MoonLight/#`**: the device's `.../on/get`, `.../brightness/get`, `.../hsv/get`, and retained `.../name` appear. - -### 2. Let HA auto-create the entity +### How HA creates 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: @@ -62,42 +51,18 @@ The MQTT control surface (topics, HSV → palette mapping, retained state) is in **Not this:** do **not** hand-configure HA's generic *MQTT light* against `homeassistant/light//…`: that's HA's own discovery topic schema, not a control API. The device answers via the auto-discovery above. -### 3. Optional: expose to Apple Home via HomeKit Bridge +### Apple Home through HA -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. +HA can act as its own HomeKit bridge, so the light reaches Apple Home 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. It assumes Homebridge and a Mosquitto broker are running, usually together on a Pi; a Mac or PC works for a test. [Set up standalone Homebridge + Mosquitto](#set-up-standalone-homebridge-mosquitto) covers installing them. Two steps: **point the device at the broker**, then **add one accessory to Homebridge**. +Standalone Apple-Home-only path, for when HA isn't wanted. It assumes Homebridge and a Mosquitto broker are running, usually together on a Pi; a Mac or PC works for a test. [Set up standalone Homebridge + Mosquitto](#set-up-standalone-homebridge-mosquitto) covers installing them. Find the broker's LAN IP with `hostname -I` (Pi/Linux), `ipconfig getifaddr en0` (macOS), or `ipconfig` (Windows). The accessory is in [The Homebridge accessory](#the-homebridge-accessory). ``` Apple Home ──HAP──▶ Homebridge ──MQTT──▶ Mosquitto ◀──MQTT── MoonLight device ``` -### 1. Point the device at the broker - -In the device's web UI, open the **MQTT** module and set: - -- `broker`: the broker's hostname or **LAN** IP (not `127.0.0.1`, which is loopback on the *broker's* host, unreachable from the device). Find it with `hostname -I` (Pi/Linux), `ipconfig getifaddr en0` (macOS), or `ipconfig` (Windows). -- `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 'MoonLight/#' -v` shows the device's `.../on/get`, `.../brightness/get`, `.../hsv/get`, and retained `.../name`. - -### 2. Add the accessory to Homebridge - -Install the [`homebridge-mqttthing`](https://github.com/arachnetech/homebridge-mqttthing) plugin from the Config UI **Plugins** screen, or with `npm i -g homebridge-mqttthing`. Then add [the `lightbulb` accessory](#the-homebridge-accessory) to your Homebridge config, in the Config UI **Config** editor or `~/.homebridge/config.json`. Set: - -- `url`: `mqtt://:1883`. When Homebridge and the broker share a host (the usual case), `mqtt://127.0.0.1:1883` works, since here loopback *is* the broker. -- `563cfe`: replaced by your device's MAC suffix throughout the `topics`. -- `username` / `password`: matching the broker, or removed if none. - -Restart Homebridge. - -### 3. Pair it in Apple Home - -Once `mosquitto_sub` shows the device and Homebridge lists the accessory, add it to HomeKit. In the **Home** app, pick **Add Accessory → More options →** the Homebridge bridge, with the PIN Homebridge prints on start or shows in its Config UI. The device shows up as a light: on/off, brightness, and the color wheel that steps through palettes. - ## 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. It is a MoonLight **output driver** rather than a hub integration: the device talks straight to the Hue bridge over its LAN HTTP API, with no broker. @@ -106,14 +71,7 @@ Because Hue is a rate-limited HTTP hub (~10 commands/s), this is **smooth ambien **Recommended layout:** set up a **one-dimensional grid: width 1, height = the number of Hue lights** you want to control. Each pixel of that column maps to one bulb, in order, so a 1×N grid gives exactly N lights with none wasted. 1D effects such as a rainbow or a chase read naturally across the bulbs. Sizing the grid to your bulb count keeps the effect and the driver in step. -To set it up: - -1. **Add a Hue driver.** In the device's web UI pipeline, add a **Hue** driver to the top-level **Drivers** container. Enter your bridge's IP in `bridgeIp` (find it in the Hue app, or at [discovery.meethue.com](https://discovery.meethue.com)). -2. **Pair with the bridge.** Press the physical **link button** on the Hue bridge, then click the driver's **`pair`** button within ~30 seconds. The device claims an app key (stored on the driver as `appKey`): a one-time step; the status line reports `paired, N lights`. -3. **Pick what it drives.** The driver lists the bridge's color-capable, reachable bulbs and its rooms; use the `room` / `light` controls to aim the effect at all bulbs, one room, or a single light. Each selected bulb becomes one pixel of the driver's window. -4. **Run an effect.** Any effect on the layer drives the bulbs, and the global brightness and color order apply as on a strip. Brightness 0 turns a bulb off. - -*Note:* a bulb is driven when it is an "Extended color light" and reachable: a white-only bulb, a plug, or a powered-off light is skipped. For true real-time (fast) Hue, the [Hue Entertainment API](https://developers.meethue.com/develop/hue-entertainment/) (DTLS streaming) is out of scope for this driver, which targets the standard API's ambient-color sweet spot. +The driver lists the bridge's color-capable, reachable bulbs and its rooms. A bulb is driven when it is an "Extended color light" and reachable: a white-only bulb, a plug, or a powered-off light is skipped. For true real-time (fast) Hue, the [Hue Entertainment API](https://developers.meethue.com/develop/hue-entertainment/) (DTLS streaming) is out of scope for this driver, which targets the standard API's ambient-color sweet spot. ## Other platforms @@ -121,7 +79,7 @@ Home Assistant and Homebridge above are the worked examples; other home-automati **Node-RED, openHAB, voice assistants via a hub.** Anything that speaks MQTT drives the device through the [control topics](#the-mqtt-topics), with the same [Mosquitto install](#set-up-standalone-homebridge-mosquitto). -Each integration is a self-contained recipe; the shared MQTT reference lives once in [The MQTT topics](#the-mqtt-topics). +The shared MQTT reference lives once in [The MQTT topics](#the-mqtt-topics). ## The MQTT topics @@ -187,7 +145,7 @@ sudo uv run moondeck/run/run_desktop.py --port 80 ## Set up the infrastructure -The two recipes above assume the hub + broker are running. Installing them takes one section each: **Home Assistant + Mosquitto**, the recommended path with the HomeKit bridge included, or **standalone Homebridge + Mosquitto** for Apple Home alone. +[Bring the device into a smart home](../moonwalk.md#smart-home) assumes the hub is running, and for an MQTT path the broker too; Home Assistant's default WLED path needs no broker. Installing them takes one section each: **Home Assistant + Mosquitto**, the recommended path with the HomeKit bridge included, or **standalone Homebridge + Mosquitto** for Apple Home alone. ### Set up Home Assistant + Mosquitto diff --git a/docs/how-to/installing-on-linux.md b/docs/how-to/installing-on-linux.md index e3e086455..8f99d2648 100644 --- a/docs/how-to/installing-on-linux.md +++ b/docs/how-to/installing-on-linux.md @@ -2,9 +2,9 @@ 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). +Deploying is covered here. Building and developing on Linux are [MoonWalk's contributor processes](../moonwalk.md#desktop-build-test). -> Windows, with screenshots: [Installing MoonLight on a desktop](installing-to-desktop.md). Flashing a board: [Install & first light](../gettingstarted.md). +> Windows, with screenshots: [Install MoonLight](../moonwalk.md#install). Flashing a board: the same process with ESP32 picked. ## Which route applies to your machine @@ -25,32 +25,7 @@ Both architectures get a released binary, so the route below is the same one and ## Install the package -The [releases page](https://github.com/MoonModules/MoonLight/releases/latest) carries one `.deb` per architecture: `_amd64.deb` for `x86_64`, `_arm64.deb` for `aarch64`. - -On a machine with a browser, download it and install: - -```sh -sudo apt install ./moonlight_X.Y.Z_arm64.deb -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: - -```sh -arch=$(dpkg --print-architecture) -url=$(curl -fsSL https://api.github.com/repos/MoonModules/MoonLight/releases/tags/latest \ - | grep -o "https://[^\"]*_${arch}\.deb" | head -1) -curl -fsSL -o moonlight.deb "$url" && sudo apt install -y ./moonlight.deb -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. - -Open `http://:8080`. A `.tar.gz` to unpack anywhere is on the same page. - -**A package built for the wrong architecture refuses to install**, which is the failure you want: `apt` rejects it by name rather than installing something that cannot run. - -The arm64 build targets glibc 2.35, so it installs on Raspberry Pi OS Bookworm, Debian 12 and 13, Ubuntu 22.04 and later. On something older, build from source below. +Install the `.deb` as [Install MoonLight](../moonwalk.md#install) describes with Linux picked: its step holds the two lines that fetch the right file over ssh on a headless board. The package needs glibc 2.35, so Raspberry Pi OS Bookworm, Debian 12 and 13, and Ubuntu 22.04 and later. apt refuses a package for the wrong architecture by name rather than installing something that cannot run. Then open `http://:8080`. On something older than that floor, build from source below. ## Build from source @@ -75,7 +50,7 @@ ping raspberrypi.local # or NanoPi-R28S.local arp -a # everything the network has seen ``` -Your router's client list is the fallback when mDNS does not resolve. A name that never resolves means the board has no mDNS responder: Raspberry Pi OS ships one, a minimal Debian or FriendlyELEC image often does not, so `.local` fails while the IP answers. Log in by IP and fix it below. +Your router's client list is the fallback when mDNS does not resolve. A name that never resolves means the board has no mDNS responder, so `.local` fails while the IP answers. Raspberry Pi OS ships one; a minimal Debian or FriendlyELEC image often does not. Log in by IP and fix it below. ### 3. Log in @@ -85,7 +60,7 @@ ssh pi@NanoPi-R28S # FriendlyELEC Debian: user pi, password pi ssh pi@192.168.1.156 # by address, when neither name resolves ``` -**A wall of `setlocale: LC_CTYPE: cannot change locale (UTF-8)` warnings on login is harmless.** macOS sends its own `LC_CTYPE` to the board, which has no locale by that name, and bash repeats the warning per startup file. It fires before anything you run. Silence it on the board: +**A wall of `setlocale: LC_CTYPE: cannot change locale (UTF-8)` warnings on login is harmless.** macOS sends its own `LC_CTYPE` to the board, which has no locale by that name. Bash repeats the warning per startup file. It fires before anything you run. Silence it on the board: ```sh sudo apt install -y locales && sudo locale-gen en_US.UTF-8 @@ -183,19 +158,35 @@ MoonLight writes to disk only when settings change, so the card is a fine home f ## Docker -The same program, installed as a container rather than a package. One published image carries amd64 and arm64, so a board and a server pull the same tag and each gets native instructions. +The same program, installed as a container rather than a package: start it as [Install MoonLight](../moonwalk.md#install) describes with Docker picked. One published image carries amd64 and arm64, so a board and a server pull the same tag and each gets native instructions. `--restart unless-stopped` brings it back after a reboot, so Docker replaces the systemd unit above rather than needing one of its own. -```sh -docker run -d --name moonlight --network host \ - -v moonlight-data:/data --restart unless-stopped \ - ghcr.io/moonmodules/moonlight:latest -``` +**Use `--network host`.** The web UI and unicast fixture output work through ordinary port mapping (`-p 8080:8080`). mDNS discovery and Art-Net's broadcast mode are multicast, and neither crosses a bridge network. Host networking is what lets a container find boards and be found by them. Verified on a NanoPi R28S driving a ColorLight card. + +**The volume is the configuration.** `/data` holds everything you set up; without it, every `docker rm` starts you over. -Open `http://:8080`. `--restart unless-stopped` is what brings it back after a reboot, so Docker replaces the systemd unit above rather than needing one of its own. +`:latest` follows the rolling prerelease, the same build the installer offers; a version tag such as `:6.0.0` pins one. The release workflow builds each image from the `.deb` that release ships, so the image and the package are the same build. -**Use `--network host`.** The web UI and unicast fixture output work through ordinary port mapping (`-p 8080:8080`), but mDNS discovery is multicast and Art-Net's broadcast mode is too, and neither crosses a bridge network. Host networking is what lets a container find boards and be found by them. Verified on a NanoPi R28S driving a ColorLight card. +| | | +|---|---| +| **Config** | `/data/MoonLight/.config/` in the volume (`XDG_DATA_HOME=/data`) | +| **Identity** | `/data/MoonLight/.config/identity`, written on the first run | +| **Logs** | standard output, so `docker logs` | +| **Output** | Art-Net UDP 6454, DDP 4048, E1.31 5568, all outbound | +| **Capabilities** | none: it binds its port as an ordinary process | -**The volume is the configuration.** `/data` holds everything you set up; without it, every `docker rm` starts you over. +**Several instances** run side by side: each container has its own ports and writes its own identity, so they are told apart on the network. CPU is the limit rather than memory, about one core each, since the desktop build renders as fast as it may; cap it with `cpus:` when running a fleet. + +### From a source checkout, with Compose + +The repository's `docker-compose.yml` builds the image from the checkout and publishes the interface on port 8081, so it never meets a native install on 8080: + +```sh +docker compose up -d # then open http://localhost:8081/ +docker compose logs -f +docker compose down # stops it; the volume and your config stay +``` + +It passes `--no-browser`, since a container has no browser to open. To update after pulling new code, `docker compose build --pull && docker compose up -d`; for a published `image:` it is `docker compose pull && docker compose up -d`. Only `docker compose down -v` deletes the volume. Discovery and broadcast output need `network_mode: host`, which has no port mapping, so pass `--port 8081` in `command:` there. ### When the container exits at once @@ -239,10 +230,10 @@ Docker widens the target from "a Debian-family board" to "anything that runs con - **A router or firewall appliance** running OpenWrt or OPNsense with Docker: plausible where the device has the RAM, and the overlay-root note above is likely to apply. - **Kubernetes**: one pod, one volume. Multicast needs an L2 CNI rather than the default bridge. -The realistic limits are architecture and memory, not the kind of device. It needs 64-bit, arm64 or amd64, because no 32-bit image is published. Memory is rarely the binding constraint: a container rendering a 16x16 grid measures 4.5 MB on a NanoPi, and a large installation grows that by its buffers rather than by a fixed overhead. A coffee machine is not out of the question if it meets both and runs Docker, though the fixtures would have to come to it. +The realistic limits are architecture and memory, not the kind of device. It needs 64-bit, arm64 or amd64, because no 32-bit image is published. Memory is rarely the binding constraint: a container rendering a 16x16 grid measures 4.5 MB on a NanoPi. A large installation grows that by its buffers rather than by a fixed overhead. A coffee machine is not out of the question if it meets both and runs Docker, though the fixtures would have to come to it. ## Where to go next -- [Install & first light](../gettingstarted.md): the same program on an ESP32. +- [Install MoonLight](../moonwalk.md#install): the same program on an ESP32. - [How MoonLight works](../tutorials/how-moonlight-works.md): layouts, layers, effects and drivers. -- [building.md](../how-to/building.md): building, testing and packaging in depth. +- [Build, run and test on the desktop](../moonwalk.md#desktop-build-test): building, testing and packaging from source. diff --git a/docs/how-to/installing-to-desktop.md b/docs/how-to/installing-to-desktop.md deleted file mode 100644 index 1bc91e00f..000000000 --- a/docs/how-to/installing-to-desktop.md +++ /dev/null @@ -1,143 +0,0 @@ -# Installing MoonLight on a desktop - -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 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 ./moonlight_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 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. - ---- - -## 1. Download it - -Open the [web installer](https://moonmodules.org/MoonLight/install/) and set **Install to** to `This computer (Windows x64)`. The Release picker offers stable releases and `latest`, a build published on every merge to main; pick a stable one unless you want the newest unreleased changes. - -![The web installer with Windows x64 selected, and the downloaded setup.exe in the browser's Downloads panel carrying a SmartScreen warning](../assets/tutorials/windows-01-download.png) - -**Download** gives you `MoonLight-windows-x64-vX.Y.Z-setup.exe`. - -Your browser will most likely flag it straight away: *"isn't commonly downloaded. Make sure you trust … before you open it."* That is step 2, and it is expected. - -## 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. 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**: - -![The SmartScreen download dialog, with the Delete dropdown open showing Keep anyway](../assets/tutorials/windows-02-keep-anyway.png) - -Chrome puts the same choice behind the download entry's **`⋯`**, then **Keep** and **Keep anyway**. - -Worth knowing so it does not surprise you later: **this happens for every new build.** Reputation attaches to a file, not to a project, so a fresh `latest` build starts from nothing again. Only code signing changes that, and it is on the backlog. - -### If Defender quarantines it as a trojan - -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 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/MoonLight/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. - -Either way the durable route is reporting it as a false positive at [microsoft.com/wdsi/filesubmission](https://www.microsoft.com/wdsi/filesubmission). Microsoft typically clears these within days, and it fixes it for everyone. The verdict attaches to that one build's fingerprint, so a later release is judged afresh. - -## 3. Run the setup - -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 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\MoonLight`, which is why it never asks for an administrator password. It needs about 1.3 MB. Click **Install**. - -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 - -MoonLight is now in the Start menu with its own icon. Type `MoonLight` and open it: - -![The Windows Start menu showing MoonLight with its icon, and an Uninstall MoonLight entry](../assets/tutorials/windows-04-start-menu.png) - -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 MoonLight is doing, and your browser opens the interface at `http://localhost:8080/`. - -![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 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 MoonLight configured with a Game of Life layer, and the setup left it exactly as it was. That is §6. - -From here, [How MoonLight works](../tutorials/how-moonlight-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. - -## 6. Where your settings live - -Everything you change is saved automatically, in a folder that belongs to **your Windows user** rather than to the application: - -```text -%LOCALAPPDATA%\MoonLight -``` - -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%\MoonLight` by hand if you want a genuinely clean slate. - -Paste `%LOCALAPPDATA%\MoonLight` into the Explorer address bar to open it. - -## 7. Updating - -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. - -The badge only appears when a release actually ships a build for your OS, and it tracks **stable releases**. If you are running a `latest` build, it will point you at the newest stable one rather than at newer `latest` builds. - -**Nothing is lost.** Running the new setup replaces the program and leaves `%LOCALAPPDATA%\MoonLight` untouched, so your layouts, effects and drivers come back exactly as you left them. - -## 8. The zip: run it, or install it without the setup - -The [releases page](https://github.com/MoonModules/MoonLight/releases) also carries `MoonLight-windows-x64-vX.Y.Z.zip`. **Extract it first**, rather than opening the executable from inside the zip, because Windows unpacks a zip-launched program into a temporary folder it may clear at any time. - -It holds the same application plus three files, so it serves two purposes: - -**Run it in place.** Double-click `MoonLight.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-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-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-MoonLight.ps1` reverses the install and leaves your settings alone. - ---- - -## When it does not start - -| Symptom | Look at | -|---|---| -| 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 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 MoonLight is running that it could not stop. Close the console window and run the setup again. | - ---- - -## Where to go next - -- **[How MoonLight works](../tutorials/how-moonlight-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/led-drivers.md b/docs/how-to/led-drivers.md index 9b869432d..26cbb5d03 100644 --- a/docs/how-to/led-drivers.md +++ b/docs/how-to/led-drivers.md @@ -1,6 +1,6 @@ # Choosing and wiring an LED driver -Addressable LEDs (WS2812B, SK6812, WS2811) go out through one of two drivers: [RMT LED](../moonmodules/light/drivers.md#rmtled) for a few strands, and [Parallel LED](../moonmodules/light/drivers.md#parallelled) for many. Both read the same shared buffer, so switching between them, or between Parallel LED's peripherals, is a change on the Drivers card with no reflash. +Addressable LEDs (WS2812B, SK6812, WS2811) go out through one of two drivers: [RMT LED](../moonmodules/light/drivers.md#rmtled) for a few strands, and [Parallel LED](../moonmodules/light/drivers.md#parallelled) for many. Both read the same shared buffer, so switching between them, or between Parallel LED's peripherals, is a change on the Drivers card with no reflash. The steps to add one, set its pins, count and brightness and test the wire are the process [Set up a driver or service for the hardware](../moonwalk.md#driver-setup); the reference behind them follows. ## Which one diff --git a/docs/how-to/logging-an-issue.md b/docs/how-to/logging-an-issue.md index 70f46fe8d..3e3da958a 100644 --- a/docs/how-to/logging-an-issue.md +++ b/docs/how-to/logging-an-issue.md @@ -1,6 +1,6 @@ # Log an issue -Something not working? Tell us. You do not need to diagnose it, describe what you saw, and hand us the state of the device it happened on. The **`{ }`** link on every module card gives you that in one click, and it is the single most useful thing you can attach. +Something not working? Tell us. You do not need to diagnose it, describe what you saw, and hand us the state of the device it happened on. The **`{ }`** link on every module card leads you there in two clicks, and it is the single most useful thing you can attach. 👉 **[Open an issue on GitHub](https://github.com/MoonModules/MoonLight/issues)** @@ -42,9 +42,8 @@ Actual behavior: **How to get it:** -1. Find the card that misbehaved, the effect that went dark, the driver that - isn't lighting, the network card that won't connect -2. Click the **`{ }`** link at the end of its title row +1. Find the card that misbehaved, the effect that went dark, the driver that isn't lighting, the network card that won't connect +2. Click the **`{ }`** link at the end of its title row, then **diagnostics** in the popup that opens 3. Select all in the new tab (Ctrl/Cmd+A), copy, and paste it into the issue **Which cards to include:** diff --git a/docs/how-to/multi-board-installation.md b/docs/how-to/multi-board-installation.md index 142e2dc54..c0b1146bb 100644 --- a/docs/how-to/multi-board-installation.md +++ b/docs/how-to/multi-board-installation.md @@ -15,9 +15,7 @@ A handful of small computers run it together on MoonLight, free lighting softwar --- -An installation is several MoonLight devices that act as one piece: one creature, one sculpture, one room. -The worked example is the StadBeest, whose legs, eyes and mouth run on devices of their own. -Each section names a choice that makes it work; borrow what fits your own piece. +This is the case study of one multi-device installation. The steps for your own are in [Run a multi-device installation](../moonwalk.md#multi-device); the StadBeest shows how one installation made each choice, with its legs, eyes and mouth on devices of their own. > **Start simple.** The StadBeest shows most of what MoonLight can do in one piece, so it can look overwhelmingly complex. > An installation needs far less to act as one piece: @@ -28,7 +26,7 @@ Each section names a choice that makes it work; borrow what fits your own piece. > > Start with the first, see it run, and add the next step only when you miss it. The autopilot, the desk, the looks and the mouth came last, one at a time. -> New here? Start with **[Install & first light](../gettingstarted.md)** and **[your first script](../tutorials/first-script.md)**. What follows assumes devices that run MoonLight on one network. +> New here? Start with **[Install MoonLight](../moonwalk.md#install)** and **[your first script](../tutorials/first-script.md)**. What follows assumes devices that run MoonLight on one network. --- @@ -36,10 +34,10 @@ Each section names a choice that makes it work; borrow what fits your own piece. | part | hardware | lights | layout | effect | |---|---|---|---|---| -| legs | LightCrafter 16 (ESP32-S3 N8R8) | 10 SK6812 RGBW strips of 144 | Tubes, `tubeDistance` 7 | [`stadbeest-walk.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-walk.mle), [`stadbeest-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-orb.mle), [`stadbeest-thrusters.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-thrusters.mle) | -| left eye | ESP32-S3-Zero, 4 MB | a 241-light ring disc | Rings241 | [`stadbeest-frog-eye.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-frog-eye.mle), [`stadbeest-eye-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-eye-orb.mle), [`stadbeest-sauron.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-sauron.mle) | +| legs | LightCrafter 16 (ESP32-S3 N8R8) | 10 SK6812 RGBW strips of 144 | Tubes, `tubeDistance` 7 | [`stadbeest-walk.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-walk.mle), [`stadbeest-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-orb.mle), [`stadbeest-thrusters.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-thrusters.mle) | +| left eye | ESP32-S3-Zero, 4 MB | a 241-light ring disc | Rings241 | [`stadbeest-frog-eye.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-frog-eye.mle), [`stadbeest-eye-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-eye-orb.mle), [`stadbeest-sauron.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-sauron.mle) | | right eye | ESP32-S3-Zero, 4 MB | a 241-light ring disc | Rings241 | the same three as the left eye | -| mouth | ESP32-C3 RGBWW PWM bulb | one RGBWW light | Grid 1×1×1 | [`stadbeest-mouth.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-mouth.mle) | +| mouth | ESP32-C3 RGBWW PWM bulb | one RGBWW light | Grid 1×1×1 | [`stadbeest-mouth.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-mouth.mle) | The three looks, as each device's preview shows them; the mouth, one light, breathes along with each. @@ -141,28 +139,13 @@ So they look the same way at the same moment, with no message between them. **Use them yourself.** The six looks and the autopilot are part of MoonLight's [script library](https://github.com/MoonModules/MoonLight/tree/main/moonlive). Every device running MoonLight lists them in its script picker and downloads one when it is chosen. -The full code: [`stadbeest-walk.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-walk.mle), [`stadbeest-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-orb.mle), [`stadbeest-thrusters.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-thrusters.mle), [`stadbeest-frog-eye.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-frog-eye.mle), [`stadbeest-eye-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-eye-orb.mle), [`stadbeest-sauron.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest-sauron.mle) and [`stadbeest-autopilot.mls`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/services/stadbeest-autopilot.mls); to write your own, start with [your first script](../tutorials/first-script.md). +The full code: [`stadbeest-walk.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-walk.mle), [`stadbeest-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-orb.mle), [`stadbeest-thrusters.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-thrusters.mle), [`stadbeest-frog-eye.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-frog-eye.mle), [`stadbeest-eye-orb.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-eye-orb.mle), [`stadbeest-sauron.mle`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/effects/stadbeest/stadbeest-sauron.mle) and [`stadbeest-autopilot.mls`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/services/stadbeest-autopilot.mls); to write your own, start with [your first script](../tutorials/first-script.md). --- -## 3. Iterate on the device +## 3. One surface layout for every device -A script is a file, so the fastest loop is to edit it where it runs. - -1. Write it to the device: `curl -X POST -H "Expect:" --data-binary @walk.mle "http:///api/file?path=/moonlive/stadbeest-walk.mle"`. -2. Read the effect's status on its card or in `/api/state`: a compile error shows there with its position. -3. Judge it on the real lights, then change one thing. - -A file in `/moonlive` shadows the factory script of the same name, so the device runs your copy until you delete it ([scripts on a device](../reference/integrating.md)). -A script that does not compile leaves its effect dark, so keep the last working version at hand and upload it back before you walk away. - ---- - -## 4. One surface layout for every device - -Each device's [Control](../moonmodules/core/system.md#control) card is a desk of eight faders, eight encoders and eight switches. -The first slot of each bank is the device's own. -From the second on the desk follows the look that runs: automap puts that effect's controls on the slots in the order it declares them. +The first slot of each bank on a [Control](../moonmodules/core/system.md#control) card is the device's own; from the second on, automap puts the running look's controls on the slots in declaration order. The legs device's Control card: each slot labeled after the control it drives, such as pulse, bpm, swing, lift and run @@ -189,11 +172,10 @@ A slot assigned by hand keeps its target, which is how the look switches and the A slot holds the value of the control it drives, so `fader2` at 33 means `bpm` 33. Each look is an effect of its own on every device, named after its script without the `stadbeest-` prefix, and `switch3` to `switch5` turn them on and off through their `enabled`. So one switch changes the look on the legs and on both eyes at once, and the desk follows it. -Assign a slot by hand from the Control card. --- -## 5. Linking the devices with OSC +## 4. Linking the devices with OSC The [OSC](../moonmodules/core/services.md#osc) service mirrors a surface to the network, which is all the linking an installation needs. @@ -220,7 +202,7 @@ flowchart LR class desk,bridge side ``` -The leader owns the state, and the eyes take it over OSC. The desk is the leader's own, over the network rather than a cable: [a MIDI bridge](#8-driving-it-by-hand-an-akai-apc40-mkii). +The leader owns the state, and the eyes take it over OSC. The desk is the leader's own, over the network rather than a cable: [a MIDI bridge](#7-driving-it-by-hand-an-akai-apc40-mkii). The leader's OSC card: listen, feedback on, multicast, group 239.255.77.78, feedbackPort 9001 A follower's OSC card: listen on port 9001, feedback off @@ -231,26 +213,17 @@ Every slot that changes on the leader, from its card, a preset, a script or a de A follower joins the group, writes each message onto its own slot N, and its assignments carry the value to its own controls. The leader's preset pad states go out on an address of their own, which a follower ignores, so its own presets stay untouched. -Three details matter: - -- The followers listen on 9001 and the leader on 9000, so the leader never hears its own feedback, and a follower never answers back. -- A follower takes a value when the leader's slot changes, and the leader sends every slot again every 30 seconds. A value lost on WiFi, or a follower that rebooted, is repaired within that time. -- WiFi multicast drops packets that arrive in a burst, so the leader sends that refresh one slot at a time. - A writer on the leader spreads its own changes the same way. - --- -## 6. One source of music +## 5. One source of music -Every device runs the [Audio](../moonmodules/core/services.md#audio) service in receive mode, and one machine sends WLED audio sync to all of them. -That machine is a device with a microphone, or a computer running MoonLight that captures what it plays. One source means one beat: the legs step, the eyes dart and every flash lands on the same moment. -Here the source is an [ESP32-S31 CoreBoard](../reference/hardware/esp32-s31-coreboard.md) listening with its on-board microphone, wired by Ethernet to the installation's router: [the network](#10-the-network). +Every device runs the [Audio](../moonmodules/core/services.md#audio) service in receive mode. The source is an [ESP32-S31 CoreBoard](../reference/hardware/esp32-s31-coreboard.md) listening with its on-board microphone, wired by Ethernet to the installation's router: [the network](#8-the-network). --- -## 7. Running unattended +## 6. Running unattended A [MoonLiveService](../moonmodules/core/services.md#moonliveservice) on the leader runs [`stadbeest-autopilot.mls`](https://github.com/MoonModules/MoonLight/blob/main/moonlive/services/stadbeest-autopilot.mls), which plays the beast through a whole museum night without anyone at the desk. @@ -269,7 +242,7 @@ A [MoonLiveService](../moonmodules/core/services.md#moonliveservice) on the lead --- -## 8. Driving it by hand: an Akai APC40 mkII +## 7. Driving it by hand: an Akai APC40 mkII A desk on the leader moves the whole installation, since its changes go out over OSC like any other. The StadBeest takes an [Akai APC40 mkII](../reference/hardware/control-surfaces.md#akai-apc40-mkii): faders, knobs and buttons for the shared slots, and a pad per preset. @@ -324,18 +297,7 @@ An APC40 mkII takes its power from USB, which this board's port does not supply, --- -## 9. Updating devices you cannot reach - -Devices inside a sculpture are hard to reach with a cable, so update them over the network and check before you send. - -- Compare the new image's size with the `partition` total on the device's Firmware card. -- A device with two app slots takes `POST /api/firmware/upload` directly. -- A device with 4 MB of flash has one app slot. `POST /api/firmware/moonbase` restarts it into MoonBase, its recovery image, which takes the upload and restarts into the new firmware ([updating firmware](updating-firmware.md)). -- Update one device, see it run, then the next. - ---- - -## 10. The network +## 8. The network The StadBeest brings its own network: a GL.iNet Slate AX (GL-AXT1800) travel router. Every device joins its WiFi, and the audio source is wired to it by Ethernet, so the beast runs the same at every venue. @@ -369,8 +331,6 @@ Two router settings keep it steady: The leader's MIDI `host` is the bridge's address, and a host setting also takes a name, but a name resolves by mDNS, which is multicast again. A device that stops hearing a group joins it again after 3 seconds of silence, so a network that drops multicast now and then recovers on its own. -For a stream on a network you do not control, unicast is the reliable choice: each copy is acknowledged and retried, where multicast goes out unacknowledged at the slowest rate. -Set the source's Audio `addressing` to `unicast` with the devices in `hosts`; the same holds for Art-Net, E1.31 and DDP from [Network Send](../moonmodules/light/drivers.md#networksend). --- diff --git a/docs/how-to/panel-cards.md b/docs/how-to/panel-cards.md index bb47c3f8a..b4e51de97 100644 --- a/docs/how-to/panel-cards.md +++ b/docs/how-to/panel-cards.md @@ -1,25 +1,8 @@ # Driving LED panels with a receiving card -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. +You bought a panel receiving card, most likely a **ColorLight** one, the family MoonLight supports. The steps are in [Drive LED panels through a receiving card](../moonwalk.md#panel-cards), with [Set the panels up in LEDVision](../moonwalk.md#panel-card-ledvision) and [Clear the v13 flicker](../moonwalk.md#panel-card-downgrade) beside it. Below is what those steps lean on: why the link matters, which board, what the settings mean, and what to check when the wall stays dark. -> New here? Start with **[Install & first light](../gettingstarted.md)**, then **[How MoonLight works](../tutorials/how-moonlight-works.md)**. What follows assumes you can find a card and change a control. - ---- - -## The short version - -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 -3. **Drivers** → press **+** → choose **PanelCard** ([§5.5](#55-add-the-driver)) -4. **`interface`** → desktop only: `en0` (macOS), `eth0` (Linux), or any part of the adapter name on Windows. Leave blank on an ESP32 ([§6](#6-from-a-desktop)) -5. **`format`** and **`firmware`** → match your card ([§7](#7-card-firmware-and-the-flicker)) -6. **brightness** → turn up BOTH the global `brightness` on the Drivers card and the PanelCard's own `localBrightness`; either one left low leaves the wall dark - -The status line on the PanelCard card tells you where you are: it should read a link speed and a packet rate. Anything else is in [§8](#8-when-it-does-not-light-up). - -**PanelCard missing from the driver list?** It is compiled only into firmwares for boards with Ethernet (S3, P4, S31), so a classic-ESP32 build does not offer it. Only the S31 is gigabit; the S3 and P4 are 100 Mbit and want a gigabit switch in between ([§2](#the-one-hardware-fact-that-decides-everything)). Check the Firmware page for the variant you are running. +> New here? Start with **[Install MoonLight](../moonwalk.md#install)**, then **[How MoonLight works](../tutorials/how-moonlight-works.md)**. --- @@ -35,15 +18,17 @@ Supported: **ColorLight 5A-75** (5A-75B and 5A-75E use the same wire format). --- +--- + ## 2. What you need | Part | Notes | |---|---| | ColorLight 5A-75 receiving card | The one this guide is about | | 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) | +| 5 V power supply | Sized from the panels' own rating: see the note on power in [§5](#5-power-wifi-and-the-status-line) | | 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 controller | An ESP32 board ([§3](#3-pick-a-board)) 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 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). | @@ -55,59 +40,11 @@ The cards have no buffering and no flow control. They latch the image when the s 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. - ---- - -## 3. What you will find here - -| Section | What it covers | -|---|---| -| [4. Set the panels up in LED Vision](#4-set-the-panels-up-in-led-vision) | Telling the card what panels it drives, before anything renders | -| [5. On an ESP32](#5-on-an-esp32) | Board, flash, wiring, layout, driver | -| [6. From a desktop](#6-from-a-desktop) | The same, on Windows, Linux, a Pi or a Mac, plus the raw-Ethernet permission each one needs | -| [7. Card firmware, and the flicker](#7-card-firmware-and-the-flicker) | The v13 defect, and the LED Upgrade 4.0 downgrade that clears it | -| [8. When it does not light up](#8-when-it-does-not-light-up) | Symptom to cause | - -The two halves are independent: an ESP32 and a desktop drive the same card the same way, and neither is a prerequisite for the other. Pick whichever hardware you have. +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. --- -## 4. Set the panels up in LED Vision - -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 -> 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 -> project's bench. Labels and menu paths move between versions; treat the shape as right and the -> exact wording as approximate. - -1. Connect the card to the machine's Ethernet port, and power the panels and the card. -2. Open LEDVision and go to the **receiving-card** setup (usually `Settings` then a receiving-card - or `Screen` panel; some versions ask for a password, commonly `168`). -3. Load the panel definition. Either pick your panel from the built-in module list, or load the - `.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 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 MoonLight: it separates "the panels are wired and configured" from "the sender works". - ---- - -## 5. On an ESP32 - -### 5.1 Pick a board +## 3. Pick a board | Board | Link | Verdict | |---|---|---| @@ -119,45 +56,19 @@ Other ESP32 variants do not ship panel-card support: their Ethernet is 100 Mbit The S3 needs a **W5500 Ethernet module** wired to its SPI pins, which the S3 firmwares already support; its pins come from the board entry in `deviceModels.json`. Because W5500 is 100 Mbit and sits behind SPI, it is the configuration most likely to need the gigabit switch described in [§2](#the-one-hardware-fact-that-decides-everything). -### 5.2 Flash it - -Use the [web installer](https://moonmodules.org/MoonLight/install/), or from a checkout: - -```sh -uv run moondeck/build/flash_esp32.py --firmware --port -``` - -Panel-card support is compiled in per firmware, and it is already on for the boards in the table above. - -### 5.3 Wire it - -1. **Controller → card.** Ethernet cable from the board to the card's *input* (some cards have two RJ45 ports; the second is for daisy-chaining to the next cabinet). -2. **Card → panels.** HUB75 ribbons from the card's outputs to the panels, in the order you intend to address them. -3. **Power.** 5 V to the panels *and* to the card. - -**On powering panels from the same supply as the board.** This works, and plenty of small setups run -that way. What it costs you is headroom. +The panel-card firmware is already on for these boards. A classic-ESP32 build does not offer the driver at all. -HUB75 panels draw far less than a naive count suggests, and the reason is **multiplexing**: the panel lights one group of scan rows at a time, cycling fast enough to look continuous, so at 1/16 or 1/32 scan only a fraction of the LEDs are on at any instant. This is why these panels have a reputation for modest consumption. Size the supply from the rating on your panel's own datasheet rather than from pixels multiplied by LED current. - -The failure when you do run short is not a clean one: the 5 V rail sags, and a sagging rail shows up as flicker, color shifts, or the controller resetting mid-frame. None of those look like a power problem, which is why they cost an evening. A wall that is stable at 30% brightness and misbehaves at 100% is telling you this is the problem, not the network. - -Nothing needs an IP address: the panel link is below IP entirely. - -**That is also how you reach the UI.** The board's Ethernet port is now carrying panel data, so -WiFi is what serves the web interface. Leave WiFi configured as normal; it is unrelated to the panel link and the two do not interfere. On the P4 this decides which firmware to flash, because the `-eth` variant has no WiFi compiled in at all: use `-eth-wifi`. The S31 and S3 firmwares carry both already. +--- -### 5.4 Describe the wall +## 4. Describe the wall 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; MoonLight owns the 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 [LEDVision](../moonwalk.md#panel-card-ledvision). 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 MoonLight does. -**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. +**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 | |---|---| @@ -166,23 +77,23 @@ the ordering: addressable panels wired as one long pixel strip, where the strip | `wiringOrder`, `X++`, `Y++`, `snake` | How pixels run *within* a panel | | `wiringOrderP`, `X++P`, `Y++P`, `snakeP` | How the panels themselves are ordered | -**Do HUB75 panels snake?** Not in the sense these controls mean. A HUB75 panel is addressed by row -and column over the ribbon, so its internal pixel order is fixed by the panel's own driver ICs and is not something a layout re-maps. A *chain* of panels can be arranged in any order, including a serpentine one, but that is configured on the card, not here. So if you are driving panels through a receiving card and every other row looks reversed, the setting to revisit is in LEDVision. +**Do HUB75 panels snake?** Not in the sense these controls mean. A HUB75 panel is addressed by row and column over the ribbon, so its internal pixel order is fixed by the panel's own driver ICs and is not something a layout re-maps. A *chain* of panels can be arranged in any order, including a serpentine one, but that is configured on the card, not here. So if you are driving panels through a receiving card and every other row looks reversed, the setting to revisit is in LEDVision. -### 5.5 Add the driver +The `interface` control is ignored on an ESP32, which has one MAC. `fps` of 40 is a good start (1-120), and `start` and `count` stay at their defaults unless one picture is split across several controllers. -Add a **Panel Card** driver under Drivers, and set: +--- -| Control | Set it to | -|---|---| -| `format` | `ColorLight 5A-75` | -| `interface` | **Leave blank**: an ESP32 has one MAC, so this is ignored | -| `fps` | 40 is a good start (1–120) | -| `start` / `count` | Leave at defaults unless you are splitting one picture across several controllers | +## 5. Power, WiFi and the status line -### 5.6 What you should see +**On powering panels from the same supply as the board.** This works, and plenty of small setups run that way. What it costs you is headroom. -The driver's status line tells you what the wire is doing, and the three states are worth recognizing: +HUB75 panels draw far less than a naive count suggests, and the reason is **multiplexing**: the panel lights one group of scan rows at a time, cycling fast enough to look continuous, so at 1/16 or 1/32 scan only a fraction of the LEDs are on at any instant. This is why these panels have a reputation for modest consumption. Size the supply from the rating on your panel's own datasheet rather than from pixels multiplied by LED current. + +The failure when you do run short is not a clean one: the 5 V rail sags, and a sagging rail shows up as flicker, color shifts, or the controller resetting mid-frame. None of those look like a power problem, which is why they cost an evening. A wall that is stable at 30% brightness and misbehaves at 100% is telling you this is the problem, not the network. + +**That is also how you reach the UI.** The board's Ethernet port is now carrying panel data, so WiFi is what serves the web interface. Leave WiFi configured as normal; it is unrelated to the panel link and the two do not interfere. On the P4 this decides which firmware to flash, because the `-eth` variant has no WiFi compiled in at all: use `-eth-wifi`. The S31 and S3 firmwares carry both already. + +**The status line** tells you what the wire is doing: | Status | Meaning | |---|---| @@ -190,58 +101,25 @@ The driver's status line tells you what the wire is doing, and the three states | `100 Mbit - …` (warning) | Sending, but see [§2](#the-one-hardware-fact-that-decides-everything): fine for a small wall, tearing on a large one. | | `no ethernet link` | Cable, card power, or the wrong port on the card. | -If the status is healthy and the panels are dark, jump to [§7](#7-card-firmware-and-the-flicker). - --- ## 6. From a desktop -A PC, a Mac or a Raspberry Pi can drive the same card. Reasons to want this: far more compute for heavy effects, and a machine you already own. - -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, 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. +A PC, a Mac or a Raspberry Pi drives the same card with the same Layout and driver, for far more compute. Two things differ. `interface` must name the NIC the frames leave from. Raw Ethernet needs permission: sending below IP is privileged on every desktop OS, and without it 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 -### 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, which is why Wireshark bundles a driver and why LEDVision needs one. [Npcap](https://npcap.com/) is the maintained choice; legacy WinPcap 4.1.3 also works and is what this driver was measured against. MoonLight loads it at run time, so it installs without either, and the driver says so when you bind with none. -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. - -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 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 MoonLight as Administrator. +Windows names its capture devices `\Device\NPF_{…GUID…}`, so MoonLight matches your `interface` text against the adapter description instead. The full device name also works. ### 6.2 Linux, including Raspberry Pi -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 ./MoonLight -``` - -For `interface`, use the kernel's name exactly: `eth0`, `enp3s0`. `ip link` lists them. +Raw frames go out over `AF_PACKET`, which needs `CAP_NET_RAW`. The kernel name goes in `interface`. ### 6.3 macOS -Raw frames go out over BPF (`/dev/bpf*`), which is root-only by default. - -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. - -> On an Apple Silicon or Intel Mac the built-in port is usually gigabit; a USB-C dongle may not be. Check the driver's status line rather than assuming. - -### 6.4 Then, on any desktop - -Set up the layout and the **Panel Card** driver exactly as in [§5.4](#54-describe-the-wall) and [§5.5](#55-add-the-driver), with `interface` filled in per your OS. The status line means the same things. +Raw frames go out over BPF (`/dev/bpf*`), root-only by default. Wireshark's **ChmodBPF** helper is the tidier option for regular use. On an Apple Silicon or Intel Mac the built-in port is usually gigabit and a USB-C dongle may not be, so check the status line rather than assuming. --- @@ -264,31 +142,11 @@ Two different faults look like "flicker", and only one of them is this. Tell the 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 - -**Use [LEDUpgrade](https://en.colorlightinside.com/product/download/383) 4.0 and firmware 11.09.** -That is the proven combination, and the easiest one, because 11.09 ships inside LEDUpgrade 4.0: it is in the preset list, so there is no firmware file to find. Treat any other pairing as a detour. - -**Why not 5.0.** Version 5.0 ships no pre-v12 firmware at all, so it cannot do this downgrade from -its preset list however long you fight it. - -**Why 11.09 rather than something older.** Anything before v12 clears the flicker, but older is not -automatically safer: cards on 11.08 were reported strobing white, which 11.09 fixes. 11.09 is the newest build on the safe side of the defect, so it carries the most fixes while carrying none of the flicker. +### Which firmware to move to -> **On Windows, if LEDUpgrade cannot find the card.** A built-in Ethernet port can be held by -> 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: MoonLight's own -> sending works from a built-in port. +**Use [LEDUpgrade](https://en.colorlightinside.com/product/download/383) 4.0 and firmware 11.09.** 11.09 ships inside LEDUpgrade 4.0, so there is no firmware file to find. Version 5.0 ships no pre-v12 firmware, so it cannot do this downgrade. Anything before v12 clears the flicker, but 11.08 was reported strobing white, which 11.09 fixes. 11.09 is the newest build on the safe side of the defect. The steps: [Clear the v13 flicker](../moonwalk.md#panel-card-downgrade). -1. Connect the card **directly** to the machine, no switch in between. -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. +> **On Windows, if LEDUpgrade cannot find the card.** A built-in Ethernet port can be held by 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. A **USB gigabit Ethernet dongle** is the reliable way through. MoonLight's own sending works from a built-in port. ### Then tell MoonLight what it is talking to @@ -299,7 +157,7 @@ Set the driver's `firmware` control to match the card: | `v12 and older` | **The default**, and a downgraded card. Brightness and sync go out once. | | `v13 and newer` | A stock card. Both go out twice, which is the copy this firmware acts on. | -The default is the downgraded generation on purpose: this page's own advice is to move a v13 card off it, so the setting is already right when you finish rather than being one last unexplained step. +The default is the downgraded generation on purpose: a v13 card is best moved off v13, so the setting is already right when you finish rather than being one last unexplained step. The mismatch is not subtle in one direction: leave a downgraded card on `v13 and newer` and it receives a second sync, treats it as another latch, aborts the refresh already running, and the wall updates once every few seconds. @@ -314,7 +172,7 @@ The mismatch is not subtle in one direction: leave a downgraded card on `v13 and | Image tears or rolls | Link speed ([§2](#the-one-hardware-fact-that-decides-everything)). Check the status line says 1000 Mbit. | | A new frame only every few seconds | `firmware` ([§7](#7-card-firmware-and-the-flicker)) is set to `v13 and newer` on a card running v12 or older. The duplicate sync latches twice and aborts the refresh. | | Flicker in time with network activity | The card's own v13 firmware defect ([§7](#7-card-firmware-and-the-flicker)), not the sender. Downgrade the card. | -| Every other row or column mirrored | With a receiving card, this is the card's own panel configuration: revisit it in LEDVision ([§4](#4-set-the-panels-up-in-led-vision)). The layout's `snake` / `snakeP` apply only to walls wired as a pixel strip. | +| Every other row or column mirrored | With a receiving card, this is the card's own panel configuration: revisit it in LEDVision ([LEDVision](../moonwalk.md#panel-card-ledvision)). The layout's `snake` / `snakeP` apply only to walls wired as a pixel strip. | | Panels in the wrong places | Same: the chain order is set on the card. | | Right image, wrong colors | `fixture` on the driver, which is where channel order and RGBW synthesis live for every driver. There is no separate color-order control here. | | Works on ESP32, not on desktop | Permission ([§6](#6-from-a-desktop)). The driver shows a warning that says so. | diff --git a/docs/how-to/presets.md b/docs/how-to/presets.md index e358b6248..cba45aab9 100644 --- a/docs/how-to/presets.md +++ b/docs/how-to/presets.md @@ -1,30 +1,9 @@ -# Save and recall presets +# Presets -A preset is a saved state you can bring back with one click: a look you liked, a geometry you wired, a hardware setup you got right. - -They live on the Control card as a grid of 64 pads. Click one to apply it. +A preset is a saved state brought back with one click: a look you liked, a geometry you wired, a hardware setup you got right. They live on the Control card as a grid of 64 pads. Saving, applying and changing one: [Save, edit and apply a preset](../moonwalk.md#presets). The gallery: [install from it](../moonwalk.md#gallery-install) and [share in it](../moonwalk.md#gallery-share). > **Two different things are called a preset.** The pads on the Control card are what people usually mean, and what follows is about those. The **fixture profile** under Drivers names which channel carries red or pan: see [FixtureProfiles](../moonmodules/light/supporting.md#fixtureprofiles). -## Save one - -Right-click a pad, or press and hold it on a touchscreen. The pad editor opens. - -Type a name, choose which container it saves, and press **save current state here**. The name, pad and source in the editor are input for the next save, not configuration, so none of them is written to flash. - -Any card saves itself as well: press its `{ }` button, name the preset, and press **save as preset**. It lands on the first free pad and holds that card alone, so applying it puts back that card and leaves its neighbors running. - -## Edit, rename or delete one - -A click applies a pad; everything else starts from the same editor. Right-click a pad that holds a preset, or press and hold it on a touchscreen. A pad's tooltip says the same. - -- **Change what it holds:** apply it, change what you want on the cards, then open its editor and press **save current state over it**. The radio buttons choose which container the new contents carry. -- **Rename it:** edit **name** in the editor. The new name applies on Enter or when you leave the field. -- **Delete it:** press **delete preset** in the editor. The pad empties. -- **Move it:** drag it onto another pad, which swaps the two, or onto an empty one. - -Each preset is also a file, `/.config/presets/.json`, so the File Manager edits or deletes it directly, and the pad follows the file. - ## What a preset holds A preset is a state document: the containers it sets, by name, with the modules and controls inside, the same document [`PATCH /api/state`](../reference/integrating.md#setting-everything-at-once) applies. @@ -48,35 +27,15 @@ A secret stays out of a document, so a preset never carries a password. A preset ## Applying one -Click the pad. The look, geometry or setup replaces what was there. - **A saved container is a restore, not an overlay.** It carries `"$patch": "replace"`, so a preset carrying more modules than the device has adds them, and one describing fewer removes what it omits. That is what makes a pad reliable: what you saved is what comes back, rather than what you saved merged with whatever drifted since. **One active preset per container**, so a layout preset and a look stay lit together. Applying a new look replaces only the look, and a palette preset holds the Drivers pad next to it. A pad is tinted by its container: layout blue, effects violet, driver green, service amber. A restore changes the module tree, which pauses rendering while it runs, so a large preset stalls the lights for a moment. Every removal runs before any creation, so a module can move between layers under its own name. -## From the gallery - -The **Gallery** under the pads shows what people shared in [MoonLight-Gallery](https://github.com/MoonModules/MoonLight-Gallery) as pages of thumbnails, most liked or newest first. Your browser fetches it, so the device needs no internet of its own, and an entry shows as soon as it is accepted. - -Search covers the name, the description and the author, and a chip per kind narrows the list to presets or one kind of script. Click a thumbnail to open the entry: its moving preview plays with the whole description, and prev and next step through what the search shows. - -- **add to a pad** puts a preset on the first free pad, with any script it needs that the device lacks, from the gallery or from the scripts MoonLight ships. -- **try now** applies it the same way without keeping it on a pad. -- **install** puts a script into `/moonlive`. - -A look, a palette, or a look with its palette applies at once. A preset that also sets pins or geometry asks first, since those belong to the author's rig. - -An entry made on a newer MoonLight than yours says so: update first. - -To share one, press a card's `{ }` button, copy the JSON, and open an issue in the gallery as its [Sharing your own](https://github.com/MoonModules/MoonLight-Gallery#sharing-your-own) describes. - -For its moving preview, apply the preset and press ⏺ on the preview bar. Your browser records three seconds of the preview and saves them as a GIF to attach to the issue; the device does no extra work. - ## Arranging the pads -Drag a pad to move it. A pad is a **position, not a list entry**: slot 14 stays slot 14 whether or not anything sits in it, and deleting slot 3 does not shuffle slot 4 into its place. +A pad is a **position, not a list entry**: slot 14 stays slot 14 whether or not anything sits in it, and deleting slot 3 does not shuffle slot 4 into its place. The order persists, stamped into each preset's own file, so a preset folder copied to another device brings its layout along. diff --git a/docs/how-to/troubleshooting.md b/docs/how-to/troubleshooting.md index e52bc7ded..0bff1f2af 100644 --- a/docs/how-to/troubleshooting.md +++ b/docs/how-to/troubleshooting.md @@ -2,18 +2,33 @@ Start from the symptom. Each entry says what to check first and where the detailed page is, because the fastest fix is usually eliminating the innocent half rather than guessing at the guilty one. -## The lights are dark +## A first run on a computer or in a container + +| Symptom | Look at | +|---|---| +| The download is flagged and will not open | Keeping it, on Windows: the choice hides behind the **`⋯`** and the **Delete** dropdown. See [Windows flags the download](#windows-flags-the-download). | +| A blue **"Windows protected your PC"** appears when you run it | The file reached you without the download prompt. Click **More info**, then **Run anyway**. | +| It opened and closed immediately | Run it from a terminal so the error stays on screen. | +| The browser shows nothing at `localhost:8080` | Check the console window is still open: closing it stops MoonLight. If another program 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; allow it on your private network. | +| Settings do not survive a restart | The log says `cannot use ... persistence disabled` and names the directory it tried. | +| The installer fails saying a file is in use | A copy of MoonLight is running that it could not stop. Close its console window and run the setup again. | +| The container exits at once | [When the container exits at once](installing-on-linux.md#when-the-container-exits-at-once): usually an overlay root filesystem. | + +## Windows flags the download -**Check the preview first.** If the 3D preview shows the effect running, the pipeline is fine and the problem is downstream: the driver, the wiring, or the power. If the preview is also dark, it is upstream: the layout, the effect, or the brightness. +Microsoft Defender SmartScreen judges a download by its **reputation**, built from how many people downloaded that exact file from a publisher it recognizes. MoonLight is not code-signed and every build is a new file, so its reputation is always zero and **every new build** gets the warning. It is about the certificate, not the contents; code signing is on the backlog. -Then, in order: +Occasionally Defender 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 fits a pattern the model weighs. 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. -- **Brightness** on the Drivers card. Zero is dark and looks identical to broken. -- **Is a driver added at all?** A fresh device previews without one. Real lights need a driver naming the pin or the destination. -- **Does the light count match?** A layout of 256 and a driver of 16 lights the first sixteen and nothing else. -- **Module status.** A card with a problem says so on itself. A red line names the failure. +- **The download fails** with *"Couldn't download - Download error"*: Defender stops it mid-transfer and there is no file to rescue. [Install from the zip](../moonwalk.md#install-windows-zip), a different file with a different fingerprint. +- **The file landed and was quarantined**: delete it in **Windows Security → Virus & threat protection → Protection history**, then download again from the [releases page](https://github.com/MoonModules/MoonLight/releases). Restoring it there works too, for a file you downloaded yourself moments before. + +Either way the durable route is reporting it as a false positive at [microsoft.com/wdsi/filesubmission](https://www.microsoft.com/wdsi/filesubmission). Microsoft typically clears these within days, for everyone. + +## The lights are dark -A scripted effect that fails to compile renders dark deliberately and shows the parse error on its card: see [when the compile fails](../tutorials/first-script.md#when-the-compile-fails). +Work through [Diagnose dark or flickering lights](../moonwalk.md#diagnose-lights): the preview first, since it says which half to look at, then the brightness, the driver, the light count and each card's status. ## Stray pixels, or wrong colors on lights that should be off @@ -33,7 +48,7 @@ The strip zig-zags and the layout does not know. Turn on **serpentine** on the G ## The device is missing from the network -- **It never joined.** An unprovisioned device opens its own access point named `MM-XXXX`. Join it: the WiFi card opens by itself as a sign-in screen, or at `http://4.3.2.1`, to set the credentials. +- **It never joined.** An unprovisioned device opens its own access point named `MM-XXXX`: [join your WiFi through it](../moonwalk.md#join-access-point). - **It lost its network and its access point stays closed** (`opens` is `first setup only`). Switch it on and off four times, each within a few seconds: its access point opens for that boot without a password, and every setting stays. Join it and set the new network on the WiFi card. To start again from scratch, **factory reset** on the System card deletes every setting. - **It joined but you cannot find it.** Try the IP from your router's client list before the `.local` name: mDNS fails on plenty of networks that route fine. A device with no mDNS responder is reachable by address all along. - **It was working and stopped.** Check the device is powered and the access point is not sitting between two networks. A repeater on one radio halves its throughput and drops the client side under load. @@ -46,7 +61,7 @@ The device drops preview frames rather than blocking the render loop, so this co ## An update failed -The device stays in [MoonBase](updating-firmware.md#when-the-device-boots-into-moonbase) rather than pretending to have worked, and its page offers you a retry. You cannot brick a device this way: MoonBase is never overwritten by an app update. +The device stays in [MoonBase](../moonwalk.md#update-firmware) rather than pretending to have worked, and its page offers you a retry. You cannot brick a device this way: MoonBase is never overwritten by an app update. ## The device rebooted on its own diff --git a/docs/how-to/updating-firmware.md b/docs/how-to/updating-firmware.md deleted file mode 100644 index c66dc5213..000000000 --- a/docs/how-to/updating-firmware.md +++ /dev/null @@ -1,54 +0,0 @@ -# Update the firmware - -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 MoonLight. - -## The normal route - -Open the device's **Firmware** card under System. - -It shows the version it runs, the build it came from, and which partition it lives in. Pick the image and start the install. The card reports progress as it downloads and writes, then the device reboots into the new firmware on its own. - -Where to get an image: - -- **`latest`** is the rolling build from `main`: every merged change, published continuously. What to use if you want the newest fixes and can live with the occasional rough edge. -- **A tagged release** is a version somebody decided was worth naming. Slower moving, and what to use on a rig that has to keep working. - -Both are on the [releases page](https://github.com/MoonModules/MoonLight/releases), one file per firmware variant. The filename names the variant, and the variant must match your device: an `esp32s3-n16r8` image on a classic ESP32 refuses to boot. - -The card checks for both channels and tells you when one is newer, and a stable release always wins. A device already on a `-dev` build is the only one offered the moving channel, so a stable device is never nudged toward an unreleased build. - -## On a device that carries MoonBase - -Some variants carry [MoonBase](../explanation/architecture/moonbase.md), a small recovery image in the factory slot. The 4 MB classic, `esp32-16mb` and the S3-Zero use it today. - -The card looks slightly different there. It gains an **image** selector, choosing whether the version shown and the install performed apply to the app or to MoonBase itself, and a **Restart in MoonBase** button. - -The install runs the same way from your side, behind one "updating firmware" overlay. Underneath, the device stages the URL, reboots into MoonBase, lets MoonBase write the app slot, and reboots back. A board cannot rewrite the partition it is executing from, so the two images install each other. - -What that buys you is the failure case. A power cut in the middle leaves the device in MoonBase rather than holding half an app, and MoonBase is a working page you can retry from over the network. A failed install stays there visibly instead of pretending to have worked. - -## Updating MoonBase itself - -The same card installs a newer MoonBase, writing the factory slot while the app runs. When the card warns that MoonBase does not match the app, that is the fix, and it needs no cable. - -This is the one write with a window where the device holds no recovery image. The app keeps running throughout, so the answer to a failure is to try again. - -## When the device boots into MoonBase - -It means the app did not start, or an update was interrupted. MoonBase serves its own page and offers three ways out: - -- **Boot the app**, which changes nothing and is worth trying first: it boots only an image that validates. -- **From a file**, installing a `firmware-....bin` you already downloaded. -- **From a URL**, fetching and installing in one step. Keep it under 255 characters: it crosses into MoonBase through a fixed-size slot, and a longer one is refused rather than truncated. - -If the device is not on your network, MoonBase opens the device's `MM-XXXX` access point, protected by your WiFi's password once it knows one. Joining it opens MoonBase's page by itself as a sign-in screen, or at **4.3.2.1** there. - -## Before a risky update - -Take a backup. The Firmware card does not touch your configuration, but a variant change or an erase does, and [restoring](backup-and-restore.md) takes a minute where reconfiguring a rig takes an evening. - -## Serial, when the network cannot help - -A device that will not boot far enough to serve a page needs the [web installer](https://moonmodules.org/MoonLight/install/) and a USB cable. That is the same path as a first install, and it is covered in [Install & first light](../gettingstarted.md). diff --git a/docs/index.md b/docs/index.md index ba68e50bd..9794b2776 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ High-performance LED & DMX lighting control for ESP32 and beyond. [:material-flash: Flash an ESP32 from your browser](/MoonLight/install/){ .md-button .md-button--primary }   [:material-github: GitHub](https://github.com/MoonModules/MoonLight){ .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. + [Install MoonLight](moonwalk.md#install) walks you from a blank ESP32 to a running device, step by step, with no build tools required, and [Show an effect on your lights](moonwalk.md#first-light-show) to your first light show. This documentation follows the latest pre-release; a tagged release may lack what is described here. @@ -32,7 +32,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](/MoonLight/install/) + [Install MoonLight](moonwalk.md#install) · [Web installer](/MoonLight/install/) - :material-palette: **Build a show** diff --git a/docs/legal/privacy-policy.md b/docs/legal/privacy-policy.md index 1ef8f3ebc..746c97182 100644 --- a/docs/legal/privacy-policy.md +++ b/docs/legal/privacy-policy.md @@ -1,6 +1,6 @@ # Privacy policy -**Last updated: 2026-09-10** +**Last updated: 2026-10-16** Covers the MoonLight software (firmware, desktop application and the web interface they serve), the [web installer](https://moonmodules.org/MoonLight/install/), and this documentation site. @@ -25,7 +25,7 @@ These go to GitHub under [its privacy statement](https://docs.github.com/en/site ## Your browser's local storage -The web interface keeps a few conveniences in your browser: a cached update check, your last selected release, and the last help file with your consent to fetch it. This never leaves your browser and holds nothing personal. +The web interface keeps a few conveniences in your browser: a cached update check, your last selected release, and the last help file with your consent to fetch it. MoonWalk keeps your marks, choices and view there too. This never leaves your browser and holds nothing personal. ## What you can switch on @@ -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. -**The help** is off until you turn it on with the ⓘ in the status bar, which asks the first time. Then your browser fetches one public file, the module help, from this documentation site, hosted by GitHub, and keeps it for offline use. It sends nothing about your installation. +**The help and MoonWalk** are off until you turn one on, with the ⓘ or the 🗺️ in the status bar, which asks the first time. Then your browser fetches public files. The module help and the MoonWalk map come from this documentation site, hosted by GitHub, and the map's graph library from cdnjs, run by Cloudflare. Both see your browser's address, as any website you open does. The help is kept for offline use. MoonWalk reads your device's state in your own browser to show what is done; it sends nothing about your installation. Your browser also keeps, for itself, what you marked done in MoonWalk, how you split the map and its text, and your choices such as your platform. None of it leaves the browser. **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. diff --git a/docs/moonmodules/core/ui.md b/docs/moonmodules/core/ui.md index e509ba9b7..80776f514 100644 --- a/docs/moonmodules/core/ui.md +++ b/docs/moonmodules/core/ui.md @@ -46,14 +46,12 @@ Fixed top, 44 px, left-to-right: 2. **MoonLight logo**, 28 px PNG from `/moonmodules-logo.png`; the same image is the page favicon. 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. -6. **WebSocket dot**, green = connected, gray = disconnected. The socket auto-reconnects with - exponential backoff from 200 ms to a 5 s ceiling on close, and a page reload covers the rare half-dead socket. -7. **Reboot button (⏻)**, red border via `data-crashed="true"` when `SystemModule.bootReason` is - PANIC / INT_WDT / TASK_WDT / BROWNOUT. Press-twice to confirm: the first click arms it (solid red), a second sends `POST /api/reboot`. Disarms after 3 s or on pointer-leave, no `confirm()` popup. +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. +6. **WebSocket dot**, green = connected, gray = disconnected. The socket auto-reconnects with exponential backoff from 200 ms to a 5 s ceiling on close, and a page reload covers the rare half-dead socket. +7. **Reboot button (⏻)**, red border via `data-crashed="true"` when `SystemModule.bootReason` is PANIC / INT_WDT / TASK_WDT / BROWNOUT. Press-twice to confirm: the first click arms it (solid red), a second sends `POST /api/reboot`. Disarms after 3 s or on pointer-leave, no `confirm()` popup. 8. **Help toggle (ⓘ)**, off by default: shows or hides every help line by flipping `body.help-hidden`, dimmed while off. The first time it turns help on it asks before downloading `help.json` from the docs site, and a yes is kept in `mm_helpConsent`; the choice is kept in `mm_helpShown`. -9. **Theme toggle (☀/🌙)**, flips `[data-theme]` on ``; preference in `localStorage['mm_theme']`. +9. **MoonWalk (🗺️)**: opens [MoonWalk](../../moonwalk.md), the map of every process, in a resizable dialog. It opens on the reader's choices: Here, the processes touching the card in view, and Next, the next ones to do. The map and its script come from the docs site and its graph library from cdnjs, under the help's consent, which names both hosts. The device's whole state, from `GET /api/state`, marks done what it shows done, and a `?walked=` list in the address, the installer's link after an install, records the processes the installer vouches for. With help on, a module's help lists the processes it takes part in, and a control's more names the step that sets it; a tap opens the map at that step. A step that points at a card has **Show me**: the map closes, and the card comes into view with its control outlined. A bar then holds the current step. It moves on when the device's state shows the step done, and a tap on it reopens the map. +10. **Theme toggle (☀/🌙)**, flips `[data-theme]` on ``; preference in `localStorage['mm_theme']`. ## Side navigation @@ -88,8 +86,7 @@ Each MoonModule renders as a card, with **child cards nested inside their parent - The parent's own controls render **above** its children; `+ add child` renders **below** them. Child cards live in a `.card-children` wrapper appended into the parent card's DOM node (not flat siblings); `renderModuleTree` recurses into the parent card, not into `main`. -- **`{ }`** opens `GET /api/modules/{name}` in a new tab, that one module's live JSON, for issue - reports (see [Log an issue](../../how-to/logging-an-issue.md)). On EVERY card, unlike `✎`/`×` (user-editable children only). +- **`{ }`** opens a popup holding the card as a state document, with **copy**, **save as preset** where a preset module exists, and **diagnostics**, which opens `GET /api/modules/{name}` in a new tab, that module's live JSON for issue reports (see [Log an issue](../../how-to/logging-an-issue.md)). On EVERY card, unlike `✎`/`×` (user-editable children only). - **Enabled toggle** in the right-hand action cluster mirrors `MoonModule::enabled()`, a styled ` shows every way, an ESP32, a desktop, a Linux board or a container, and what each takes.

This browser can't flash an ESP32. @@ -248,7 +249,7 @@

MoonLight Installer