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
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,17 @@ moondeck/moondeck.json
# on the next device refresh to link last_port → device.
moondeck/.last_flash.json

# Generated files
# Generated files. These change on every build (a git hash, a build date, a minified blob), so
# tracking them would put a diff of their own volatility in every commit.
src/ui/ui_embedded.h
src/core/build_info.h

# script_catalog.h is generated too, but it is TRACKED on purpose: it changes only when a script
# is added, removed, or edits its dimensions()/tags(), which is exactly the change worth reviewing.
# A wrong dimension or a missing emoji is one readable line in a diff, and a generator that silently
# read a declaration as "unsaid" shows up as a diff rather than as nothing at all. The scripts stay
# the source of truth; this is a checked-in view of them.

# ESP-IDF: per-board build directories live under /build/esp32-*/; legacy
# esp32/build/ is still ignored so a developer's existing tree doesn't
# show up as dirty.
Expand Down
114 changes: 85 additions & 29 deletions CLAUDE.md

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,36 @@ add_custom_command(
)
add_custom_target(ui_embed DEPENDS ${CMAKE_SOURCE_DIR}/src/ui/ui_embedded.h)

# The MoonLive script catalog: names of every factory script, so the picker can offer the whole
# library while the device holds only what someone actually picked. Globbed, so the DEPENDS is the
# directory contents rather than a file list that would drift; CONFIGURE_DEPENDS re-globs when a
# script is added or removed.
file(GLOB MOONLIVE_SCRIPTS CONFIGURE_DEPENDS
${CMAKE_SOURCE_DIR}/moonlive/effects/*.mle
${CMAKE_SOURCE_DIR}/moonlive/layouts/*.mll
${CMAKE_SOURCE_DIR}/moonlive/modifiers/*.mlm)
# The file LIST itself is a dependency, not just each file's timestamp: DEPENDS notices an edited
# script but not a DELETED one, so removing a script left it in the catalog and the device went on
# offering a name that no longer exists upstream. The stamp is written at configure time from the
# glob, so a removal changes it and the command re-runs.
string(REPLACE ";" "\n" MOONLIVE_SCRIPT_LIST "${MOONLIVE_SCRIPTS}")
set(MOONLIVE_STAMP ${CMAKE_BINARY_DIR}/moonlive_scripts.stamp)
file(CONFIGURE OUTPUT ${MOONLIVE_STAMP} CONTENT "${MOONLIVE_SCRIPT_LIST}\n")
add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/src/light/moonlive/script_catalog.h
COMMAND ${CMAKE_COMMAND} -DSCRIPT_DIR=${CMAKE_SOURCE_DIR}/moonlive -DOUT=${CMAKE_SOURCE_DIR}/src/light/moonlive/script_catalog.h -DUV_EXECUTABLE=${UV_EXECUTABLE} -P ${CMAKE_SOURCE_DIR}/src/light/moonlive/catalog_scripts.cmake
DEPENDS ${MOONLIVE_SCRIPTS} ${MOONLIVE_STAMP} ${CMAKE_SOURCE_DIR}/src/light/moonlive/catalog_scripts.cmake ${CMAKE_SOURCE_DIR}/src/light/moonlive/catalog_scripts.py
COMMENT "Generating MoonLive script catalog"
)
add_custom_target(moonlive_catalog DEPENDS ${CMAKE_SOURCE_DIR}/src/light/moonlive/script_catalog.h)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# mm_core's HttpServerModule.cpp consumes ui_embedded.h and SystemModule.h
# (a mm_core public header) consumes build_info.h. Anything that links mm_core
# transitively includes those headers, so both generated files must exist
# before any mm_core compile unit starts. CI's parallel clean build exposes
# the race (test/ compiles SystemModule.h while build_info_gen is still
# running); wiring the dep here makes the ordering explicit on every consumer.
add_dependencies(mm_core ui_embed build_info_gen)
add_dependencies(mm_core ui_embed build_info_gen moonlive_catalog)

# Windows: give the exe its own icon, so it is recognizable in Explorer, the taskbar and the Start
# menu whether it was installed or just unzipped. Generated from the same mooninstaller/favicon.png
Expand Down Expand Up @@ -243,7 +266,7 @@ endif()
# Application
add_executable(projectMM src/main.cpp src/platform/desktop/main_desktop.cpp ${MM_WIN_RESOURCES})
target_link_libraries(projectMM PRIVATE mm_core mm_platform)
add_dependencies(projectMM ui_embed build_info_gen)
add_dependencies(projectMM ui_embed build_info_gen moonlive_catalog)

# Tests
enable_testing()
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ If you like projectMM, give it a ⭐️, fork it, or open an issue or pull reque

🛠️ **ESP-IDF directly, no Arduino**: the ESP32 build is pure ESP-IDF (v6.x): native LED drivers, `esp_http_server`, FreeRTOS, built with `idf.py`, not PlatformIO or the Arduino framework. See [building.md § Why not Arduino](docs/building.md#why-not-arduino).

📦 **No third-party libraries**: no FastLED, no ESPAsyncWebServer, no ArduinoJson. The color math, the HTTP/WebSocket server, and the control storage are all in-tree. A library, when genuinely needed, lives behind the platform boundary in `src/platform/`, never in core. The full rationale + replacements: [building.md § Third-party libraries](docs/building.md#third-party-libraries).
📦 **No third-party libraries**: no FastLED, no ESPAsyncWebServer, no ArduinoJson. The color math, the HTTP/WebSocket server, and the control storage are all in-tree. A library, when genuinely needed, lives behind the platform boundary in `src/platform/`, never in core. The full rationale + replacements: [building.md § Third-party libraries](docs/building.md#third-party-libraries); why we take the trade at all: [Why we write our own code](docs/why-we-write-our-own.md).

🔬 **Industry standards, our own code**: we study the prior art hard (friend repos, peripheral datasheets, the Art-Net / E1.31 / WS2812 standards), carry its *ideas* forward, and credit it by name; but we write our own code rather than copying theirs or tracing their structure. Each feature is spec'd from the primary source, its behaviour pinned with unit + scenario tests, then written fresh against our own architecture, so the result is independent by construction, not a renamed fork. Textbook algorithm, textbook name, our implementation. The method: [CLAUDE.md § Principles](CLAUDE.md#principles).
🔬 **Industry standards, our own code**: we study the prior art hard (friend repos, peripheral datasheets, the Art-Net / E1.31 / WS2812 standards), carry its *ideas* forward, and credit it by name; but we write our own code rather than copying theirs or tracing their structure. Each feature is spec'd from the primary source, its behavior pinned with unit + scenario tests, then written fresh against our own architecture, so the result is independent by construction, not a renamed fork. Textbook algorithm, textbook name, our implementation. The method: [CLAUDE.md § Principles](CLAUDE.md#principles); how we tell good theft from bad: [Why we write our own code](docs/why-we-write-our-own.md#good-theft-and-bad-theft).

🧱 **One module model**: every effect, modifier, layout, and driver is a `MoonModule`: one base class, a uniform lifecycle, declared controls. That uniformity is why the UI renders any module with zero per-module code, and why a new capability is a new file, not a new framework. See [architecture.md § MoonModules](docs/architecture.md#moonmodules).

Expand Down Expand Up @@ -107,7 +107,7 @@ The numbers above are observations. The **contracts** projectMM commits to, what
Then open `http://localhost:8080/`. It opens by itself on start; pass `--no-browser` to suppress
that (a headless server, or a service manager), and `--port <n>` to serve somewhere else.

**Your settings live with your user, not beside the executable**, so they survive moving the app, reinstalling, and upgrading: `%LOCALAPPDATA%\projectMM` on Windows, `~/Library/Application Support/projectMM` on macOS, and `$XDG_DATA_HOME/projectMM` on Linux, falling back to `~/.local/share/projectMM` when that is unset. An uninstall leaves them in place; delete that folder to start clean. Set `MM_DATA_DIR` to put them somewhere else. Running from a source checkout keeps using `build/` instead, so a development tree stays self-contained.
**Your settings live with your user, not beside the executable**, so they survive moving the app, reinstalling, and upgrading: `%LOCALAPPDATA%\projectMM` on Windows, `~/Library/Application Support/projectMM` on macOS, and `$XDG_DATA_HOME/projectMM` on Linux, falling back to `~/.local/share/projectMM` when that is unset. An uninstall leaves them in place; delete that folder to start clean. Set `MM_DATA_DIR` to put them somewhere else. Running from a source checkout keeps using `build/fs/` instead (config under `build/fs/.config/`), so a development tree stays self-contained.

Once running, the UI lets you build a render pipeline visually (layouts → layers with effects + modifiers → drivers), preview the result in 3D, send it to Art-Net, and save it. The source tree also builds for Teensy, Raspberry Pi, and Linux from source (see [building.md](docs/building.md)), though currently only the macOS, Windows, Linux and ESP32 binaries ship as releases.

Expand Down
22 changes: 22 additions & 0 deletions docs/MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul

## Unreleased (`next-iteration`)

### The desktop build keeps its files in `build/fs`, not `build`

**Action: move your data, or lose your settings.** Affects the DESKTOP build only, and only a
developer running it from a repository checkout; devices are unaffected.

A desktop install used the build directory itself as the device's filesystem, so the File Manager's
root listed CMake caches, object archives and every ESP32 variant's build folder alongside the four
directories a device actually has. It now roots at `build/fs`, so what the desktop shows is what a
board shows.

An existing checkout starts with an empty-looking device, because its `.config` is one level up.
Move what you want to keep:

```sh
mkdir -p build/fs
mv build/.config build/moonlive build/.hls build/fs/ 2>/dev/null
```

Nothing is deleted if you skip this: the old directories stay where they are, and the device simply
starts fresh. `MM_DATA_DIR` still overrides the location, and a packaged desktop install (which uses
the per-user data directory) is unchanged.

### projectMM no longer appears in WLED apps by default

Device discovery now announces on the multicast group `239.255.77.77` and, by default, **not** on
Expand Down
29 changes: 18 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,14 +685,21 @@ The light domain plugs into the UI at three points: a fixed top-level tree (Layo

## Tag emoji legend

A module's chips come from three sources, rendered identically on the card and the type picker: a **role** chip (UI-derived from `role`), a **dimensional** chip (UI-derived from `dim`), and the curated **`tags()`** string (a flash literal the module returns; the UI splits it into grapheme clusters, one chip each). Role and dim are *not* repeated in `tags()` — only the categories below are. The `ROLE_EMOJI` / `DIM_EMOJI` maps in `app.js` are the single source of truth for the UI-derived chips; the legend takes [MoonLight](https://github.com/MoonModules/MoonLight)'s set as the canonical basis:

| Category | Emoji | Meaning |
|---|---|---|
| **Role** (UI-derived) | 🔥 effect · 💎 modifier · 🚥 layout · ☸️ driver · 🛰️ service · 🥞 layer · ⚙️ generic | what kind of module (from `role`, via `ROLE_EMOJI`) |
| **Dimensionality** (UI-derived) | 📏 1D · 🟦 2D · 🧊 3D | native axes (from `dim`) |
| **Origin / library** (`tags()`) | 💫 MoonLight · 🐙 WLED · ⚡️ FastLED · *(projectMM-native is the default origin — an origin emoji marks a module that came from elsewhere)* | which library the module came from; the migration files docs by this, the emoji filters by it |
| **Creator** (`tags()`) | 🦅 a named contributor (credited at the introduction site) | individual authorship credit |
| **Audio** (`tags()`) | 🔊 audio-reactive | reads `AudioService::latestFrame()` |

`tags()` carries **only** origin + creator + audio (+ any genuinely module-specific marker); a module can carry several (e.g. `💫🦅` = MoonLight origin, a named creator). Role and dim are added by the UI, so a module never duplicates them in its string. When migrating, set each module's `tags()` from this legend so the chip set is consistent across the library.
The legend itself lives with the people who read the chips: [How projectMM works § The emoji on
every card](tutorials/how-projectmm-works.md#5-the-emoji-on-every-card). What belongs here is the
mechanism.

A module's chips come from three sources, rendered identically on the card and the type picker: a
**role** chip and a **dimensional** chip, both UI-derived from `role` and `dim` through the
`ROLE_EMOJI` / `DIM_EMOJI` maps in `app.js` (the single source of truth for those two), and the
curated **`tags()`** string, a flash literal the module returns which the UI splits into grapheme
clusters, one chip each.

**Role and dim are never repeated in `tags()`**: the UI already adds them, and a module that spells
them again gets the chip twice. `tags()` carries origin, creator, and the capability groups the
legend lists. An emoji earns its place by GROUPING several modules, which is what the picker's chip
filter is for: a unique marker per module filters nothing, so a module that fits no group returns
"".

A scripted module answers the same way a compiled one does: `MoonLiveEffect::tags()` returns what
the loaded script's `string tags()` declared, so a script's row reads like any other.
Loading
Loading