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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## What This Is

A high-performance multi-platform system driving large LED installations and DMX fixtures. ESP32 is the primary target; also Teensy, macOS, Windows, Linux, RPi. System design: [docs/architecture.md](docs/architecture.md); coding conventions: [docs/coding-standards.md](docs/coding-standards.md). This file holds only the rules.
A high-performance system driving large LED installations and DMX fixtures. One source tree drives ESP32, Teensy, Raspberry Pi, macOS, Windows and Linux. System design: [docs/architecture.md](docs/architecture.md); coding conventions: [docs/coding-standards.md](docs/coding-standards.md). This file holds only the rules.

## Principles

Expand Down Expand Up @@ -127,7 +127,8 @@ Published at [moonmodules.org/projectMM](https://moonmodules.org/projectMM/); so
- [MIGRATING.md](https://moonmodules.org/projectMM/MIGRATING.html) — breaking-change log
- [backlog/](https://moonmodules.org/projectMM/backlog/index.html) — forward-looking to-build lists (core / light / mixed)
- [adr/](https://moonmodules.org/projectMM/adr/index.html) — immutable architecture decision records (Nygard format); immutable except the status line: superseded/amended ADRs get a dated pointer to their successor
- [history/](https://moonmodules.org/projectMM/history/index.html) — lessons, prior-project inventories, friend-repo digests
- [friend-repos/](https://github.com/MoonModules/projectMM/tree/main/docs/friend-repos): monthly activity digests of related open-source LED projects
- [history/](https://moonmodules.org/projectMM/history/index.html): lessons, prior-project inventories
- [moonmodules/](https://github.com/MoonModules/projectMM/tree/main/docs/moonmodules) — module catalog pages + generated technical pages

Docs describe the system as it is; git is the history; specs precede implementation. **Documentation model**: [coding-standards.md § Documentation model](docs/coding-standards.md#documentation-model).
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# projectMM

Drive large LED installations and DMX lighting from ESP32, Teensy, Raspberry Pi, Windows, macOS or Linux desktop. One source tree, multiple targets.
Drive large LED installations and DMX fixtures. One source tree drives ESP32, Teensy, Raspberry Pi, macOS, Windows and Linux.

![Web UI](docs/assets/ui/ui_theme.gif)

Expand Down Expand Up @@ -172,6 +172,7 @@ Specific people whose work directly shaped parts of projectMM. We study their th
- **The [Improv Wi-Fi](https://github.com/improv-wifi) project**: the open Improv serial provisioning standard ([sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js)) that the projectMM web installer uses to provision a freshly-flashed device over USB.
- **[FastLED](https://github.com/FastLED/FastLED)**: the canonical LED-effects library whose conventions the LED-effect world shares. projectMM links no part of FastLED, but it carries forward FastLED's recognisable *names and models* for the color/animation primitives (`scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), the `beatsin8` / `inoise8` / `qadd8` family), so a contributor recognises them on sight. The implementations are projectMM's own, integer-only and hot-path-tuned for our render loop; FastLED is the prior art behind the convention, credited here and in each primitive's notes.
- **[FPP](https://github.com/FalconChristmas/fpp) (Falcon Player)**: the show player that drives LED panel receiver cards from a Raspberry Pi. Seeing an FPP rig feed a wall of HUB75 panels is what prompted [PanelCardDriver](docs/moonmodules/light/drivers.md#panelcard): if a Linux host can send those frames, so can a board that is already rendering them, which removes the host from the installation entirely. FPP is the inspiration, and the reference point for what good looks like here: it sustains 50 fps.
- **Damian Schneider ([dedehai](https://github.com/DedeHai))**: author of the WLED Particle System, whose emitters, forces and walls over one shared pool are the shape our [particle kernel](docs/moonmodules/light/power-functions.md#particles) and the scripted `pool` / `emit` / `step` builtins follow, in our own fixed-point implementation.
- **wladi ([myhome-control](https://shop.myhome-control.de))**: designer of the [MHC-WLED ESP32-P4 shield](https://shop.myhome-control.de/en/ABC-WLED-ESP32-P4-shield/HW10027), and the source of the hardware and the pinout details that got its **line-in audio** working in [AudioService](docs/moonmodules/core/moxygen/AudioService.md): the onboard PCM1808 I2S ADC (WS 26 / SD 33 / SCK 32 / MCLK 36), the PCM1808's stereo wiring, and its `FMT` format-select jumper (open = I2S/Philips, our default; tie to 3V3 for left-justified), which is what confirmed the standard-I2S path the ADC needs.

## Contributing
Expand Down
8 changes: 7 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,13 @@ See NoiseEffect / MetaballsEffect for the canonical pattern. Animation speed mus

**A faster device renders the same motion more smoothly, not more motion.** The tempting fix — quantise to a fixed 60 Hz and skip the frames in between — is wrong here, because it discards exactly the smoothness the extra frames were rendered for. Instead scale the work by the fraction of a reference frame that actually elapsed, so a device rendering ten times as fast takes ten steps a tenth the size: the same trajectory at ten times the resolution. `particles::FrameTime` is the shared implementation (8.8 fixed point, 256 = one reference frame, whose rate is the constructor's `referenceHz` — 60 by default). It carries the undivided numerator and divides late, for the same reason `BeatPhase` does: one unit is a fraction of a millisecond, so a remainder held in whole milliseconds cannot represent it and the truncated time — which differs by render rate — becomes a framerate dependency of its own.

The check is mechanical: **run the effect at two very different framerates over the same span of simulated time and compare.** If the result differs, something is counting frames.
The check is mechanical: **run the effect at two very different framerates over the same span of simulated time and compare.** If the result differs, something is counting frames. `unit_Effects_framerate.cpp` runs exactly that sweep over every registered effect at 60 and 1200 fps.

**It applies to modifiers too, and to anything else on the tick path.** A modifier that scrolls, rotates or animates its fold is state advanced per call, so the same rule holds: a scroll driven by a per-tick increment moves at the render rate. Anything whose output changes between two ticks with identical inputs is animating and owes elapsed time; a modifier that only folds coordinates from its controls is a pure function and owes nothing.

**Where the machinery lives, and why it is not in the effects.** A trail fade is the case with the most callers, so it is the worked example: `Layer::fadeToBlackBy` takes a RATE per reference frame and the Layer scales it once, for every effect at once. Three effects used to carry that conversion themselves and had already drifted into two versions of it (one carried the fraction, two floored to 1 and so applied many times the intended decay at high rates), which is the duplication the one-home rule exists to prevent. Every amount is a rate, with no exception: an effect that wants the buffer blank NOW calls `draw::fill`, since a clear is not a fast fade. Giving 255 a second meaning put a discontinuity in kind at the top of six user-facing fade sliders.

Two traps worth naming, both found by hitting them. A quantity that is already gated by wallclock must not ALSO be scaled: StarField requested its fade only on stepping frames, and the Layer then scaled each request again, throttling it twice. And a COMPOUNDING spatial operation is not a rate: `draw::blur` applied twice at half strength is not one blur at full strength, so the carry pattern that fixes a fade does not transfer to it (BlurzEffect is the open case).

**An effect renders a pattern; it does not transform geometry.** When migrating or adding an effect, strip out anything that is really a *modifier* — mirroring, tiling, rotation, scrolling/offset, a kaleidoscope fold, masking, any remap of *where* pixels land — and add it as a separate [modifier](#modifiers) instead. WLED (and other sources we port from) routinely fold these into the effect's own loop (a "mirror" checkbox, a "2D" rotation, a built-in pinwheel), because WLED has no modifier concept; we do. Keeping them out of the effect is what lets any effect compose with any modifier (the same RotateModifier rotates Fire, Noise, or a network-received frame) instead of every effect re-implementing its own half-baked mirror. The test: an effect's `tick()` should only *write colors into the logical buffer for its own coordinates*; if it's reading or rewriting positions to move/fold/duplicate the image, that behaviour belongs in a modifier. (This is the light-domain face of *Complexity lives in core; domain modules stay simple* — geometry transforms are the modifier's job, shared once, not duplicated into every effect.)

Expand Down
Loading
Loading