Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/generated-files.yml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 3 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
12 changes: 7 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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 <string> in a unit test.
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
6 changes: 5 additions & 1 deletion .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 <page>` 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
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -136,15 +136,15 @@ 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

The developer console: one page that builds, flashes, runs, tests and monitors across every target, and discovers devices on the network.

![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

Expand All @@ -164,27 +164,23 @@ 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):

```sh
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 |
Expand Down
8 changes: 1 addition & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/assets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
15 changes: 10 additions & 5 deletions docs/assets/help-deck.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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"));
Expand All @@ -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({
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
Loading
Loading