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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ flowchart TB

Keep each commit under ~100 changed files: CodeRabbit reviews the files a pushed commit changes, and past that the commit silently loses a review layer. A mechanical sweep (em-dashes, a metrics refresh) is a commit of its own, apart from behavioral fixes, so both stay reviewable.

**A contributed pull request is one topic, about 400 changed lines and ten files at most**, 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.

### 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).
Expand Down Expand Up @@ -179,7 +181,7 @@ On "run pre-commit": run the checks whose trigger the diff matches, report one l

**A failure is something to report, not to fix and re-run.** A second run needs the words again, as much after a failure or a fix as at any other time.

**The gates run once per commit.** A run costs minutes, so scope freezes at "run pre-commit": a request made during a run goes to the next commit. A change made after a run re-checks only itself, on the product owner's go. A fixed failure reruns that one check; any other change runs the checks its own diff triggers. A full rerun needs the word "restart". A cosmetic change can ride on the run it followed, since the next commit's gates check it again.
**Each check runs once per commit.** A run costs minutes, so scope freezes at "run pre-commit": a request made during a run goes to the next commit. A change made after a run re-checks only itself, on the product owner's go. A fixed failure reruns that one check; any other change runs the checks its own diff triggers. A full rerun needs the word "restart". A cosmetic change can ride on the run it followed, since the next commit's gates check it again.

```mermaid
flowchart LR
Expand Down
Binary file added docs/assets/core/PictureService.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/dw3000.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/hc-sr501.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/hlk-ld2450.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/mpu6050.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/p4-csi-camera.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/vl53l0x.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/hardware/sensors/vl53l8cx.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/light/effects/FseqPlayerEffect.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/light/effects/FseqPlayerEffect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/light/effects/NetworkReceiveEffect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/light/effects/PictureEffect.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/light/effects/PictureEffect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions docs/how-to/adding-a-stream-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Adding a picture or channel-data source

A new way for frames to reach the lights, from a capture device, a file format or a network protocol.
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.

---

## Which family

| Your frames are | The family | Where they land | Examples |
|---|---|---|---|
| width × height pixels, any size | picture | [Picture service](../moonmodules/core/services.md#picture) | USB capture, a camera, a GIF, a browser tab |
| one value per light, in wiring order or row by row | channel data | the channel-data sink | Art-Net, E1.31, DDP, an FSEQ file |

A picture knows nothing of the lights: the [Picture effect](../moonmodules/light/effects.md#picture) samples it onto whatever layout the layer has, fitted, filled or stretched.
Channel data is already per light: it is placed through the layer's mapping, or straight to the output when the sender mapped it.

---

## A picture source

A picture source is an option of the Picture service's `source`, in `src/core/services/PictureService.h`.

1. **Add the option.** Append it to the `Source` enum and its name to the `source` select, and say whether it runs in a task of its own in `kRunsInTask`.
2. **A still picture** draws once in `prepare()`, as the test pattern does: write into the picture `prepareBuffers` sized, and set the frame.
3. **A moving picture** comes from a task of its own, a capture or a receiver, so the render loop never waits on a device. Start the task in `prepare()` and stop it in `release()`. For each picture:

```cpp
uint8_t* px = beginWrite(); // the back picture, width x height x 3 bytes, top row first
decodeInto(px, width, height); // your source: a decoder writes here directly, nothing is copied
endWrite(); // swap it to the front and bump the generation
```

That is the whole contract.
With room for two pictures, `beginWrite` hands out the back one and `endWrite` swaps the pointers, so effects read the front picture undisturbed and never see half of one.
Short of memory, the service keeps one picture and locks it while you write: frames may be skipped, never torn, and its status says why.
Effects read through `PictureService::Reader`, which holds the picture still for one gather; you never touch them.

**What a picture source brings:**

- its option and its row on the Picture service's spec card;
- a unit test that drives it on the desktop with a stand-in for the device;
- the platform seam for the device itself, with stubs on every other platform.

The thread-safety of the swap is already pinned by the service's own tests, a writer task racing a reader.

Sources are options of one module, so a source edits that file. When there are more than a handful, a source per file behind a small registry is the next step.

---

## A channel-data source

A channel-data source is an effect of its own, with its own settings, that owns a `ChannelDataSink` and writes its frames into it.
`src/light/effects/FseqPlayerEffect.h` is the template, and `NetworkReceiveEffect.h` the complete example.

```cpp
class MyFormatEffect : public EffectBase {
public:
void prepare() override {
sink_.prepare(/*senderMapped=*/true); // wiring order: one value per physical light
// open your file or socket, check its header, set a status for what you refuse
}
void tick() MM_NONBLOCKING override {
// read this frame and hand it over: sink_.write(offset, data, length);
sink_.tick(); // the sink places a changed frame
}
bool deliversWiringOrder() const override { return sink_.wiringOrder(); }
void copyWiringFrame(uint8_t* out, size_t bytes, bool force) override { sink_.copyWiringFrame(out, bytes, force); }
private:
ChannelDataSink sink_{*this};
};
```

The sink does the rest: it holds the last frame so the lights keep it between frames, skips a frame that did not change, and places it.
A frame mapped by the receiver is copied into the layer, which applies this board's wiring.
A frame mapped by the sender, alone on its layer, goes to the output as it is, one copy on any layout.
Beside another effect or under a modifier it is placed on the grid by the layout's positions, so a modifier transforms it.
A source writing from a task of its own calls `write` there, which waits out at most one render copy; one that reads in `tick()` calls `write<false>`.

**What a channel-data source brings:**

- its effect, its settings and its spec card;
- 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, based on main, about 400 changed lines and ten files at most, as [the process](../contributing/principles-and-process.md) describes.
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.
19 changes: 17 additions & 2 deletions docs/moonmodules/core/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ A user-added Service: the audio source the audio-reactive effects consume. `mode
- `micMode`: (Local, I²S targets) `I2S` for a three-wire part, `PDM` for a two-wire one.
- `sckPin` / `wsPin` / `sdPin`: (Local, I²S targets) the bus GPIOs, unset until entered.
- `mclkPin`: (Local, I²S targets) the master clock an ADC or codec needs; unset for a plain mic.
- `codec`: (Local, I²S targets) `ES8311` for a mic behind that codec, else `none`.
- `codecAddr`: (with a codec) its address on the [I2C bus](system.md#i2c-bus), in hex (`0x18`).
- `codec`: (Local, I²S targets) `ES8311` or `ES7210` in front of the mic, else `none`.
- `codecAddr`: its [I2C](system.md#i2c-bus) address: `0x18` (ES8311), `0x40` to `0x43` (ES7210).
- `device`: (Local, desktop) the OS capture input, `default` following the system setting.
- `sampleRate`: (Local) mic/ADC sample rate.
- `levels`: (Local) who sets the display window: `manual`'s two sliders, or `automatic`, a learner.
Expand All @@ -42,6 +42,21 @@ Detail: [technical](moxygen/AudioService.md) · [the sync packet](moxygen/WLEDAu

[Tests](../../reference/tests/unit-tests.md#audioservice)

<a id="picture"></a>

### Picture

A user-added Service: the picture the [Picture effect](../light/effects.md#picture) samples onto the lights. One is active at a time, as with Audio.

<img src="../../assets/core/PictureService.png" width="300" alt="Picture module controls">

- `source`: `test pattern`, a test card in the layout of Philips' PM5544, at any size.
- `width` / `height`: the picture in pixels, up to 512 a side; 64 × 48 by default.

Detail: [technical](moxygen/PictureService.md)

[Tests](../../reference/tests/unit-tests.md#pictureservice)

<a id="osc"></a>

### OSC
Expand Down
2 changes: 1 addition & 1 deletion docs/moonmodules/core/supporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ A flat parser returns zero for a missing key, which is indistinguishable from a
| [`ImprovOpReassembler`](moxygen/ImprovOpReassembler.md) | The state machine that reassembles a chunked operation into one buffer, with the duplicate and out-of-order guard. The platform layer owns the serial side around it. |
| [`FirmwareImage`](moxygen/FirmwareImage.md) | Reads an image's header without the vendor framework. The layout is a fixed on-disk format, so the same bytes parse on a device and in a test. |
| [`PinList`](moxygen/util_PinList.md) · [`HostList`](moxygen/HostList.md) | The two list parsers a board configuration needs: a pin list and a host list of addresses and names, both typed by a human into one text control. |
| [`Addressing`](moxygen/Addressing.md) · [`HostResolver`](moxygen/HostResolver.md) | One rule for sending to many receivers, unicast, multicast or broadcast, and the cache that turns a host name into an address off the render thread. |
| [`Addressing`](moxygen/Addressing.md) · [`HostResolver`](moxygen/HostResolver.md) | One rule for sending to many receivers, unicast, multicast or broadcast, and the cache that turns a host name into an address without blocking the render thread. |
| [`ConfigScrape`](moxygen/ConfigScrape.md) · [`Ipv4`](moxygen/Ipv4.md) | The keys MoonBase reads out of the app's saved config without a parser, and the IPv4 parse and static-address rule both images check by. |
| [`sha256`](moxygen/sha256.md) · [`crc`](moxygen/crc.md) | A cryptographic digest for update verification and identity, and a cheap checksum used as a fingerprint of a block of state. |
| [`Sha1`](moxygen/Sha1.md) · [`Base64`](moxygen/Base64.md) | The two codecs the WebSocket handshake needs, the digest that standard mandates and the encoding that carries it. |
Expand Down
15 changes: 8 additions & 7 deletions docs/moonmodules/light/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The block every driver card opens with, shown here on RMT LED. Added once by [`D

<img src="../../assets/light/drivers/RmtLedDriver.png" width="300" alt="The shared block at the top of a driver card, here on RMT LED">

- `localBrightness`: this driver's dim (0–255), multiplied with the global brightness.
- `localBrightness`: this driver's dim (0 to 255), multiplied with the global brightness.
- `curve`: brightness to output: `CIE 1931`, `gamma 2.2`, `gamma 2.8`, or `linear`.
- `fixture`: the [fixture profile](supporting.md) applied per light, for order and white.
- `whiteMode`: how W is derived, shown when the profile carries a W channel.
Expand Down Expand Up @@ -125,14 +125,15 @@ Detail: [technical](moxygen/PwmLightDriver.md)

<img src="../../assets/light/drivers/NetworkSendDriver.png" width="300" alt="NetworkSend controls">

Streams the buffer over UDP as **Art-Net**, **E1.31 / sACN** or **DDP**, one burst per frame, to Falcon/Advatek controllers, xLights and LedFx. Feeds several receivers from one driver, each its own slice. Mixed DMX fixtures and the addressing rules are in [the details below](#network-send-details).
Streams the buffer over UDP as **Art-Net**, **E1.31 / sACN** or **DDP**, its packets paced so receivers keep up, to Falcon/Advatek controllers, xLights and LedFx. Feeds several receivers from one driver, each its own slice. Mixed DMX fixtures and the addressing rules are in [the details below](#network-send-details).

- `protocol`: Art-Net / E1.31 / DDP (default Art-Net); the port follows.
- `addressing`: unicast for all three, multicast for E1.31, broadcast for Art-Net.
- `hosts`: the receivers, such as `192.168.1.70-74, panel-01.local`. **Blank idles.**
- `lightsPerHost`: lights per receiver. Blank splits evenly, one number to all, a list each.
- `universe_start`: first universe for Art-Net / E1.31, restarting per receiver. DDP ignores it.
- `fps`: frame-rate limit (default 50, 1–120).
- `fps`: frame-rate limit (default 50, 1 to 120).
- `packet_gap`: µs between a frame's packets (default 500); 0 sends the frame as one burst.

[Tests](../../reference/tests/unit-tests.md#networksenddriver)

Expand Down Expand Up @@ -187,7 +188,7 @@ Streams a true-shape 3D preview to the web UI as a **point list**, only the real

It streams on its own WebSocket channel so a large frame never delays the control plane, and only while a viewer is watching. Moving heads show their beam. How the rate and detail trade off is in [the details below](#preview-details).

- `targetFps`: the rate to aim for (default 24, 1–25). **Lower for detail, raise for smoothness.**
- `targetFps`: the rate to aim for (default 24, 1 to 25). **Lower for detail, raise for smoothness.**

[Tests](../../reference/tests/unit-tests.md#previewdriver)

Expand All @@ -204,7 +205,7 @@ Publishes the layer as an **NDI video source**, so OBS, Resolume, TouchDesigner
The grid becomes the frame, one light per pixel, output correction applied, so a receiver sees what the wall sees. **Desktop only, and you install the NDI runtime yourself**; without it the driver says so and nothing else changes. See [the details below](#ndi-details).

- `sourceName`: the name a receiver lists. Blank uses the device's own name.
- `fps`: frame-rate ceiling (default 30, 1–120), declared in every frame.
- `fps`: frame-rate ceiling (default 30, 1 to 120), declared in every frame.

Detail: [technical](moxygen/NdiDriver.md)

Expand All @@ -218,7 +219,7 @@ Streams the layer as **H.264 over HLS** from the device's own HTTP server. Open

The grid becomes the frame, output correction applied. Latency is HLS's own, **2-5 seconds**, so this is for watching rather than live-control feedback. Runs on desktop (you install ffmpeg) and on the **ESP32-P4**, which encodes in hardware. See [the details below](#hls-details).

- `targetFps`: encode-rate ceiling (default 30, 1–120). Also the bandwidth knob: bitrate is derived.
- `targetFps`: encode-rate ceiling (default 30, 1 to 120), and the bandwidth knob: bitrate follows.
- `scale`: video pixels per light (0 = auto). Each light is a solid block, never a blur.
- `encoder`: which ffmpeg encoder (desktop only).
- read-only: `url` to play. The card's status line carries state, drops, or why the encoder stopped.
Expand All @@ -235,7 +236,7 @@ Streams the layer as **H.264 over RTSP**, which a player pulls rather than fetch

Runs on the **ESP32-P4** and on the desktop. One encoder, so **RTSP and [HLS](#hls) run one at a time**. See [the details below](#rtsp-details).

- `targetFps`: encode-rate ceiling (default 30, 1–60), which is also the keyframe interval.
- `targetFps`: encode-rate ceiling (default 30, 1 to 60), which is also the keyframe interval.
- `scale`: video pixels per light (0 = auto). Each light is a solid block, never a blur.
- read-only: `url` to play. The card's status line names the connected viewer.

Expand Down
40 changes: 39 additions & 1 deletion docs/moonmodules/light/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -1331,16 +1331,37 @@ Detail: [technical](moxygen/DemoReelEffect.md)

[Tests](../../reference/tests/unit-tests.md#demoreeleffect)

<a id="fseqplayer"></a>

### FseqPlayer 🎞️

<img src="../../assets/light/effects/FseqPlayerEffect.gif" width="300" alt="The reference sweep fseq_play writes, the show an FSEQ player plays">

An FSEQ show, xLights' and Falcon Player's frame-by-frame channel data, played from a file. It lands where a sender-mapped [Network Receive](#networkreceive) stream does, so alone on its layer it reaches the lights in one copy on any layout. It reads no file yet, and its status says so: playback is the next step.

- `file`: the show's path, an `.fseq` file on flash or under `/sd`.
- `loop`: start again at the end.
- `speed`: the playback speed, in percent of the file's step time, up to 255.

Origin: MoonLight original (FSEQ v2, as Falcon Player documents it)

Detail: [technical](moxygen/FseqPlayerEffect.md)

[Tests](../../reference/tests/unit-tests.md#fseqplayereffect)

<a id="networkreceive"></a>

### NetworkReceive 📡🌙

<img src="../../assets/light/effects/NetworkReceiveEffect.gif" width="300" alt="NetworkReceive effect preview">

Receives lights over UDP and writes them into the layer: the receive side for Resolume, Madrix, xLights and LedFx.
Receives lights over UDP into the layer, from Resolume, Madrix, xLights and LedFx. Sender mapping is one copy alone on its layer, else two mappings, as the status shows. Behind a correcting sender, keep the profile in byte order, brightness 255 and the linear curve.

- `universe_start`: the first incoming universe to map, mirroring the sender.
- `channels_per_universe`: bytes each universe maps to; 510 or 512.
- `mapped_by`: **receiver** (sent row by row) or **sender** (in wiring order).
- `packets_per_s`: packets received in the last second, read-only.
- `frames_per_s`: frames received in the last second, read-only.

Origin: MoonLight original (E1.31 / Art-Net receive)

Expand All @@ -1350,6 +1371,23 @@ Detail: [technical](moxygen/NetworkReceiveEffect.md)

Listens for Art-Net, E1.31 and DDP at once. The end-to-end pair with [Network Send](drivers.md).

<a id="picture"></a>

### Picture 🖼️ · 2D

<img src="../../assets/light/effects/PictureEffect.gif" width="300" alt="Picture effect preview">

The active [Picture service](../core/services.md#picture)'s picture, sampled onto the layer, so a picture of any size shows on a layout of any size. A modifier rotates, mirrors or tiles it like any effect.

- `fit`: keep the aspect with bands (**fit**), cropped (**fill**), or **stretch**.
- `sampling`: **point**, one pixel per light, or **area**, a linear-light mean.

Origin: MoonLight original

Detail: [technical](moxygen/PictureEffect.md)

[Tests](../../reference/tests/unit-tests.md#pictureeffect)

<a id="sine"></a>

### Sine 💫 · 3D
Expand Down
Loading
Loading