diff --git a/docs/assets/deviceModels/esp32-p4-olimex-p4-pc.jpg b/docs/assets/deviceModels/esp32-p4-olimex-p4-pc.jpg new file mode 100644 index 00000000..4f183dbe Binary files /dev/null and b/docs/assets/deviceModels/esp32-p4-olimex-p4-pc.jpg differ diff --git a/docs/design/effects-power-functions-inventory.md b/docs/design/effects-power-functions-inventory.md index fbd4c602..cc70d679 100644 --- a/docs/design/effects-power-functions-inventory.md +++ b/docs/design/effects-power-functions-inventory.md @@ -1,6 +1,6 @@ # Effects on the power functions, an inventory -One row per effect in the tree on 2026-09-03 (58), recording what each uses and which power functions it could run on, all of them in [power-functions.md](../moonmodules/light/power-functions.md): gradient noise, `PolarLut`, `Oscillators`, `advect` and the velocity rules, `decay`, `lineAA` and `disc` among them. It is the input for [the catalog sweep](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287081620), under [the migration rule](../moonmodules/light/power-functions.md#migrating-an-effect). A rewritten effect's row is deleted, not ticked. +One row per effect in the tree on 2026-09-03 (58), recording what each uses and which power functions it could run on, all of them in [power-functions.md](../moonmodules/light/power-functions.md): gradient noise, `PolarLut`, `Oscillators`, `advect` and the velocity rules, `decay`, `lineAA` and `disc` among them. It is the input for [the catalog sweep](https://github.com/MoonModules/MoonLight/issues/221), under [the migration rule](../moonmodules/light/power-functions.md#migrating-an-effect). A rewritten effect's row is deleted, not ticked. **Kind.** N = natural motion (physics, fields, noise, trails, rotation, oscillation): rewritten on the kernels. A = algorithmic (a rule set or a game is the effect): keeps its logic, draws through the library. M = audio meter (bars, levels, spectra): the geometry and ballistics rows apply. U = utility or test: no rewrite owed. Anything in doubt is N. diff --git a/docs/design/leddriver.md b/docs/design/leddriver.md index b52a7f8d..d3ba6846 100644 --- a/docs/design/leddriver.md +++ b/docs/design/leddriver.md @@ -4,7 +4,7 @@ What shipped and where it lives: [Choosing and wiring an LED driver](../referenc ## LED output from a Raspberry Pi's own pins -Backlog: [LED output from a Raspberry Pi's own pins](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287086230). The prerequisite is a `src/platform/rpi/` backend; a Pi runs the desktop build and reaches LEDs through the network drivers. +Backlog: [LED output from a Raspberry Pi's own pins](https://github.com/MoonModules/MoonLight/issues/218). The prerequisite is a `src/platform/rpi/` backend; a Pi runs the desktop build and reaches LEDs through the network drivers. **Pi 4 and earlier (BCM2711 and before).** rpi_ws281x (BSD-2, ) drives the header by DMA through PWM (2 channels), PCM (1) or SPI (1), up to about 2,700 lights a string on PWM and 5,400 on PCM. It is the reference for that route; its last release is v1.0.0 of March 2023. @@ -20,7 +20,7 @@ Backlog: [LED output from a Raspberry Pi's own pins](https://github.com/users/Mo ## The classic ESP32 past 2,048 lights -Backlog: [Classic-ESP32 shift-register ring on raw I2S](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080365). +Backlog: [Classic-ESP32 shift-register ring on raw I2S](https://github.com/MoonModules/MoonLight/issues/218). ### Verdict @@ -139,15 +139,15 @@ This is not a niche worry: every ESP32 LED library that streams rather than whol The 74HCT595 expander ships on `i80` and `MoonI80`, the LCD_CAM peripheral of the S3, P4 and S31 ([Wiring a 74HCT595 pin expander](../reference/hardware/led-drivers.md#wiring-a-74hct595-pin-expander)). What it needs from a peripheral: a shift clock on a pin, the latch as one bit of every bus word, and eight bus words per slot, so the DMA frame grows eightfold. RMT has no clock line and never qualifies. The other two parallel peripherals could: -- **Parlio (P4).** `ParlioPeripheral::supportsPinExpander()` is false. The transmit unit generates its pixel clock itself, and `platform_esp32_parlio.cpp` leaves `clk_out_gpio_num` unconnected because WS2812 ignores it; connected, it is the '595's shift clock. Whether Parlio's clock output and bit packing carry the expander's protocol is a bench question, and the eightfold frame meets the single-transaction ceiling the [chunked transfer](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080935) lifts. Backlog: [A '595 expander on Parlio](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287079383). -- **GPIO-DMA (Teensy 4.1).** `GpioDmaPeripheral::supportsPinExpander()` is false. The port write carries no clock of its own, so the shift clock has to come from the writes themselves, at a DMA rate still to measure. FlexIO clocks by itself, and [the Teensy plan](../work/Plan-20261010%20-%20Teensy%204.1%20(3%20of%206).md) set it aside because no 8 contiguous FlexIO pins stay free beside the W5500. TriantaduoWS2811 () is the prior art: 32 strands from 3 Teensy pins through FlexIO clocking '595s at zero CPU cost. Backlog: [A '595 expander on the Teensy's GPIO-DMA bus](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287078012). +- **Parlio (P4).** `ParlioPeripheral::supportsPinExpander()` is false. The transmit unit generates its pixel clock itself, and `platform_esp32_parlio.cpp` leaves `clk_out_gpio_num` unconnected because WS2812 ignores it; connected, it is the '595's shift clock. Whether Parlio's clock output and bit packing carry the expander's protocol is a bench question, and the eightfold frame meets the single-transaction ceiling the [chunked transfer](https://github.com/MoonModules/MoonLight/issues/218) lifts. Backlog: [A '595 expander on Parlio](https://github.com/MoonModules/MoonLight/issues/218). +- **GPIO-DMA (Teensy 4.1).** `GpioDmaPeripheral::supportsPinExpander()` is false. The port write carries no clock of its own, so the shift clock has to come from the writes themselves, at a DMA rate still to measure. FlexIO clocks by itself, and [the Teensy plan](../work/Plan-20261010%20-%20Teensy%204.1%20(3%20of%206).md) set it aside because no 8 contiguous FlexIO pins stay free beside the W5500. TriantaduoWS2811 () is the prior art: 32 strands from 3 Teensy pins through FlexIO clocking '595s at zero CPU cost. Backlog: [A '595 expander on the Teensy's GPIO-DMA bus](https://github.com/MoonModules/MoonLight/issues/218). - **RP1 PIO (Pi 5).** Research grade, above. The expander multiplies slots, not pins, so each peripheral's frame or ring grows eightfold too: the memory question arrives with the clock question. ## The sigrok cross-check -Backlog: [sigrok/fx2lafw cross-check + MoonDeck LED driver test script](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287130440). +Backlog: [sigrok/fx2lafw cross-check + MoonDeck LED driver test script](https://github.com/MoonModules/MoonLight/issues/218). The loopback test trusts the chip's own clock, and on an ESP32 it runs on the chip whose WiFi causes the flicker it looks for. An analyzer with its own clock crosses it: where the two disagree there is a real bug. The reference device is a NanoDLA v1.3 (CY7C68013A, fx2lafw firmware, 24 MS/s on 8 channels, 10 to 15 dollars); any board sigrok's `fx2lafw` driver enumerates works the same. 24 MS/s is 41 ns a sample, enough to tell a 350 ns pulse from a 700 ns one and marginal for a timing-margin test (320 against 380 ns). A DSLogic Plus (16 channels at 100 MS/s) is the upgrade when a flicker reproduces that 24 MS/s cannot resolve, and the scripts carry over unchanged. diff --git a/docs/design/moonlive.md b/docs/design/moonlive.md index 46eb95fe..05f7fe0e 100644 --- a/docs/design/moonlive.md +++ b/docs/design/moonlive.md @@ -1,6 +1,6 @@ # MoonLive: the language roadmap -[MoonLive](../explanation/architecture/moonlive.md) is the engine as built (grammar, IR, codegen), and [power-functions.md](../moonmodules/light/power-functions.md) is the builtin surface it calls into. This analysis is the ordered plan for closing the gap between them: which language features to add next, and why that order. Every limit quoted was measured against the shipped compiler, not read off the source. The steps are one backlog item, [The next language steps, in order](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287082542), and the code cap is [its own item](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287082596). +[MoonLive](../explanation/architecture/moonlive.md) is the engine as built (grammar, IR, codegen), and [power-functions.md](../moonmodules/light/power-functions.md) is the builtin surface it calls into. This analysis is the ordered plan for closing the gap between them: which language features to add next, and why that order. Every limit quoted was measured against the shipped compiler, not read off the source. The steps and the code cap are two backlog items, both listed in [MoonLive language and catalog](https://github.com/MoonModules/MoonLight/issues/221). ## What this is for diff --git a/docs/design/streams.md b/docs/design/streams.md new file mode 100644 index 00000000..759bee0b --- /dev/null +++ b/docs/design/streams.md @@ -0,0 +1,122 @@ +# Streams: every frame from outside, one model + +Cameras, video, files, the network and the browser all deliver a stream of frames from outside the device. This study models them once, by the textbook, so each becomes a combination of a few parts rather than a feature of its own. What is built of it, the two families and how a sender-mapped frame lands in a layer, is in [MoonLight, frames from outside](../explanation/architecture/moonlight.md#frames-from-outside); the picture sources still to come are planned in [Video in, LED walls out](../work/Plan-20261024%20-%20Video%20in%2C%20LED%20walls%20out%20(0%20of%2010).md). + +## Three independent axes + +Every media framework separates the same three questions, as GStreamer, FFmpeg, Media Foundation and Video4Linux all do: where the bytes come from, how they are encoded, and what shape the frame has. + +| Axis | Values in MoonLight's world | +|---|---| +| **Transport** | a file on flash or the SD card, the network (UDP, TCP, the browser's WebSocket), USB, a camera port (MIPI-CSI) | +| **Format** | raw RGB, PPM, GIF, MJPEG, FSEQ, the DDP, Art-Net and E1.31 framings; H.264 on the way out | +| **Geometry** | a picture (width × height), a light list (N lights, mapped by the sender), a volume (width × height × depth) | + +A source is a transport plus a decoder, delivering frames with a small descriptor: the geometry, the pixel format and the frame's time. An output is an encoder plus a transport. What users ask for is a combination: + +| What users ask for | Transport | Format | Geometry | +|---|---|---|---| +| An xLights show (FSEQ) | SD file | FSEQ | light list | +| Network Receive | UDP | DDP, Art-Net, E1.31 | light list | +| A GIF animation | flash or SD file | GIF | picture | +| An HDMI grabber | USB | MJPEG | picture | +| A camera on the P4 | MIPI-CSI | raw, through the image processor | picture | +| A screen shared from a browser tab | WebSocket | raw or DDP | picture or light list | +| A volumetric show for a cube | SD file | FSEQ | volume, or a light list placed in 3D | +| HLS and RTSP | HTTP, RTSP | H.264 | picture | +| Recording | SD file | FSEQ | light list | + +Recording is the bridge between transports and files: a live stream in, an FSEQ file out, written from the drivers' output. Art-Net "from the SD card" is that recording, as Falcon Player records it; FSEQ does not travel as a stream, since Falcon Player copies the file to its remotes. + +## Live and recorded + +Streams split a second way, by who sets the pace: the textbook split between live and on-demand media. + +| | Live | Recorded | +|---|---|---| +| Examples | Network Receive, USB and CSI capture, a browser tab | FSEQ, GIF and image sequences from flash or SD, a baked show | +| Who sets the pace | the sender: a frame arrives when it arrives | the device's clock: it pulls the frame the timeline asks for | +| Late or missing frames | the newest frame wins and an old one is dropped, never queued, since a queued frame is a late frame | none: the next frame is read ahead, one frame of buffer at most | +| Seek, loop, pause | not possible | the frame index comes from the clock, so they are arithmetic | +| Sync across boards | each board shows what it receives | every board computes the same frame index from a shared clock | +| What it costs | latency, kept to one frame | storage bandwidth, kept to one frame per tick | + +The transport decides the kind: a socket, a grabber and a camera are live; a file is recorded. A file can also play at the pace of a sender elsewhere, which is how a recording replays exactly what arrived. A third case never reaches the device as a stream: **authoring**, such as baking a video into FSEQ for a layout, runs in the browser at whatever speed it takes, and only its result plays in real time. + +## Browser and device + +One rule decides the split, and MoonLight already follows it where the device cannot do a job (the Firmware card's download for a board without TLS, MoonCloud's outbox, scripts fetched from GitHub by the page): **what must run with no browser open belongs on the device; what happens once, with a person at the screen, belongs in the browser.** + +| Tier | Where | Examples | +|---|---|---| +| Authoring: once, a person present | browser | crop, scale and convert images; video to GIF or to FSEQ; fonts; an xLights model imported as a layout | +| Live, a browser as the source | the browser samples, the device displays | a screen (`getDisplayMedia`), a webcam or a video in a tab, over the existing WebSocket | +| Run time: autonomous | device | effects, playback from flash or SD, USB and CSI capture, sync, recording, a show running for days | + +**The browser can bake for this layout.** It knows every light's position, since it draws them in the preview, so it can sample a picture or a video per light once, with the area filter in linear light, and write an FSEQ file. The device plays it on the cheap light-list path, with no decoder. + +| | Baked in the browser (FSEQ) | Decoded on the device (GIF) | +|---|---|---| +| Device cost | lowest: read and copy | decode and sample every frame | +| Follows a layout change | no: bake again | yes | +| Storage | large: 8000 lights at 40 fps is about 1 MB/s, so SD only | small: fits in flash | + +A live browser source costs lights × 3 bytes × fps: about 60 KB/s for a 600-light TV frame at 30 fps, too much for 8000 lights over WiFi. A TV's picture from a set-top box or console never passes through a computer, so a backlight following it needs the grabber on the board. + +## 3D layouts and the P4 + +- **A light list carries no positions:** the layout places it, on 1D, 2D and 3D layouts alike, numbering its lights as the xLights model does. +- **A picture reaches a 3D layout by projection:** each light samples the picture at its position seen along one axis, the planar projection media servers use. +- **A volume is the native 3D stream:** a frame per depth slice, copied straight into a 3D layer. +- **The P4's hardware video codec encodes H.264 but does not decode it**; its JPEG codec works both ways. So video arrives as MJPEG from a grabber, raw from a camera, or baked in the browser, and leaves through the H.264 encoder HLS drives. The Olimex ESP32-P4-PC carries every piece on one board: HDMI and MIPI-DSI out, a MIPI-CSI connector, four USB host ports, a microSD slot, Ethernet with PoE and 32 MB of PSRAM. + +## Where MoonLight stands among the tools + +| Tool | Weight on | Runs on | +|---|---|---| +| Resolume | video clips in layers, pixel mapping to fixtures | PC | +| TouchDesigner | a general dataflow graph, anything to anything | PC | +| MADRIX | LED pixels: generative effects, layers, a 2D and 3D patch, media and capture, many outputs, record and replay on a standalone player | PC, standalone nodes | +| xLights with Falcon Player | sequencing offline, playback standalone | PC, Raspberry Pi | +| Hyperion, HyperHDR | capture to LED zones | Raspberry Pi, PC | +| WLED | effects on segments, a realtime override | ESP32 | + +MoonLight is closest to MADRIX: generative effects, layers, 2D and 3D layouts and many outputs, running on the controller itself. MADRIX's media, capture and record-for-standalone-playback are what this model adds: MADRIX-class pixel control, embedded. + +## What WLED did, and where it stopped + +Checked 2026-10-13 against WLED main and WLED-MM. + +- **One picture format, decoded on the device.** The Image effect (PR #3835, WLED 16.0) plays a GIF from the internal file system, up to 320 × 320, with about 24 KB of decoder RAM, scales by nearest neighbor, and redraws only changed pixels, which upsets brightness limiting. +- **Every conversion in the browser.** PixelForge (PR #4982) crops, scales and encodes a GIF in the page; Video Lab turns video into GIFs. +- **No capture on the device.** The only video path is a browser page streaming DDP over the WebSocket (PR #4997); Hyperion does backlight processing on a computer. +- **FSEQ: five attempts, none merged** (#2413, #3551, #4570, #5641). They bolted playback onto the realtime override, which replaces effects rather than composing with them, and each bundled an SD driver, a player, a UI and Falcon Player sync in one pull request. + +ESPixelStick, the reference small-device FSEQ player, plays uncompressed FSEQ v2 with sparse ranges from SD, floors the step at 25 ms, and takes the frame index from a monotonic clock plus a sync offset. What it teaches: GIF first among picture formats; conversion as browser pages; the layer as the home of every stream, exactly where WLED's FSEQ attempts went wrong. + +## Shapes left out, and why + +| Shape | Why not | +|---|---| +| A backlight effect of its own | one sampler setting of the picture effect; a card of its own duplicates the picture path | +| PPM as a user format | GIF covers the need with tools users own; PPM stays a test fixture | +| HDR tone curves (PQ, HLG) | no planned source delivers HDR | +| Video decoding beyond MJPEG on the device | the browser bakes video; the device decodes only what its hardware decodes | +| FSEQ extras up front (zstd, `.xlz`, FPP Connect, MultiSync) | the bundling that stalled WLED's attempts; each extra waits for a measurement or a user | +| One module for every transport and data type | one module model with typed data, as GStreamer and TouchDesigner do; one module would flatten the pixel protocols' semantics | +| A feed written straight into the drivers' output | WLED's realtime override gives up blending, modifiers and composition | +| FPP MultiSync for multi-board sync | MoonLight's own clock sync first; FPP compatibility later, as an adapter | + +## Open questions + +- The names: `PictureService` and `PictureEffect`, or names closer to the industry's (media source, pixel map)? +- Whether a layout reads xLights model exports, so a 3D model's node order and positions come from the file that designed the show. + +## Prior art + +- GStreamer and FFmpeg: transport, format and geometry as separate concerns. +- TouchDesigner: one operator model separated by data type, channel operators (DMX, Art-Net, MIDI) and texture operators (video, capture), the reference for the two families. +- Inverse mapping: a fragment shader sampling through a composed transform, OpenCV's `remap`. +- MADRIX, Resolume, Hyperion and HyperHDR, as in the table above. +- xLights and the FSEQ v2 format; Falcon Player for standalone playback and MultiSync; ESPixelStick for FSEQ from SD on an ESP32. +- WLED: the Image effect (#3835), PixelForge (#4982), DDP over WebSocket (#4997), the unmerged FSEQ attempts. diff --git a/docs/design/ui-extensibility.md b/docs/design/ui-extensibility.md index 296841f8..2fa92f0b 100644 --- a/docs/design/ui-extensibility.md +++ b/docs/design/ui-extensibility.md @@ -93,7 +93,7 @@ The convergent answer is **a small registry + a per-module widget contract, both There are **three tiers** the top-down should name, so a module reaches for the lightest that fits (minimalism): (a) **generic controls**, no custom UI, the default; (b) **generic-with-richer-list-detail**, a module whose need is nested/tabular detail; (c) **full custom widget**, a Custom Element for genuinely bespoke UI (file tree, board diagram). FileManager is (c); TasksModule is (a)-with-a-workaround that (b) would fix; Memory/Pins are likely (b) or (c). -This is directly tied to the system modules: Tasks and Pins ([core system](../moonmodules/core/system.md)) and [Memory](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287074597) are the modules that will want tier (b)/(c) UI, so the extension architecture should land before (or alongside) building Memory and Pins, otherwise each repeats FileManager's inlined-in-app.js mistake. +This is directly tied to the system modules: Tasks and Pins ([core system](../moonmodules/core/system.md)) and [Memory](https://github.com/MoonModules/MoonLight/issues/220) are the modules that will want tier (b)/(c) UI, so the extension architecture should land before (or alongside) building Memory and Pins, otherwise each repeats FileManager's inlined-in-app.js mistake. ## The acceptance test (from the governing statement) diff --git a/docs/explanation/architecture/mooncore.md b/docs/explanation/architecture/mooncore.md index 4a1ffb3b..27595e8c 100644 --- a/docs/explanation/architecture/mooncore.md +++ b/docs/explanation/architecture/mooncore.md @@ -50,7 +50,7 @@ A **service** is a MoonModule (role `ModuleRole::Service`) that bridges to the o The defining line is the **data relationship, not the connector**: *does the module consume the light output buffer?* If yes it's a **driver** (ArtNet, DMX, SPI-LED all consume the buffer, differing only in transport; a DMX sender uses a UART/RS-485 transport but is a driver because it sends the rendered buffer). If no, it's a **service**. -Services are **user-add/deletable children of the `Services` container**, the core-domain twin of the light pipeline's `Effects`/`Drivers`: a top-level container holding user-added children of one role. The firmware is identical whether or not the hardware is wired. The user adds the module when they solder a gyro on and removes it later, reusing the generic child add, replace, delete and persistence machinery; `Services` declares `acceptsChildRoles("service")`. Fixed device infrastructure lives under **System** instead, wired by code rather than user-added: identity, network, the inspection tool Tasks, and the board's I2C bus. That is the System/Services split. Direction is per-module, not a role: a service may read (gyro), write (relay), or both, so one `Service` role spans the category. Each is a header-only or `.h`+`.cpp` core module under `src/core/`, reaches hardware only through a domain-neutral platform primitive (`platform::i2c*`, `platform::audioMic*`, …), and gets a spec in `docs/moonmodules/core/services.md` (enforced by `check_specs.py`). Most poll in `tick20ms` or `tick1s`. The exception is a service whose data an effect consumes *every frame*: [AudioService](../../moonmodules/core/services.md#audio) reads and analyzes its I²S microphone in `tick()`, because the audio effects react per render tick. Its per-tick cost, one FFT, is part of the render budget. Automatic bus-probe detection is out of scope; the manual path is the foundation. +Services are **user-add/deletable children of the `Services` container**, the core-domain twin of the light pipeline's `Effects`/`Drivers`: a top-level container holding user-added children of one role. The firmware is identical whether or not the hardware is wired. The user adds the module when they solder a gyro on and removes it later, reusing the generic child add, replace, delete and persistence machinery; `Services` declares `acceptsChildRoles("service")`. Fixed device infrastructure lives under **System** instead, wired by code rather than user-added: identity, network, the inspection tool Tasks, and the board's I2C bus. That is the System/Services split. Direction is per-module, not a role: a service may read (gyro), write (relay), or both, so one `Service` role spans the category. Each is a header-only or `.h`+`.cpp` core module under `src/core/`, reaches hardware only through a domain-neutral platform primitive (`platform::i2c*`, `platform::audioMic*`, …), and gets a spec in `docs/moonmodules/core/services.md` (enforced by `check_specs.py`). Most poll in `tick20ms` or `tick1s`. The exception is a service whose data an effect consumes *every frame*: [AudioService](../../moonmodules/core/services.md#audio) reads and analyzes its I²S microphone in `tick()`, because the audio effects react per render tick. Its per-tick cost, one FFT, is part of the render budget. **An effect reads a service's data** through the shared-struct pull pattern from [Data exchange](moonmodule.md#data-exchange-between-modules), with no new mechanism. The service owns a small POD struct overwritten in place each poll or tick, and the consuming effect holds a `const` pointer to it. The first concrete case is audio: AudioService produces an `AudioFrame` (level + 16-band spectrum + peak) that [AudioSpectrumEffect](../../moonmodules/light/effects.md) and the other audio effects consume. It reaches the frame through a static `AudioService::latestFrame()` rather than a boot-time setter. That variation exists because an audio effect can be added through the UI *after* boot and must still find the one live mic (a setter only wired the boot instance). The active mic registers itself in `setup()` and clears the pointer in `release()`, so add/remove in any order returns either the live frame or a static silent one, never null. A service that only *displays* its readings (the gyro today) skips the consumer side entirely. @@ -66,7 +66,7 @@ Three domain-neutral services let several controllers act as one installation. T - **Discovery**: devices find each other by presence packets on a multicast group and the broadcast address WLED listens on, and `NetworkModule` advertises each over mDNS. Live. - **Shared controls**: boards follow each other's control surface over [OSC](../../moonmodules/core/services.md#osc), the industry-standard protocol for it. A surface change on one board goes to the others as feedback, by unicast to `hosts` or to a multicast group. Each writes it onto its own surface and may send it on once, back to its origin too. Only a changed value is sent, so the origin, already holding it, stops it there. Live. -- **Clock sync**: a shared monotonic clock, so boards with the same controls also render the same moment. The design is filed in [backlog/core](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287077324). +- **Clock sync**: a shared monotonic clock, so boards with the same controls also render the same moment. Its design is the backlog card Clock sync. What the synced clock is *for* is a domain question; the light domain's use of it (synced animation across a wall) is in [Multi-device sync](moonlight.md#multi-device-sync). diff --git a/docs/explanation/architecture/moonlight.md b/docs/explanation/architecture/moonlight.md index bdaf897e..f80a895f 100644 --- a/docs/explanation/architecture/moonlight.md +++ b/docs/explanation/architecture/moonlight.md @@ -261,7 +261,7 @@ Two rules separate those channels from color, and both matter: This is what makes a moving head reachable by the same pipeline as a pixel. The light domain produces one logical light per fixture, and the driver expands each into that fixture's channel block through the profile. It is also the cheapest thing to configure, since only the start address and the fixture type are needed, never a per-fixture address table. -The trade is deliberate: **a chain must be homogeneous**. Mixing fixture types on one universe, or leaving gaps between fixtures, has no expression in this model, and neither does a fixture whose address does not sit on the stride. Those need a per-fixture address map, which is the fixture-model work ([backlog](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287082264)). Until then, a mixed rig gives each fixture type its own driver instance with its own fixture profile, start address and count. +The trade is deliberate: **a chain must be homogeneous**. Mixing fixture types on one universe, or leaving gaps between fixtures, has no expression in this model, and neither does a fixture whose address does not sit on the stride. Those need a per-fixture address map, which is the fixture-model work ([backlog](https://github.com/MoonModules/MoonLight/issues/219)). Until then, a mixed rig gives each fixture type its own driver instance with its own fixture profile, start address and count. ## Multicast and IGMP snooping @@ -293,7 +293,7 @@ How lighting uses the core [multi-device runtime](mooncore.md#multi-device-runti - **The same effect on every board.** [Shared controls](mooncore.md#multi-device-runtime) carry the surface, and whatever the Control card assigns it to, so several boards run one effect with one set of controls. This is the controls half of supersync; the clock is the other. - **Synced visuals from the shared clock.** Effects animate off elapsed time ([Effects](#effects)), so a synced clock is what makes a wall of controllers animate in lockstep regardless of each one's frame rate. This is the light-domain payoff of the core clock sync. -- **Light distribution**: one device sending rendered light data to another uses the existing ArtNet / E1.31 / DDP standards. The ArtNet *driver* sends to fixtures; device-to-device distribution as a sync topology is filed in [backlog/core](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287077324). No bespoke protocol. +- **Light distribution**: one device sending rendered light data to another uses the existing ArtNet / E1.31 / DDP standards. The ArtNet *driver* sends to fixtures; device-to-device distribution as a sync topology is part of the backlog card Clock sync. No bespoke protocol. ## What bounds a parallel driver diff --git a/docs/explanation/architecture/moonwalk.md b/docs/explanation/architecture/moonwalk.md index 3158002f..ebef8b47 100644 --- a/docs/explanation/architecture/moonwalk.md +++ b/docs/explanation/architecture/moonwalk.md @@ -34,12 +34,33 @@ Running the same script from several processes is a call, its explanation living **Text and shape have caps, as the help's cards do.** Text past a cap goes to its Diátaxis kind rather than being squeezed: a table to a reference page, a reason to an explanation, a symptom to Troubleshooting, a second route to its own process. +**A step has no loop**, as Gherkin has none: a format with variables and repetition is a programming language, so work that repeats over boards or measures a spread stays a script a `run` step names. **A `call` gets no derived `then`**, since its result is not in its action; one is written by hand where the call changes the device. **A process no runner verifies is listed, not counted**, since a count of them could never fall: they are the ones a person alone can judge. + ## Done is observed **A step is done when its `then` holds now.** The app reads its own state, so "WiFi is set up" is done the moment a network is known, however it got there. This is the onboarding checklist's textbook rule, completion observed rather than recorded, and it needs no storage. A process is done when every expectation the state can answer holds, and it has at least one: its last step alone is not enough, since a last step is often one the state cannot see. What the state cannot show is vouched for by whoever knows it. The installer marks the install done, since it knows the platform for certain where a running device cannot tell a desktop from a Linux board or a container. MoonDeck marks a process done when every script its steps run last passed, and a run counts only while it is newer than the sources it covers, as a firmware build does. Those marks stay in the browser. The website cannot read a device, since an https page cannot call a device's http address, so there only the reader's marks count. +## The definition in the repository, the runs on GitHub + +A process file is the definition and one run of it is an instance, the split workflow engines make. The repository keeps the one definition; GitHub keeps the instances and the work. It is no process engine, editor or server: a schema, the process files, one generator and an adapter in each runner. + +- **Two kinds of step.** A volatile step acts on a device or in the app, and nothing records it. A recorded step changes the repository or what ships, such as a commit gate, a merge, a release or a verification on hardware, and leaves a record of its run. The kind follows from the process, not the action: every gate process MoonDeck lists, plus `pick-spec-plan` and `contribute-pr`, records its steps. +- **Every CI job runs locally too.** A workflow is a thin caller of the MoonDeck script its process step names, which `test_workflows.py` holds it to, so GitHub is one runner of the processes among others: [run_process](../../../moondeck/MoonDeck.md#run_process) runs a process's script steps, and [act](https://github.com/nektos/act) runs the Linux jobs in containers. Only publishing (the release upload, Pages, the container registry) is GitHub's alone. +- **What copies a process is generated, what points at it is written.** The pull request template is generated from `contribute-pr`'s questions, the bug form from Log an issue; `CONTRIBUTING.md` is one sentence linking the process. +- **The work is the Project's cards**, mirrored into `docs/work/backlog_mirror/` by [project_mirror](../../../moondeck/MoonDeck.md#project_mirror), so git versions every card edit. A card accepted moves to Next and becomes an issue. A doc links an issue, the item's own or a tracking issue listing its card, since GitHub keeps an issue's address and not a card's, as [Pick, spec and plan a topic](../../moonwalk.md#pick-spec-plan) describes, and a plan that ships or stops becomes a closed issue labeled `plan`. A card's Process field, rather than a label per process, links work to the process it serves, and the map lists each process's open cards from it. + +| Project field | Holds | +|---|---| +| Status | Inbox, Backlog, Next, In progress, Waiting (blocked on someone outside the project: a reply, a part, a tester), In review, Done | +| Area | the `src/` tree and the module roles, plus Process and docs | +| Module | the module's class name, empty for an item across modules | +| Kind | Bug, Feature, Restructure, Investigation | +| Trigger | what makes the item worth building, so a met trigger is found by scanning one column | +| Platform | classic, S3, P4, C3, Teensy, desktop, Pi, all | +| Stage, Process | the MoonWalk stage and process the item serves | + ## The map `moonwalk.js` draws `moonwalk.json` with Cytoscape.js, from cdnjs and about 430 KB, so it never enters the firmware. The layout is the story map's grid, stages as columns and processes stacked in `dependsOn` order, computed in a few lines rather than by a layout library. The parts of a page that are reference rather than steps, such as a table of ports or a test strategy, stay on a reference page and are linked. diff --git a/docs/help/help.json b/docs/help/help.json index 6bf19017..336672ac 100644 --- a/docs/help/help.json +++ b/docs/help/help.json @@ -4261,7 +4261,7 @@ "guide": [ { "title": "What a row does", - "text": "Each row binds one pin to a target, a surface control such as `Control.switch1`. A catalog board's first button targets `Control.switch1`, which switches the lights and their relay. The live pressed readout shows a button working before it is bound." + "text": "Each row binds one pin to a surface control; a catalog board's first button targets `Control.switch1`, the lights and their relay. The pressed readout shows a button working before it is bound.\n\nA push button takes `toggle`, each press flipping the target. A latching switch takes `set` with value 1: on while closed, off once open." } ], "image": "https://moonmodules.org/MoonLight/assets/core/ButtonService.png", diff --git a/docs/reference/backup-and-restore.md b/docs/reference/backup-and-restore.md index 7efb9880..cf141d7a 100644 --- a/docs/reference/backup-and-restore.md +++ b/docs/reference/backup-and-restore.md @@ -10,6 +10,23 @@ The browser walks the whole filesystem. Every file is byte-verified against the 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. +## Restore or clone + +Choosing a bundle asks what it is for. **Restore** puts a device back as the backup was, its identity included: the same device after an erase or a failed update. **Clone** makes a device another one like the backup, as a cloned disk image gets its own machine identity: the device keeps what makes it itself and takes everything else. + +| Kept by a clone | Why it differs per device | +|---|---| +| `deviceName` | its network name: the mDNS hostname, the access point and the DHCP hostname all derive from it | +| WiFi's known networks | each with its password, which stays on the device it was typed into | +| Ethernet's static IP settings | two devices claiming one address both drop off the network | +| the desktop's stored hardware address | the default device name derives from it | + +What differs per device beyond its identity, such as each pole's Art-Net universe start, is set after the clone, by hand or through [the HTTP API](integrating.md). + +## Many devices alike + +A device's setup is two parts, and each already has its tool. The **board** is its device model: the [installer](https://moonmodules.org/MoonLight/install/) applies a model's pins, drivers and layout while it flashes. The **show** is everything else: clone it from a backup of the first device, or apply it as a [preset](presets.md), a state document a click recalls and the gallery shares. Flash each device with its model, then clone or apply the show onto it. + ## What a restore reports The report lists what needed an eye: @@ -20,7 +37,7 @@ The report lists what needed an eye: Most of a restore applies live, module by module, as each file lands. Two things do not, and the dialog names both: -- **Network settings** apply at boot, because bringing an interface up is not a re-runnable operation. The dialog offers the restart, after which the device joins the network the backup names. Its file is written **last** for that reason: joining another network mid-restore would cut off the writes still to come. +- **Network settings** apply at boot, because bringing an interface up is not a re-runnable operation. The dialog offers the restart, after which a restored device joins the network the backup names and a cloned one rejoins its own. Its file is written **last** for that reason: joining another network mid-restore would cut off the writes still to come. - **The web server's port**, which binds at boot. ## Why the browser does the work diff --git a/docs/reference/build-system.md b/docs/reference/build-system.md index fe313537..f8aff432 100644 --- a/docs/reference/build-system.md +++ b/docs/reference/build-system.md @@ -83,7 +83,7 @@ Some variants carry [MoonBase](../explanation/architecture/moonbase.md), the sec ### The v6 ecosystem -Anything the IDF makes the recognized way to build, install, provision or ship is the path to take rather than a bespoke one. It is adopted step by step, each its own commit and hardware re-test, and only once it clears the v6.0 floor. None may leak an IDF assumption into shared `src/` or the desktop and Teensy builds. The plan per item is in [backlog/platform](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287085832). +Anything the IDF makes the recognized way to build, install, provision or ship is the path to take rather than a bespoke one. It is adopted step by step, each its own commit and hardware re-test, and only once it clears the v6.0 floor. None may leak an IDF assumption into shared `src/` or the desktop and Teensy builds. The plan per item is in [backlog/platform](https://github.com/MoonModules/MoonLight/issues/220). | Change | Where MoonLight stands | |---|---| diff --git a/docs/reference/hardware/gpio-usage.md b/docs/reference/hardware/gpio-usage.md index aa1ae5aa..ea4f5de0 100644 --- a/docs/reference/hardware/gpio-usage.md +++ b/docs/reference/hardware/gpio-usage.md @@ -22,7 +22,7 @@ For **LED output** specifically — the pins a WS2812-class strand data line can | **ESP32-P4 (P4-NANO)** | 20, 21, 22, 23, 24, 25, 26, 27, 32, 33, 39-48 | 20 | Board-specific: the NANO commits eth-RMII (28-31/49-52), C6-SDIO (14-19/54), I2C (7-8), UART0 (37-38). A carrier board changes the exposed set — the MHC-WLED shield routes through transceivers (no bare GPIO). | | **ESP32-S31 (coreboard)** | take from the [coreboard reference](esp32-s31-coreboard.md) free set | — | Most of the header is committed to on-board RGMII eth + ES8311 codec + SD + USB-host; don't guess. | -**Mapping these to actual device-model defaults** (which of a board's usable pins to pre-fill in `deviceModels.json`) is a separate, per-board step: the catalog defaults one strip for a bare dev board (the user wires the rest), or the exact wired set for a purpose-built board (QuinLED Dig-Octa: `0,1,2,3,4,5,12,13`). That per-model work is tracked in [backlog/core § LED output pins](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287077175). +**Mapping these to actual device-model defaults** (which of a board's usable pins to pre-fill in `deviceModels.json`) is a separate, per-board step: the catalog defaults one strip for a bare dev board (the user wires the rest), or the exact wired set for a purpose-built board (QuinLED Dig-Octa: `0,1,2,3,4,5,12,13`). That per-model work is tracked in [backlog/core § LED output pins](https://github.com/MoonModules/MoonLight/issues/220). ## ESP32 (classic) diff --git a/docs/reference/hardware/light-fixtures.md b/docs/reference/hardware/light-fixtures.md index c216941e..2fa6de9c 100644 --- a/docs/reference/hardware/light-fixtures.md +++ b/docs/reference/hardware/light-fixtures.md @@ -41,7 +41,7 @@ That is a DIFFERENT model: this one is 11/13 channels with a plain linear dimmer | 10 | 0-255 | blue, 0 = closed | `B` | | 11 | 0-255 | white, 0 = closed | `W` | -CH6 is a straight linear dimmer. The driver holds it fully open and keeps brightness in the color values; routing brightness onto it is the better model and is [backlogged](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080535). The channels left unmapped hold at 0, which is what a light driver wants: **strobe off** (CH7) and full-speed movement (CH5). The fine channels repeat the coarse byte, so a full-scale aim reaches the fixture's 65535; true 16-bit positioning waits on a 16-bit aim. +CH6 is a straight linear dimmer. The driver holds it fully open and keeps brightness in the color values; routing brightness onto it is the better model and is [backlogged](https://github.com/MoonModules/MoonLight/issues/219). The channels left unmapped hold at 0, which is what a light driver wants: **strobe off** (CH7) and full-speed movement (CH5). The fine channels repeat the coarse byte, so a full-scale aim reaches the fixture's 65535; true 16-bit positioning waits on a 16-bit aim. Two things matter when driving it by hand. Every channel at 255 makes the fixture **strobe**, because CH7 is a strobe channel rather than part of the dimmer. And the 13-channel mode's CH12 at 250-255 selects **sound mode**, where the fixture ignores DMX movement and runs its own program. diff --git a/docs/reference/hardware/mhc-wled-esp32-p4-shield.md b/docs/reference/hardware/mhc-wled-esp32-p4-shield.md index 227acd09..672492d6 100644 --- a/docs/reference/hardware/mhc-wled-esp32-p4-shield.md +++ b/docs/reference/hardware/mhc-wled-esp32-p4-shield.md @@ -93,4 +93,4 @@ The loopback self-test drives a WS2812 frame out one pin and reads it back on a ## Cross-reference -Chip-level GPIO constraints (straps, flash/PSRAM) for the P4 are in [gpio-usage.md § ESP32-P4](gpio-usage.md#esp32-p4); the sections here are the *board* wiring. The catalog entry is [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json) (`MHC-WLED ESP32-P4 shield`). RS-485 / DMX-512 as a first-class MoonLight output is tracked in the [RS-485 / DMX-512 wired-output backlog item](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080691). +Chip-level GPIO constraints (straps, flash/PSRAM) for the P4 are in [gpio-usage.md § ESP32-P4](gpio-usage.md#esp32-p4); the sections here are the *board* wiring. The catalog entry is [`mooninstaller/deviceModels.json`](../../mooninstaller/deviceModels.json) (`MHC-WLED ESP32-P4 shield`). RS-485 / DMX-512 as a first-class MoonLight output is tracked in the [RS-485 / DMX-512 wired-output backlog item](https://github.com/MoonModules/MoonLight/issues/219). diff --git a/docs/reference/hardware/panel-cards.md b/docs/reference/hardware/panel-cards.md index 784f7321..dd5f72fe 100644 --- a/docs/reference/hardware/panel-cards.md +++ b/docs/reference/hardware/panel-cards.md @@ -1,6 +1,6 @@ # Driving LED panels with a receiving card -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. +You bought a panel receiving card, most likely a **ColorLight** one, the family MoonLight supports. The MoonLight steps are in [Drive LED panels through a receiving card](../../moonwalk.md#panel-cards), with [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, how the card itself is set up, and what to check when the wall stays dark. > New here? Start with **[Install MoonLight](../../moonwalk.md#install)**, then **[How MoonLight works](../../explanation/how-moonlight-works.md)**. @@ -64,7 +64,7 @@ The panel-card firmware is already on for these boards. A classic-ESP32 build do 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 [LEDVision](../../moonwalk.md#panel-card-ledvision). 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](#8-set-the-card-up-in-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. @@ -163,16 +163,42 @@ The mismatch is not subtle in one direction: leave a downgraded card on `v13 and --- -## 8. When it does not light up +## 8. Set the card up in LEDVision + +The card stores its panel size, count and driver IC, written once with ColorLight's [LEDVision](https://en.colorlightinside.com/product/download/380) on Windows. MoonLight then needs no panel settings. Use **8.8**, the last version with net card output; 8.5 works as well. Labels move between versions, so the wording below is approximate. The example is two 128x64 panels stacked, 128x128 in all. + +**Intelligent Setting defines one panel.** The cabinet size, the data group order and the receiver mapping after it define the wall, and a wall that stays wrong after Intelligent Setting is usually one of those three. + +**A module is one data group, not one panel.** A HUB75 panel has two sets of color lines: RGB1 drives its top half and RGB2 its bottom half. So a 128x64 panel is two data groups of 128x32 each, at 32 scan, and LEDVision counts in data groups. + +1. **Connect** the card to the machine's Ethernet port. Power the panels through their own power pins, never the HUB75 ribbon. Use the outputs in order: the first panel on J1, the next on J2. +2. **Open the receiving-card setup**, usually under `Settings`. Some versions ask for a password: `168` or `admin`. Choose **net card** as the sending method. +3. **Run Intelligent Setting.** Pick the driver IC printed on the panel's chips, such as FM6124, and complete every step of the guide. In its pixel grid, click pixels 1 to 128 in the first row, then click each row below it, and every row fills in. A less common panel can load its supplier's .rcfgx or .rcvx file instead. +4. **Check the module.** Intelligent module settings reads width 128, height 64, and shows `1-1` above `2-1`. +5. **Set the cabinet size**: the pixels this card drives, 128 x 128. +6. **Set the data group order** in data group swap, as the ribbons chain: J1 on the top panel and J2 on the bottom one reads 1, 2, 3, 4 downward. Mirrored rows or misplaced panels are fixed here. +7. **Set the receiver mapping**: the card's area reads 128 x 128. Apply. +8. **Send to receivers, twice**: once on the receiver parameters tab, and again on the receiver mapping tab. LEDVision holds the settings only while it is open, and each tab writes its own part. +9. **Save on the card**, a separate action from sending in most versions. Skipped, the wall comes back wrong after a power cycle. +10. **Run a LEDVision test pattern.** It fills the wall, which proves the card before MoonLight sends anything. + +**Pixel noise on longer ribbons is the panel clock.** Lower DCLK in the receiver parameters and send again; tuning the SCK duty ratio and blanking phase brings it back up, to 25 MHz on 50 cm ribbons. + +A video of the card setup, driven from FPP: [Setting up a Colorlight Card with FPP v6.3 and LED Vision 8.5](https://www.youtube.com/watch?v=L4lHbwUszAs). + +--- + +## 9. When the wall stays dark | Symptom | Look at | |---|---| | `no ethernet link` | On a desktop, check `interface` first: a string that matches no adapter fails the bind and reports exactly this, with nothing to distinguish it from an unplugged cable. Then cable seated, card powered, and plugged into the card's **input** port. On a 100 Mbit controller, try a gigabit switch in between. | | Status healthy, panels dark | The Layout, not the driver. If the wall is 0 lights, or the driver's window (`start`/`count`) selects none, there is nothing to send. | -| Image tears or rolls | Link speed ([§2](#the-one-hardware-fact-that-decides-everything)). Check the status line says 1000 Mbit. | +| Image tears or rolls | Link speed ([§2](#the-one-hardware-fact-that-decides-everything)). Check the status line says 1000 Mbit. On a desktop, a USB network adapter has been seen to tear where a built-in port does not. | +| Lit when cabled directly, dark through several switches | The frames are raw Ethernet, and the cards react badly to other traffic. Carry them in one VLAN: the first switch gives the untagged port from the controller a PVID, the switches pass it tagged between them, and the last strips the tag on the card's port. | | 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 ([LEDVision](../../moonwalk.md#panel-card-ledvision)). 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 the data group order in LEDVision ([§8](#8-set-the-card-up-in-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/reference/metrics/prose.json b/docs/reference/metrics/prose.json index bd246144..1281dc39 100644 --- a/docs/reference/metrics/prose.json +++ b/docs/reference/metrics/prose.json @@ -1,13 +1,13 @@ { - "(total)": 2017, + "(total)": 2013, "MoonLight.EmDash": 1106, "MoonLight.LinkText": 7, - "MoonLight.NegatedHeading": 6, - "MoonLight.SectionLength": 42, + "MoonLight.NegatedHeading": 5, + "MoonLight.SectionLength": 41, "MoonLight.SelfReference": 16, "MoonLight.Spelling": 39, "MoonLight.Temporal": 170, "MoonLight.TonePadding": 1, - "MoonLight.VagueClose": 28, - "MoonLight.Weasel": 602 + "MoonLight.VagueClose": 27, + "MoonLight.Weasel": 601 } diff --git a/docs/reference/metrics/repo-health.json b/docs/reference/metrics/repo-health.json index f228e4b4..e5ea4cb0 100644 --- a/docs/reference/metrics/repo-health.json +++ b/docs/reference/metrics/repo-health.json @@ -1,44 +1,49 @@ { - "commit": "76bfadbf", + "commit": "4e653f4d", "flash": { - "esp32s3-n16r8": 2355408, - "esp32": 2145104, - "esp32p4rev1-eth": 2416496, - "esp32p4rev1-eth-wifi": 2669040, - "esp32s3-n8r8": 2331664, - "esp32s31": 2659712, - "esp32-16mb": 2399680, - "esp32-eth": 1681040, - "esp32-wrover": 1843760, + "esp32s3-n16r8": 2355984, + "esp32": 2145792, + "esp32p4rev1-eth": 2418032, + "esp32p4rev1-eth-wifi": 2670560, + "esp32s3-n8r8": 2355984, + "esp32s31": 2660656, + "esp32-16mb": 2263392, + "esp32-eth": 1747760, + "esp32-wrover": 2196080, "qemu": 1383648, - "esp32p4rev3-eth": 2414048, - "esp32s3-zero": 2117296, - "esp32-pico": 2107168, - "desktop-macos": 2274600, + "esp32p4rev3-eth": 2422832, + "esp32s3-zero": 2125456, + "esp32-pico": 2316864, + "desktop-macos": 2274648, "desktop-windows": 1925120, - "esp32c3": 2251520, + "esp32c3": 2252320, "desktop-linux": 2548000, - "esp32s3-n32r16": 2377888, - "teensy41": 1167304 + "esp32s3-n32r16": 2378864, + "teensy41": 1168328, + "esp32p4rev3-eth-wifi": 2675184, + "esp32s3-n8r2": 2247072 }, "measured": { - "esp32p4rev1-eth": "2026-10-27", - "esp32s31": "2026-10-27", - "esp32": "2026-10-27", - "esp32-pico": "2026-09-09", - "esp32s3-n16r8": "2026-10-27", - "esp32s3-n8r8": "2026-10-25", - "esp32s3-zero": "2026-10-25", - "esp32-16mb": "2026-10-22", - "esp32p4rev1-eth-wifi": "2026-10-27", - "esp32-eth": "2026-09-11", - "esp32p4rev3-eth": "2026-10-25", - "desktop-macos": "2026-10-27", + "esp32p4rev1-eth": "2026-10-30", + "esp32s31": "2026-10-30", + "esp32": "2026-10-30", + "esp32-pico": "2026-10-30", + "esp32s3-n16r8": "2026-10-30", + "esp32s3-n8r8": "2026-10-30", + "esp32s3-zero": "2026-10-30", + "esp32-16mb": "2026-10-30", + "esp32p4rev1-eth-wifi": "2026-10-30", + "esp32-eth": "2026-10-30", + "esp32p4rev3-eth": "2026-10-30", + "desktop-macos": "2026-10-30", "desktop-windows": "2026-09-30", - "esp32c3": "2026-10-27", + "esp32c3": "2026-10-30", "desktop-linux": "2026-10-12", - "esp32s3-n32r16": "2026-10-27", - "teensy41": "2026-10-27" + "esp32s3-n32r16": "2026-10-30", + "teensy41": "2026-10-30", + "esp32-wrover": "2026-10-30", + "esp32p4rev3-eth-wifi": "2026-10-30", + "esp32s3-n8r2": "2026-10-30" }, "perf": { "esp32": { @@ -795,8 +800,8 @@ } }, "desktop-macos": { - "tick_us": 7, - "fps": 142857, + "tick_us": 8, + "fps": 125000, "scenario_p50": { "Effects_pipeline_builds_and_renders": { "p50": 7, @@ -836,20 +841,20 @@ } }, "loc": { - "core": 33397, - "light": 31681, + "core": 33413, + "light": 31677, "platform": 22993, - "ui": 13043, - "test": 76140, - "moondeck": 32680 + "ui": 13138, + "test": 76758, + "moondeck": 33648 }, "comments": { "core": { - "lines": 6760, + "lines": 6762, "ratio": 0.224 }, "light": { - "lines": 6660, + "lines": 6655, "ratio": 0.236 }, "platform": { @@ -857,32 +862,32 @@ "ratio": 0.206 }, "ui": { - "lines": 3481, - "ratio": 0.283 + "lines": 3491, + "ratio": 0.281 }, "test": { - "lines": 7505, - "ratio": 0.114 + "lines": 7520, + "ratio": 0.113 }, "moondeck": { - "lines": 4420, - "ratio": 0.155 + "lines": 4470, + "ratio": 0.153 } }, "tests": { - "cases": 2836, + "cases": 2841, "scenarios": 36 }, "docs": { - "md_files": 170, - "md_lines": 31236, - "plans_files": 59, - "backlog_lines": 4001, - "lessons_lines": 546, + "md_files": 114, + "md_lines": 18090, + "plans_files": 10, + "backlog_lines": 2996, + "lessons_lines": 447, "claude_md_lines": 119 }, "complexity": { - "functions": 5520, + "functions": 5521, "over_threshold": 345, "worst_ccn": 128 } diff --git a/docs/reference/metrics/repo-health.md b/docs/reference/metrics/repo-health.md index ccd85bee..1248ad76 100644 --- a/docs/reference/metrics/repo-health.md +++ b/docs/reference/metrics/repo-health.md @@ -1,63 +1,71 @@ # Repo health -Measured at `76bfadbf`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `4e653f4d`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** Current state only; the trend is this file's git history (`git log -p docs/reference/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. -## Since v6.0.0 +## Since v7.0.0 -| Metric | v6.0.0 | Now | Change | +| Metric | v7.0.0 | Now | Change | |---|---:|---:|---:| -| flash desktop-linux | - | 2,488 KB | new | -| flash desktop-macos | 1,963 KB | 2,221 KB | +258.3 KB (+13.2%) | -| flash esp32 | 2,061 KB | 2,095 KB | +33.6 KB (+1.6%) | -| flash esp32-16mb | 2,012 KB | 2,343 KB | +331.4 KB (+16.5%) | -| flash esp32c3 | - | 2,199 KB | new | -| flash esp32p4rev1-eth | 2,022 KB | 2,360 KB | +338.1 KB (+16.7%) | -| flash esp32p4rev1-eth-wifi | 2,304 KB | 2,606 KB | +302.9 KB (+13.1%) | -| flash esp32p4rev3-eth | 2,025 KB | 2,357 KB | +332.2 KB (+16.4%) | -| flash esp32s3-n16r8 | 2,111 KB | 2,300 KB | +188.7 KB (+8.9%) | -| flash esp32s3-n32r16 | - | 2,322 KB | new | -| flash esp32s3-n8r8 | 2,038 KB | 2,277 KB | +238.8 KB (+11.7%) | -| flash esp32s3-zero | 1,977 KB | 2,068 KB | +90.9 KB (+4.6%) | -| flash esp32s31 | 2,395 KB | 2,597 KB | +202.0 KB (+8.4%) | -| flash teensy41 | - | 1,140 KB | new | +| flash desktop-macos | 2,221 KB | 2,221 KB | +0.0 KB (+0.0%) | +| flash esp32 | 2,095 KB | 2,096 KB | +0.7 KB (+0.0%) | +| flash esp32-16mb | 2,343 KB | 2,210 KB | -133.1 KB (-5.7%) | +| flash esp32-eth | 1,642 KB | 1,707 KB | +65.2 KB (+4.0%) | +| flash esp32-pico | 2,058 KB | 2,263 KB | +204.8 KB (+10.0%) | +| flash esp32-wrover | 1,801 KB | 2,145 KB | +344.1 KB (+19.1%) | +| flash esp32c3 | 2,199 KB | 2,200 KB | +0.8 KB (+0.0%) | +| flash esp32p4rev1-eth | 2,360 KB | 2,361 KB | +1.5 KB (+0.1%) | +| flash esp32p4rev1-eth-wifi | 2,606 KB | 2,608 KB | +1.5 KB (+0.1%) | +| flash esp32p4rev3-eth | 2,357 KB | 2,366 KB | +8.6 KB (+0.4%) | +| flash esp32p4rev3-eth-wifi | - | 2,612 KB | new | +| flash esp32s3-n16r8 | 2,300 KB | 2,301 KB | +0.6 KB (+0.0%) | +| flash esp32s3-n32r16 | 2,322 KB | 2,323 KB | +1.0 KB (+0.0%) | +| flash esp32s3-n8r2 | - | 2,194 KB | new | +| flash esp32s3-n8r8 | 2,277 KB | 2,301 KB | +23.8 KB (+1.0%) | +| flash esp32s3-zero | 2,068 KB | 2,076 KB | +8.0 KB (+0.4%) | +| flash esp32s31 | 2,597 KB | 2,598 KB | +0.9 KB (+0.0%) | +| flash teensy41 | 1,140 KB | 1,141 KB | +1.0 KB (+0.1%) | +| free heap esp32 | 33,034 KB | 33,034 KB | +0.0 KB (+0.0%) | | tick Effects_pipeline_builds_and_renders (desktop-macos) | 7 µs | 7 µs | +0 (+0.0%) | -| tick Layouts_resize_reallocates_live (desktop-macos) | 12 µs | 10 µs | -2 (-16.7%) | -| lines core | 22,299 | 33,397 | +11,098 (+49.8%) | -| lines light | 31,054 | 31,681 | +627 (+2.0%) | -| lines platform | 16,798 | 22,993 | +6,195 (+36.9%) | -| lines ui | 11,394 | 13,043 | +1,649 (+14.5%) | -| lines test | 57,759 | 76,140 | +18,381 (+31.8%) | -| lines moondeck | 29,325 | 32,680 | +3,355 (+11.4%) | -| unit tests | 2,120 | 2,836 | +716 (+33.8%) | -| scenarios | 21 | 36 | +15 (+71.4%) | - -Against the release `v6.0.0`, the fixed point a user compares with; the tables below compare against the last commit. A firmware not rebuilt since `v6.0.0` is left out, and a measure the release did not record yet, such as the code check, joins from the next one. +| tick Layouts_resize_reallocates_live (desktop-macos) | 10 µs | 10 µs | +0 (+0.0%) | +| lines core | 33,397 | 33,413 | +16 (+0.0%) | +| lines light | 31,681 | 31,677 | -4 (-0.0%) | +| lines platform | 22,993 | 22,993 | +0 (+0.0%) | +| lines ui | 13,043 | 13,138 | +95 (+0.7%) | +| lines test | 76,140 | 76,758 | +618 (+0.8%) | +| lines moondeck | 32,680 | 33,648 | +968 (+3.0%) | +| code findings | 1,642 | 1,638 | -4 (-0.2%) | +| unit tests | 2,836 | 2,841 | +5 (+0.2%) | +| scenarios | 36 | 36 | +0 (+0.0%) | + +Against the release `v7.0.0`, the fixed point a user compares with; the tables below compare against the last commit. A firmware not rebuilt since `v7.0.0` is left out, and a measure the release did not record yet, such as the code check, joins from the next one. ## Firmware size | Target | Flash | Capacity | Used | Built | |---|---:|---:|---:|:--:| -| desktop-linux | 2,488 KB | - | - | **STALE 15d** | -| desktop-macos | 2,221 KB | - | - | yes | -| desktop-windows | 1,880 KB | - | - | **STALE 27d** | -| esp32 | 2,095 KB (+3 KB) ⚠ | 2,496 KB | 84% | yes | -| esp32-16mb | 2,343 KB | 4,096 KB | 57% | carried 5d | -| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 46d** | -| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 48d** | -| esp32-wrover | 1,801 KB | 2,496 KB | 72% | carried (age?) | -| esp32c3 | 2,199 KB (+4 KB) ⚠ | 2,496 KB | 88% | yes | -| esp32p4rev1-eth | 2,360 KB (+496 B) ⚠ | 4,096 KB | 58% | yes | -| esp32p4rev1-eth-wifi | 2,606 KB (+3 KB) ⚠ | 4,096 KB | 64% | yes | -| esp32p4rev3-eth | 2,357 KB | 4,096 KB | 58% | carried 2d | -| esp32s3-n16r8 | 2,300 KB (+288 B) ⚠ | 4,096 KB | 56% | yes | -| esp32s3-n32r16 | 2,322 KB | 4,096 KB | 57% | carried 0d | -| esp32s3-n8r8 | 2,277 KB | 3,072 KB | 74% | carried 2d | -| esp32s3-zero | 2,068 KB | 2,496 KB | 83% | carried 2d | -| esp32s31 | 2,597 KB (−82 KB) ✓ | 4,096 KB | 63% | yes | +| desktop-linux | 2,488 KB | - | - | **STALE 18d** | +| desktop-macos | 2,221 KB (+48 B) ⚠ | - | - | yes | +| desktop-windows | 1,880 KB | - | - | **STALE 30d** | +| esp32 | 2,096 KB (+688 B) ⚠ | 2,496 KB | 84% | yes | +| esp32-16mb | 2,210 KB (−133 KB) ✓ | 4,096 KB | 54% | yes | +| esp32-eth | 1,707 KB (+65 KB) ⚠ | 2,496 KB | 68% | yes | +| esp32-pico | 2,263 KB (+205 KB) ⚠ | 3,072 KB | 74% | yes | +| esp32-wrover | 2,145 KB (+344 KB) ⚠ | 2,496 KB | 86% | yes | +| esp32c3 | 2,200 KB (+800 B) ⚠ | 2,496 KB | 88% | yes | +| esp32p4rev1-eth | 2,361 KB (+2 KB) ⚠ | 4,096 KB | 58% | yes | +| esp32p4rev1-eth-wifi | 2,608 KB (+1 KB) ⚠ | 4,096 KB | 64% | yes | +| esp32p4rev3-eth | 2,366 KB (+9 KB) ⚠ | 4,096 KB | 58% | yes | +| esp32p4rev3-eth-wifi | 2,612 KB | 4,096 KB | 64% | yes | +| esp32s3-n16r8 | 2,301 KB (+576 B) ⚠ | 4,096 KB | 56% | yes | +| esp32s3-n32r16 | 2,323 KB (+976 B) ⚠ | 4,096 KB | 57% | yes | +| esp32s3-n8r2 | 2,194 KB | 3,072 KB | 71% | yes | +| esp32s3-n8r8 | 2,301 KB (+24 KB) ⚠ | 3,072 KB | 75% | yes | +| esp32s3-zero | 2,076 KB (+8 KB) ⚠ | 2,496 KB | 83% | yes | +| esp32s31 | 2,598 KB (+944 B) ⚠ | 4,096 KB | 63% | yes | | qemu | 1,351 KB | - | - | carried (age?) | -| teensy41 | 1,140 KB | 3,456 KB | 33% | yes | +| teensy41 | 1,141 KB (+1 KB) ⚠ | 3,456 KB | 33% | yes | `Built: yes` was measured this run. `carried (age?)` was not rebuilt either and predates this record, so its age is unknown: it dates itself on the next build. `carried Nd` was NOT rebuilt and its number is N days old, so an absent delta says nothing about the change. **STALE** marks a carry older than 7 days: the number has gone unchecked long enough that growth will surface later as one jump, blamed on whichever commit happens to rebuild that target. `Used` is against the app slot in the firmware's own partition table. @@ -65,21 +73,29 @@ Against the release `v6.0.0`, the fixed point a user compares with; the tables b | Target | Net | New | Removed | Grown | Shrunk | Other | Since | |---|---:|---:|---:|---:|---:|---:|---| -| esp32 | +3.3 KB | +4.1 KB | -4.1 KB | +4.7 KB | -1.4 KB | -0.1 KB | `76bfadbf` | -| esp32c3 | +3.6 KB | +3.8 KB | -3.6 KB | +4.8 KB | -1.3 KB | -0.1 KB | `76bfadbf` | -| esp32p4rev1-eth | +0.5 KB | +0.5 KB | -1.5 KB | +3.5 KB | -1.9 KB | -0.1 KB | `76bfadbf` | -| esp32p4rev1-eth-wifi | +2.7 KB | +2.7 KB | -3.2 KB | +4.8 KB | -1.4 KB | -0.1 KB | `76bfadbf` | -| esp32s3-n16r8 | +0.3 KB | +0.1 KB | +0.0 KB | +0.5 KB | -0.2 KB | -0.0 KB | `76bfadbf` | -| esp32s31 | -82.3 KB | +213.1 KB | -305.3 KB | +35.5 KB | -25.8 KB | +0.2 KB | `76bfadbf` | +| esp32 | +0.7 KB | +0.3 KB | -0.9 KB | +1.4 KB | -0.2 KB | +0.1 KB | `4e653f4d` | +| esp32-16mb | -133.1 KB | +50.6 KB | -102.8 KB | +47.2 KB | -128.0 KB | -0.2 KB | `4e653f4d` | +| esp32c3 | +0.8 KB | +0.4 KB | -0.9 KB | +1.2 KB | -0.1 KB | +0.1 KB | `4e653f4d` | +| esp32p4rev1-eth | +1.5 KB | +0.6 KB | -0.9 KB | +1.7 KB | -0.1 KB | +0.2 KB | `4e653f4d` | +| esp32p4rev1-eth-wifi | +1.5 KB | +0.6 KB | -0.9 KB | +1.7 KB | -0.1 KB | +0.2 KB | `4e653f4d` | +| esp32p4rev3-eth | +8.6 KB | +8.1 KB | -5.7 KB | +11.3 KB | -5.4 KB | +0.3 KB | `4e653f4d` | +| esp32s3-n16r8 | +0.6 KB | +0.3 KB | -0.8 KB | +1.0 KB | -0.0 KB | +0.1 KB | `4e653f4d` | +| esp32s3-n32r16 | +1.0 KB | +0.8 KB | -0.8 KB | +2.4 KB | -1.4 KB | -0.0 KB | `4e653f4d` | +| esp32s3-n8r8 | +23.8 KB | +17.5 KB | -3.3 KB | +9.4 KB | -5.3 KB | +5.5 KB | `4e653f4d` | +| esp32s31 | +0.9 KB | +0.4 KB | -0.9 KB | +1.3 KB | +0.0 KB | +0.1 KB | `4e653f4d` | Largest moves: -- **esp32**: `void mm::NetworkReceiveEffect::drainSockets(int)` +2,195 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,691 B, `mm::NetworkReceiveEffect::receiveTrampoline(void*)` +1,671 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,179 B, `void mm::NetworkSendDriver::sendFrame(unsigned char c` +1,092 B -- **esp32c3**: `void mm::NetworkReceiveEffect::drainSockets(int)` +1,910 B, `mm::NetworkReceiveEffect::receiveTrampoline(void*)` +1,690 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,238 B, `void mm::NetworkSendDriver::sendFrame(unsigned char c` +1,202 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,090 B -- **esp32p4rev1-eth**: `mm::NetworkReceiveEffect::receiveTrampoline(void*)` +1,714 B, `void mm::NetworkReceiveEffect::drainSockets(int)` -1,580 B, `mm::MappingLUT::startRuns()` +400 B, `mm::NetworkSendDriver::onCorrectionChanged()` +394 B, `mm::NetworkSendDriver::setSourceBuffer(mm::Buffer*)` +390 B -- **esp32p4rev1-eth-wifi**: `void mm::NetworkReceiveEffect::drainSockets(int)` +2,078 B, `mm::NetworkReceiveEffect::receiveTrampoline(void*)` +1,728 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,476 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,290 B, `mm::MappingLUT::startRuns()` +400 B -- **esp32s3-n16r8**: `mm::ui::file1` +227 B, `mm::NetworkReceiveEffect::countGap(unsigned short, unsigned ` +104 B, `void mm::NetworkReceiveEffect::drainSockets(int)` -96 B, `mm::NetworkReceiveEffect::receiveTrampoline(void*)` -80 B, `void mm::NetworkSendDriver::sendFrame(unsigned char c` +56 B -- **esp32s31**: `mm::ui::appJs` -151,132 B, `mm::ui::file1` +69,746 B, `mm::ui::styleCss` -27,548 B, `mm::ui::preview3dJs` -27,283 B, `mm::ui::file4` +23,567 B +- **esp32**: `mm::ui::file1` +889 B, `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -851 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +342 B, `mm::HttpServerModule::handleWebSocketUpgrade(mm::platform::T` +215 B, `mm::FixtureProfilesModule::seedBuiltins()` -214 B +- **esp32-16mb**: `mm::ui::file1` -82,600 B, `mm::ui::file6` -27,418 B, `mm::ui::file11` -23,567 B, `mm::ui::file4` +21,985 B, `mm::ui::file2` -19,240 B +- **esp32c3**: `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -908 B, `mm::ui::file1` +889 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +432 B, `mm::HttpServerModule::handleWebSocketUpgrade(mm::platform::T` +242 B, `mm::ControlModule::ControlModule()` -148 B +- **esp32p4rev1-eth**: `mm::ui::file1` +889 B, `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -880 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +432 B, `mm::HttpServerModule::handleWebSocketUpgrade(mm::platform::T` +254 B, `enc_process` +140 B +- **esp32p4rev1-eth-wifi**: `mm::ui::file1` +889 B, `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -880 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +432 B, `mm::HttpServerModule::handleWebSocketUpgrade(mm::platform::T` +254 B, `enc_process` +140 B +- **esp32p4rev3-eth**: `void mm::NetworkReceiveEffect::drainSockets(int)` +2,078 B, `mm::ui::file1` +1,771 B, `mm::NetworkReceiveEffect::receiveTrampoline(void*)` +1,728 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,476 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,290 B +- **esp32s3-n16r8**: `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -851 B, `mm::ui::file1` +662 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +342 B, `mm::HttpServerModule::handleWebSocketUpgrade(mm::platform::T` +227 B, `mm::ui::file2` +31 B +- **esp32s3-n32r16**: `mm::ui::file1` +919 B, `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -851 B, `mm::MappingLUT::startRuns()` +378 B, `mm::Layer::buildFoldedLUT(mm::Coord3D const&, unsigned long,` -356 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +342 B +- **esp32s3-n8r8**: `ext_hub_process` +2,147 B, `mm::ui::file1` +1,860 B, `void mm::NetworkReceiveEffect::drainSockets(int)` +1,659 B, `mm::NetworkReceiveEffect::receiveTrampoline(void*)` +1,510 B, `void mm::NetworkReceiveEffect::drainSockets()` -1,195 B +- **esp32s31**: `mm::ui::file1` +889 B, `mm::sha1(unsigned char const*, unsigned int, unsigned char*)` -880 B, `mm::detail::sha1Block(unsigned long*, unsigned char const*)` +432 B, `mm::HttpServerModule::handleWebSocketUpgrade(mm::platform::T` +254 B, `mm::Scheduler::addModule(mm::MoonModule*)` +40 B What this commit's flash went to, symbol by symbol against the measurement at `Since`. New is new functionality, removed and shrunk are what a cleanup saved, and other is what no symbol owns, such as merged strings and alignment. A rename reads as removed plus new, which the largest moves expose. @@ -88,7 +104,7 @@ What this commit's flash went to, symbol by symbol against the measurement at `S | Target | Tick | FPS | |---|---:|---:| | desktop-linux | 5 µs | 200,000 | -| desktop-macos | 7 µs | 142,857 | +| desktop-macos | 8 µs (+1 µs) ⚠ | 125,000 (−17,857) ⚠ | | desktop-windows | 4 µs | 250,000 | | esp32 | 1,716 µs | 582 | @@ -145,25 +161,25 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 33,397 (+45) ⚠ | 6,760 | 22.4 % (+0.1 %) ⚠ | -| light | 31,681 (+23) ⚠ | 6,660 | 23.6 % | -| platform | 22,993 (−1) ✓ | 4,223 | 20.6 % | -| ui | 13,043 (+15) ⚠ | 3,481 | 28.3 % | -| test | 76,140 (+105) ⚠ | 7,505 | 11.4 % | -| moondeck | 32,680 (+20) ⚠ | 4,420 | 15.5 % | +| core | 33,413 (+16) ⚠ | 6,762 | 22.4 % | +| light | 31,677 (−4) ✓ | 6,655 | 23.6 % | +| platform | 22,993 | 4,223 | 20.6 % | +| ui | 13,138 (+95) ⚠ | 3,491 | 28.1 % (−0.2 %) ✓ | +| test | 76,758 (+618) ⚠ | 7,520 | 11.3 % (−0.1 %) ✓ | +| moondeck | 33,648 (+968) ⚠ | 4,470 | 15.3 % (−0.2 %) ✓ | ## Tests | Kind | Count | |---|---:| -| unit cases | 2,836 (+5) ✓ | +| unit cases | 2,841 (+5) ✓ | | scenarios | 36 | ## Complexity | Metric | Value | |---|---:| -| functions | 5,520 (+4) ✓ | +| functions | 5,521 (+1) ✓ | | over threshold | 345 | | worst CCN | 128 | @@ -171,10 +187,10 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Metric | Value | |---|---:| -| markdown files | 170 | -| markdown lines | 31,236 (+15) ⚠ | -| plan files | 59 | -| backlog lines | 4,001 | -| lessons lines | 546 | +| markdown files | 114 (−56) ✓ | +| markdown lines | 18,090 (−13,146) ✓ | +| plan files | 10 (−49) ✓ | +| backlog lines | 2,996 (−1,005) ✓ | +| lessons lines | 447 (−99) ✓ | | CLAUDE.md lines | 119 | diff --git a/docs/reference/performance.md b/docs/reference/performance.md index 1686ed10..0f9ebe7d 100644 --- a/docs/reference/performance.md +++ b/docs/reference/performance.md @@ -406,12 +406,12 @@ Each parallel LED driver ran on real hardware at a 128×128 = 16384-light grid w | Peripheral | Board | Pins used (8 lanes) | Result | Ceiling / bound | |---|---|---|---|---| -| **Parlio** | ESP32-P4 (Waveshare P4-NANO) | `20,21,22,23,24,25,26,27` | `Drivers` tick ~30100 µs, fps 30 at 16384 lights (8 lanes, SWAR transpose). | Parlio's single-shot transfer caps at 65535 bytes in total, and a light costs `channels × 24 × slotBytes`. The ceiling is **897 lights/lane at 8 lanes RGB** and 673 RGBW. It halves to 442/332 at 16 lanes, since a 16-bit bus doubles `slotBytes`. Over that, the driver reports `too many lights per pin` and keeps running. The [chunked-DMA work](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080935) lifts the ceiling (tier 1 → ~16-21K). | +| **Parlio** | ESP32-P4 (Waveshare P4-NANO) | `20,21,22,23,24,25,26,27` | `Drivers` tick ~30100 µs, fps 30 at 16384 lights (8 lanes, SWAR transpose). | Parlio's single-shot transfer caps at 65535 bytes in total, and a light costs `channels × 24 × slotBytes`. The ceiling is **897 lights/lane at 8 lanes RGB** and 673 RGBW. It halves to 442/332 at 16 lanes, since a 16-bit bus doubles `slotBytes`. Over that, the driver reports `too many lights per pin` and keeps running. The [chunked-DMA work](https://github.com/MoonModules/MoonLight/issues/218) lifts the ceiling (tier 1 → ~16-21K). | | **LCD_CAM i80** (MultiPinLedDriver) | ESP32-S3 N16R8 Dev | data `18,5,6,7,8,9,10,11` · WR(clock) `12` · DC `13` | Same encoder, healthy on real i80. Encode scales ~6 µs/light (8×512 = 4096 → 23 ms; 8×1024 = 8192 → 50 ms). | **Single-DMA init ceiling 8192–12288 lights**: 8×1024 initializes, and 8×1536 fails with `LCD-IDF: bus init failed, check pins / memory`. | -| **RMT** | classic ESP32 (LOLIN D32 / WROOM) | `2,4,13,14,16,17,18,19` (pin 2 = a real 24-LED strand) | 8-pin RMT drives **8×256 = 2048 lights** at a ~12.6 ms tick. It scales to ~8192 before the tick plateaus. All lanes are healthy, and the pin-2 strand is verified lit. | **Silent alloc failure:** the RMT symbol buffer is sized for the driver's `count` window. `count=0` on a 16384 grid needs ~1.5 MB, which fails on the ~90 KB heap, and `tick()` returns with **no status** (LEDs dark). Bound the driver with the start/count window; a status for this is [backlogged](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287079814). | +| **RMT** | classic ESP32 (LOLIN D32 / WROOM) | `2,4,13,14,16,17,18,19` (pin 2 = a real 24-LED strand) | 8-pin RMT drives **8×256 = 2048 lights** at a ~12.6 ms tick. It scales to ~8192 before the tick plateaus. All lanes are healthy, and the pin-2 strand is verified lit. | **Silent alloc failure:** the RMT symbol buffer is sized for the driver's `count` window. `count=0` on a 16384 grid needs ~1.5 MB, which fails on the ~90 KB heap, and `tick()` returns with **no status** (LEDs dark). Bound the driver with the start/count window; a status for this is [backlogged](https://github.com/MoonModules/MoonLight/issues/218). | | **I2S i80** | classic ESP32 (ESP32-WROVER) | data `2,4,13,14,18,19,21,22` · WR(clock) `32` · DC `33` (pin 2 = a real strand, verified lit) | The **same** `MultiPinLedDriver`, over the **I2S peripheral in i80 mode**. IDF routes the i80 API to I2S on the classic and to LCD_CAM on the S3 and P4. 8-lane doubling sweep, 128×128 grid, 2026-07-13. 64/pin (512) → 4877 µs, 128/pin (1024) → 8575 µs, 256/pin (2048) → 15638 µs. It scales linearly at **~7.6 µs/light**, against ~6 µs on the S3's LCD_CAM. `frameTime` reports the WS2812 wire floor (512 → 243 fps, 2048 → 67 fps). **16 lanes work on classic too** (16×256 = 4096 verified), but the WROVER exposes only ~13 non-strap pins, so 8 lanes is the practical set. | **Internal-RAM ceiling: 2048 lights at 8 lanes (4096 at 16).** The classic I2S backend **cannot DMA from PSRAM**: `esp_lcd_i80_alloc_draw_buffer` rejects `MALLOC_CAP_SPIRAM` ("external memory is not supported"). Its frame buffer is internal DMA RAM only (`maxBlock` ≈ 76 KB). At 8 lanes, 512/pin (4096) and above fail with `I2S-IDF: bus init failed, check pins / memory`, a **clean degrade** with uptime climbing through every rung. **The render is decoupled from this ceiling:** the same sweep rendered the full 16384-light grid at every rung (`Layer` ≈ 511 ms/frame, from PSRAM) while the output was capped. At 16K lights the render (511 ms) dwarfs the output (24 ms), so the render is the wall on this chip and multicore cannot help. | | **LCD_CAM 16-lane** | ESP32-S3 (SE 16 V1 + LightCrafter 16, n8r8) | SE16 data `47,48,21,38,14,39,13,40,12,41,11,42,10,2,3,1` · WR/DC `5`/`6`; LC16 data `47,21,14,9,8,16,15,7,1,2,42,41,40,39,38,48` · WR/DC ghost `33`/`34` | **Reaches the full 16384 lights, where Parlio caps at 4096.** SE16 16-lane doubling sweep, 128×128 grid, `doubleBuffer` ON, 2026-07-13. 512 → 1843 µs, 1024 → 3422 µs, 2048 → 6612 µs, 4096 → 15153 µs, 8192 → 26788 µs, **16384 → 49916 µs (~20 fps)**. The driver tick is the encode alone; the WS2812 wire overlaps in background DMA, and `frameTime` reports it (16384 → 28786 µs). With `doubleBuffer` OFF: 4096 → 22518 µs and 16384 → 77732 µs, so async is ~30–56% faster. | **PSRAM-backed, so the contiguous-block limit that caps Parlio stays out of the way.** LCD_CAM allocates its DMA buffer through `esp_lcd_i80_alloc_draw_buffer` **from PSRAM**, clear of the ~368 KB largest internal block. **At 16K the encode is the wall.** The 28,786 µs wire alone allows ~35 fps, so the tick is the 49,916 µs encode at ~20 fps. Inline on **core 0**, that encode **starves the LC16's W5500 SPI-Ethernet**, which the [multicore pipeline](#multicore-the-whole-output-stage-on-core-1-multicore-step-2) resolves. | -| **Parlio 16-lane** | ESP32-P4 (testbench, n16r8) | 16 data pins `21,20,22,23,24,25,26,27,32,33,39,40,41,42,43,44` | 16-lane doubling sweep (2026-07-12), reproduced within 0.3% on a second P4. `ledsPerPin` runs 32 to 256 on a 128×128 grid. The tick scales **linearly**: 512 → 1653 µs, 1024 → 2925 µs, 2048 → 5514 µs, **4096 → 10760 µs**. With `doubleBuffer` ON (2026-07-13), the ~7.5 ms wire wait moves into background DMA. The driver tick at 256/pin is **3,790 µs against 10,820 µs (92 driver-fps) OFF**. The board runs **76 fps against 48**, with a system tick of 13.0 against 20.6 ms. `frameTime` reports the measured wire floor: **7474 µs (133 fps max)**. The tick is effect render (~7.3 ms) plus driver (~3.8 ms) in series, which the [multicore pipeline](#multicore-the-whole-output-stage-on-core-1-multicore-step-2) overlaps toward that ceiling. OFF saves one frame of latency, below the perceptual A/V-sync threshold, so ON suits every use, audio-reactive included. | **Single-DMA ceiling ≈ 4096 lights (256/pin).** 512/pin (8192) fails with `Parlio init failed, check pins / memory`. The P4 has 33 MB free heap, but its largest contiguous internal block is ~368 KB, and the 16-bit single-shot DMA buffer needs one contiguous block. It is a **contiguous-block limit rather than total memory**, and it bites before the 65535-byte cap. The full 16384 (1024/pin) needs the [Parlio chunked-transfer](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080935) work, which splits the frame across DMA bursts. | +| **Parlio 16-lane** | ESP32-P4 (testbench, n16r8) | 16 data pins `21,20,22,23,24,25,26,27,32,33,39,40,41,42,43,44` | 16-lane doubling sweep (2026-07-12), reproduced within 0.3% on a second P4. `ledsPerPin` runs 32 to 256 on a 128×128 grid. The tick scales **linearly**: 512 → 1653 µs, 1024 → 2925 µs, 2048 → 5514 µs, **4096 → 10760 µs**. With `doubleBuffer` ON (2026-07-13), the ~7.5 ms wire wait moves into background DMA. The driver tick at 256/pin is **3,790 µs against 10,820 µs (92 driver-fps) OFF**. The board runs **76 fps against 48**, with a system tick of 13.0 against 20.6 ms. `frameTime` reports the measured wire floor: **7474 µs (133 fps max)**. The tick is effect render (~7.3 ms) plus driver (~3.8 ms) in series, which the [multicore pipeline](#multicore-the-whole-output-stage-on-core-1-multicore-step-2) overlaps toward that ceiling. OFF saves one frame of latency, below the perceptual A/V-sync threshold, so ON suits every use, audio-reactive included. | **Single-DMA ceiling ≈ 4096 lights (256/pin).** 512/pin (8192) fails with `Parlio init failed, check pins / memory`. The P4 has 33 MB free heap, but its largest contiguous internal block is ~368 KB, and the 16-bit single-shot DMA buffer needs one contiguous block. It is a **contiguous-block limit rather than total memory**, and it bites before the 65535-byte cap. The full 16384 (1024/pin) needs the [Parlio chunked-transfer](https://github.com/MoonModules/MoonLight/issues/218) work, which splits the frame across DMA bursts. | **LOLIN D32 (classic ESP32-WROOM) usable LED GPIOs:** `4,13,14,18,19,21,22,23,25,26,27,32,33` plus `16,17` (free on WROOM; they are the PSRAM bus on WROVER). Avoid straps `0,2,12,15`, the onboard LED on `5`, and battery-sense on `35`; input-only `34–39` can't drive an LED. (Chip-level set: [gpio-usage.md](hardware/gpio-usage.md).) @@ -450,7 +450,7 @@ The P4's WiFi comes from an on-board ESP32-C6 over SDIO. Compiling that path in Compiling the co-processor in costs roughly **4× per request and 2× on throughput**. Render holds at 359 fps on the WiFi build, so the frame loop is unaffected. The cost is per request rather than per byte, which points at a periodic blocker a request waits out rather than a slow pipe. -Measured on IDF v6.1-rc1; the remaining penalty is tracked in [backlog/platform](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287085924). +Measured on IDF v6.1-rc1; the remaining penalty is tracked in [backlog/platform](https://github.com/MoonModules/MoonLight/issues/220). ## State over the WebSocket (`teensy41`) diff --git a/docs/work/Plan-20260901 - Input mapping and scripted sensors (7 of 12).md b/docs/work/Plan-20260901 - Input mapping and scripted sensors (7 of 12).md index 8e26aa2a..7dd6273c 100644 --- a/docs/work/Plan-20260901 - Input mapping and scripted sensors (7 of 12).md +++ b/docs/work/Plan-20260901 - Input mapping and scripted sensors (7 of 12).md @@ -256,7 +256,7 @@ and a test that the rename left no `IrService` behind. Bench: the Dig-2-Go's GPI and the infrared remote on another, both driving `Control.switch1`, so two inputs reaching one target is proven rather than assumed. -## Step 1b: an encoder that shows what it selects 🚧 (the Open Stage Control session has no strip widget; its label and pad-matrix gaps stay on [their card](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287130773)) +## Step 1b: an encoder that shows what it selects 🚧 (the Open Stage Control session has no strip widget; its label and pad-matrix gaps stay on [the backlog](#backlog)) Small, and it follows step 1 rather than joining it, so the rebuild is not also carrying a UI change. @@ -294,7 +294,7 @@ cannot show text simply ignores it. The shipped [Open Stage Control session](../reference/examples/open-stage-control.json) gains a text widget bound to it, which is also worth doing because that session already has known gaps (its widget labels do not render and its pad matrix draws nothing, both recorded in -[their card](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287130773)): adding the strip is the moment to fix the session +[the backlog](#backlog)): adding the strip is the moment to fix the session by building one widget of each kind in its own editor and copying the shape it produces. **Test:** the infrared remote's palette next/prev rows driving `Control.encoder1`, with the palette @@ -675,7 +675,7 @@ to zero, so a pedal pushed fully down settled at 253 rather than 255 and full br unreachable. It now takes the remaining distance whole once the step rounds to nothing. **Still open**: the bench half. A pot on an ADC pin, both paths on the same hardware. The SE16 and -LightCrafter sense pins are the natural rig and are now free ([their card](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287086286)), but the scale +LightCrafter sense pins are the natural rig and are now free ([the backlog](#backlog)), but the scale factor per board needs the schematic. ## Step 4: I2C sensors 🚧 (only the shared bus and its scan, `I2cBusModule`; no register read, sensor module, published frame or `i2cRead` builtin) @@ -772,7 +772,7 @@ peripheral (PCNT for the encoder, RMT or a timed capture for the echo) rather th **Test:** an encoder on the bench driving `Control.encoder1` through the module, and an HC-SR04 reporting distance through a script. -## Step 6: PIR, and the trigger vocabulary ✅ (a PIR is a `set` row, which already holds a level, so no `level` kind: two kinds would do one job; the help and the momentary-row test name it. Multi-click waits for a panel that needs it, on [its card](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287131477)) +## Step 6: PIR, and the trigger vocabulary ✅ (a PIR is a `set` row, which already holds a level, so no `level` kind: two kinds would do one job; the help and the momentary-row test name it. Multi-click waits for a panel that needs it, on [the backlog](#backlog)) The cheap finish. A PIR is a digital pin, so it needs no seam at all: it is a `ButtonService` row with the `set` event, which holds the pin's level rather than reacting to a press. MoonLight @@ -804,6 +804,12 @@ lights on another is the obvious use, and the two-step model already makes that device's surface is just another target). That is enough to note; the design belongs in its own analysis alongside the backlog's existing network entries. +## Backlog + +- **SE16 and LightCrafter power monitoring.** Both boards carry voltage and current sensors: the SE 16 V1's on GPIO 8 (voltage) and 9 (current), the LightCrafter's on [its page](../reference/hardware/lightcrafter-16.md#power-sensing). Open per board: `voltagePin` and `currentPin` in the two device definitions, "Power monitoring" from `planned` to `supported`, the SE16's scale factors (its divider ratio and shunt, from the schematic), and a reading checked against a meter. It gives the ADC seam a real bench rig, better than a hand-wired potentiometer. +- **The Open Stage Control session's labels.** The shipped session ([open-stage-control.json](../reference/examples/open-stage-control.json)) works for every fader, encoder and switch, but draws no widget names and an empty pad matrix. A session Open Stage Control saves itself carries no `label` key on a fader, so the reliable fix is to build one widget of each kind in its editor, save, and copy the shape. Cosmetic: only the labels and the pad grid are missing. +- **Double-click and multi-click on a button**, from ESPHome's trigger vocabulary: the same debounce timers with more states, so one button does several things. Built when a panel needs more actions than it has buttons. + ## Not in this plan - **USB game controllers**: a HID report-descriptor parser plus USB Host bring-up, S3/P4 only. diff --git a/docs/work/Plan-20261001 - Gamepads and MIDI desks (2 of 7).md b/docs/work/Plan-20261001 - Gamepads and MIDI desks (2 of 7).md index 204f5310..7c9ae8ce 100644 --- a/docs/work/Plan-20261001 - Gamepads and MIDI desks (2 of 7).md +++ b/docs/work/Plan-20261001 - Gamepads and MIDI desks (2 of 7).md @@ -10,7 +10,7 @@ Everything this needs on the receiving side is built. The plan adds transports a - **ControlModule** is the surface: 8 switches, 8 encoders, 8 faders and a pad grid, each assignable to any `Module.control`, driven through `Scheduler::setControl` and following its target back. - **The input services** map a physical input onto the surface, or onto any control directly, with the shared `InputMapping` rows (Toggle, Set, Delta): `ButtonService` for GPIO buttons, `AnalogService` for potentiometers, `InfraredService` for remotes (with a `learn` field: the next code received binds to the row), `OscModule` for OSC. -- **`ControlSurface`** is the two-way interface a desk attaches through (`sendValue`, `sendRing`, `sendColor`, `sendLabel`), and `setTouched` tells the surface a hand is on a control. It was written for MIDI hardware, and OSC is so far its only transport ([the Open Stage Control session's labels](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287130773)). +- **`ControlSurface`** is the two-way interface a desk attaches through (`sendValue`, `sendRing`, `sendColor`, `sendLabel`), and `setTouched` tells the surface a hand is on a control. It was written for MIDI hardware, and OSC is so far its only transport ([the Open Stage Control session's labels](Plan-20260901%20-%20Input%20mapping%20and%20scripted%20sensors%20(7%20of%2012).md#backlog)). - **[Control surfaces](../reference/hardware/control-surfaces.md)** records the bench desks: the X-Touch and the QCon both speak Mackie Control (MCU) over MIDI, neither speaks OSC, the X-Touch adds RTP-MIDI on its Ethernet port, and the QCon is USB only. - **`setControl` calls `onControlChanged`** on every write, from any of those writers, and `ControlDescriptor::live` marks a control as state that is never written to flash. diff --git a/docs/work/Plan-20261013 - Audio reactive live performance (0 of 7).md b/docs/work/Plan-20261013 - Audio reactive live performance (0 of 7).md index 25b4f8a8..25eaf45c 100644 --- a/docs/work/Plan-20261013 - Audio reactive live performance (0 of 7).md +++ b/docs/work/Plan-20261013 - Audio reactive live performance (0 of 7).md @@ -108,11 +108,11 @@ The sensors, from what is listed to what a stage needs: | HLK-LD2450 | [mmWave radar, HLK-LD2450](https://www.tinytronics.nl/en/sensors/motion/hi-link-hlk-ld2450-24ghz-radar-sensor-module-with-bluetooth) | up to 3 targets with x and y, about 6 m, UART | the cheapest real tracker, working in darkness and haze | Tinytronics €7.00 | | DWM3000EVB | [UWB, Qorvo DWM3000EVB](https://www.digikey.nl/nl/products/detail/qorvo/DWM3000EVB/24367408) | about 10 cm, with anchors and a tag per performer | how professional radio trackers work | DigiKey €31.34 per node | | | [PosiStageNet in](https://posistage.net/) | positions from a professional system | MoonLight joining a rig that already tracks | free, open protocol, MIT-licensed SDK | -| OV5647 camera | [A camera on the P4](https://www.tinytronics.nl/en/raspberry-pi/raspberry-pi-compatible-camera-5mp-v1.3) (OV5647, MIPI-CSI) | person detection | later, with the [streams](Plan-20261013%20-%20Streams%2C%20every%20frame%20from%20outside%20(11%20of%2013).md) work | Tinytronics €8.00 | +| OV5647 camera | [A camera on the P4](https://www.tinytronics.nl/en/raspberry-pi/raspberry-pi-compatible-camera-5mp-v1.3) (OV5647, MIPI-CSI) | person detection | later, with the [Video in, LED walls out](Plan-20261024%20-%20Video%20in%2C%20LED%20walls%20out%20(0%20of%2010).md) work | Tinytronics €8.00 | **On the bench (2026-10-19): two HLK-LD2450 kit B** from Hi-Link's own store, each with a USB-to-serial adapter, a Bluetooth antenna and a cable. The adapter lets the radar be tried on a computer with Hi-Link's tool before the UART seam exists: targets, range and zone exclusion on day one. Two units answer the stage questions: whether two 24 GHz radars side by side interfere, and how their targets merge where their views overlap. -Whether kit B's Bluetooth carries live target frames, or only firmware updates and configuration, is unverified: Hi-Link's tool on the bench answers it. The plan reads over UART either way, since a wire needs no pairing and puts no second 2.4 GHz radio beside the WiFi, and its protocol is simple with plenty of open firmware to read as prior art. Two siblings fit other jobs: the **LD2451** is larger and reaches further, far enough to track cars, and the **LD2410** is low-powered, so it does not see through walls and needs no distance gates set to stop it. The **LD2410C** also sends its UART data over Bluetooth Low Energy when it is only powered from 5 V, with an option to switch that off or require pairing, which would let a board read a radar with no wire. The radar's UART seam is the same one wired DMX-512 needs, [RS-485 / DMX-512 wired output](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287080691), so one seam serves both. +Whether kit B's Bluetooth carries live target frames, or only firmware updates and configuration, is unverified: Hi-Link's tool on the bench answers it. The plan reads over UART either way, since a wire needs no pairing and puts no second 2.4 GHz radio beside the WiFi, and its protocol is simple with plenty of open firmware to read as prior art. Two siblings fit other jobs: the **LD2451** is larger and reaches further, far enough to track cars, and the **LD2410** is low-powered, so it does not see through walls and needs no distance gates set to stop it. The **LD2410C** also sends its UART data over Bluetooth Low Energy when it is only powered from 5 V, with an option to switch that off or require pairing, which would let a board read a radar with no wire. The radar's UART seam is the same one wired DMX-512 needs, [RS-485 / DMX-512 wired output](https://github.com/MoonModules/MoonLight/issues/219), so one seam serves both. The order: the radar first, which needs a UART seam the platform does not have yet; PosiStageNet in second, which is UDP and needs no new seam; the ToF zone grid as the indoor alternative once the input-mapping plan's I2C step lands and a VL53L8CX read is measured that does not stall the render task: an 8 × 8 frame over I2C is large, so the candidates are SPI, a lower ranging rate, or reading on a task off the render core. Verify with a person walking the bench stage and the heads following. diff --git a/docs/work/Plan-20261013 - Streams, every frame from outside (11 of 13).md b/docs/work/Plan-20261013 - Streams, every frame from outside (11 of 13).md deleted file mode 100644 index 3b5cb875..00000000 --- a/docs/work/Plan-20261013 - Streams, every frame from outside (11 of 13).md +++ /dev/null @@ -1,326 +0,0 @@ -# Plan: streams, every frame from outside, one model - -Plan, for the product owner's approval; started as an analysis on 2026-10-13, when three pull requests by Gohnnyman (#138 a backlight effect and VideoService, #139 USB video capture, #140 backlight wiring) arrived the same week TouchMyLight announced an FSEQ player for a 20 × 20 × 20 cube (on #134). Read one by one they look like unrelated features: a TV backlight, a capture card, a show player. Read from MoonLight's architecture they are the same thing: a stream of frames arriving from outside the device. This plan models that once, by the textbook, so cameras, video, files, the network and the browser become combinations of a few parts rather than a feature each. - -## The textbook model: three independent axes - -Every media framework separates the same three questions: GStreamer, FFmpeg, Media Foundation and Video4Linux all do. Where do the bytes come from, how are they encoded, and what shape is the frame? Each axis varies on its own. - -| Axis | Values in MoonLight's world | -|---|---| -| **Transport** | a file on flash or the SD card, the network (UDP, TCP, the browser's WebSocket), USB, a camera port (MIPI-CSI) | -| **Format** | raw RGB, PPM, GIF, MJPEG, FSEQ, the DDP, Art-Net and E1.31 framings; H.264 on the way out | -| **Geometry** | a picture (width × height), a light list (N lights, mapped by the sender), a volume (width × height × depth) | - -A source is then a transport plus a decoder, delivering frames with a small descriptor: the geometry, the pixel format and the frame's time. An output is an encoder plus a transport. Everything users ask for is a combination: - -| What users ask for | Transport | Format | Geometry | -|---|---|---|---| -| An xLights show (FSEQ) | SD file | FSEQ | light list | -| Network Receive today | UDP | DDP, Art-Net, E1.31 | light list | -| A GIF animation | flash or SD file | GIF | picture | -| An HDMI grabber (#139) | USB | MJPEG | picture | -| A camera on the P4 | MIPI-CSI | raw, through the image processor | picture | -| A screen shared from a browser tab | WebSocket | raw or DDP | picture or light list | -| A volumetric show for a cube | SD file | FSEQ | volume, or a light list placed in 3D | -| HLS and RTSP (shipped) | HTTP, RTSP | H.264 | picture | -| Recording | SD file | FSEQ | light list | - -## How it lands on what MoonLight has: two families - -The three axes describe any stream, and they do not make every stream one module: two families, each with its own semantics, meeting in the layer, as [MoonLight, frames from outside](../explanation/architecture/moonlight.md#frames-from-outside) describes. The roadmap across both: - -| | Channel data, the pixel protocols | Pictures | -|---|---|---| -| Live in | Network Receive: Art-Net, sACN (E1.31), DDP | capture: USB, MIPI-CSI, a browser tab | -| Recorded in | FSEQ playback | GIF, image sequences | -| Out | Network Send, FSEQ recording | HLS and RTSP, HDMI | -| Data | values mapped by the sender | width × height pixels | -| Into a layer by | placement through the layout | sampling per light | - -Recording is the bridge between transports and files: a live stream in, an FSEQ file out, written from the drivers' output. Art-Net "from the SD card" is that recording, as Falcon Player records it; FSEQ does not travel as a stream, since Falcon Player copies the file to its remotes. - -Mapped by the receiver or by the sender, `mapped_by`, and the paths a sender-mapped frame takes: [MoonLight, frames from outside](../explanation/architecture/moonlight.md#frames-from-outside). What each path costs: [Performance, Network Receive](../reference/performance.md#network-receive). - -## Real time and not real time - -Streams split a second way, by who sets the pace, and the split is the textbook one between live and on-demand media. - -| | Live (real time) | Recorded (not real time) | -|---|---|---| -| Examples | Network Receive, USB and CSI capture, a browser tab, MIDI-driven content | FSEQ, GIF and image sequences from flash or SD, a baked show | -| Who sets the pace | the sender: a frame arrives when it arrives | the device's clock: it pulls the frame the timeline asks for | -| Late or missing frames | the newest frame wins and an old one is dropped, never queued, since a queued frame is a late frame | none: the next frame is read ahead, one frame of buffer at most | -| Seek, loop, pause | not possible | the frame index comes from the clock, so they are arithmetic | -| Sync across boards | each board shows what it receives | clock sync: every board computes the same frame index from the shared clock | -| What it costs | latency, which the design keeps to one frame | storage bandwidth, which the design keeps to one frame per tick | - -Each source states which kind it is, so its effect knows whether to show the newest frame or the frame the clock asks for. The transport decides it: a socket, a USB grabber and a camera are live; a file is recorded. A file can also be played live, at the pace of a sender elsewhere, which is how a recording replays exactly what arrived. - -Not real time also names a third case that never reaches the device as a stream: **authoring**. Baking a picture or a video into FSEQ for a layout runs in the browser at whatever speed it takes, and only its result plays in real time. - -## The roadmap - -Each family's consumer is an effect doing one gather per frame, and modifiers stay the one home for geometry: [MoonLight, frames from outside](../explanation/architecture/moonlight.md#frames-from-outside). - -```mermaid -flowchart TB - subgraph CH["Channel data: mapped by the sender"] - direction LR - nr["Network Receive
Art-Net, sACN, DDP"] - fseq["FSEQ playback
from /sd
TouchMyLight"] - end - subgraph PIC["Pictures: width × height"] - direction LR - cap["capture
USB #139, CSI, browser tab"] - gif["GIF, sequences
flash, /sd"] - ps["picture source
test pattern
#138"] - end - place["light-list placement
through the layout"] - samp["picture effect
sampled per light
backlight #138"] - mix["Layers: effects, opacity, blend modes, modifiers"] - map["Layouts: 2D, 3D, backlight.mll #140"] - subgraph OUT["Out"] - direction LR - drv["LED drivers
color trim #139"] - ns["Network Send"] - rec["FSEQ recording"] - hls["HLS, RTSP"] - hdmi["HDMI
ESP32-P4-PC"] - end - browser["Browser: converts and bakes
at authoring time"] - - nr --> place - fseq --> place - cap --> ps - gif --> ps - ps --> samp - place --> mix - samp --> mix - mix --> map - map --> drv - map --> ns - map --> rec - map --> hls - map --> hdmi - browser -.-> gif - browser -.-> fseq - - classDef shipped fill:#1f4d3d,stroke:#5fb89a,color:#fff - classDef pr fill:#2d3561,stroke:#7b88c9,color:#fff - classDef proposal fill:#3d2d61,stroke:#a07bc9,color:#fff - class nr,fseq,place,cap,ps,samp,mix,map,ns,hls shipped - class drv pr - class gif,rec,hdmi,browser proposal - style CH fill:none,stroke:#888 - style PIC fill:none,stroke:#888 - style OUT fill:none,stroke:#888 -``` - -Green ships, blue waits on its contribution (the color trim), purple is proposed: recording here, GIF, HDMI and the browser in [Video in, LED walls out](Plan-20261024%20-%20Video%20in%2C%20LED%20walls%20out%20(0%20of%2010).md). The diagram is configuration, not runtime stages. - -## Where MoonLight stands among the tools - -The tools look different, but they share this pipeline and put their weight in different places. - -| Tool | Weight on | Runs on | -|---|---|---| -| Resolume | video clips in layers, pixel mapping to fixtures | PC | -| TouchDesigner | a general dataflow graph, anything to anything | PC | -| MADRIX | LED pixels: generative effects, layers, a 2D and 3D patch, media and capture, many outputs, record and replay on a standalone player | PC, standalone nodes | -| xLights with Falcon Player | sequencing offline, playback standalone | PC, Raspberry Pi | -| Hyperion, HyperHDR | capture to LED zones | Raspberry Pi, PC | -| WLED | effects on segments, a realtime override | ESP32 | - -MoonLight is closest to MADRIX: generative effects, layers, 2D and 3D layouts and many outputs. The difference is that it runs on the controller itself. MADRIX's media, capture, and record-for-standalone-playback are what this stream model adds. The position is MADRIX-class pixel control, embedded, which no tool in the list occupies. - -## What WLED did, and where it stopped - -Checked 2026-10-13 against WLED main and WLED-MM. - -- **One picture format, decoded on the device.** The Image effect (Aircoookie, PR #3835, finished by netmindz, shipped in WLED 16.0, May 2026) plays a GIF from the internal file system, up to 320 × 320, with about 24 KB of decoder RAM (bitbank2's AnimatedGIF). It scales by nearest neighbor, decodes one image at a time across all segments, and redraws only changed pixels, which upsets brightness limiting. WLED-MM backported it. -- **Every conversion in the browser.** DedeHai's PixelForge (PR #4982, WLED 16.0) crops, scales and encodes a GIF in the page and uploads it; Video Lab turns video into GIFs, Pixel Paint draws on the matrix, Font Factory makes scrolling-text fonts. -- **No capture on the device.** The only video path is a browser page streaming DDP over the WebSocket (DedeHai, PR #4997). Hyperion does all backlight processing on a computer and drives WLED over DDP. -- **FSEQ: five attempts, none merged** (#2413, #3551, #4570, #5641 open since May 2026). They bolted playback onto the realtime override, which replaces effects rather than composing with them, and each bundled an SD driver, a player, a UI and Falcon Player sync in one pull request. Recording was planned and never built. -- **SD** is a usermod nothing in core reads. - -ESPixelStick, the reference small-device FSEQ player, plays uncompressed FSEQ v2 with sparse ranges from SD, floors the step at 25 ms, takes the frame index from a monotonic clock plus a sync offset, and seeks per frame, which keeps looping and sync trivial. - -What it teaches: GIF first among picture formats, decoded per source rather than in one global decoder; conversion as browser pages; the layer as the home of every stream, which is exactly where WLED's FSEQ attempts went wrong; FSEQ starting where ESPixelStick stops. New beyond WLED: a stream composing in layers on any layout, 3D included; area sampling in linear light; capture decoded on the device; recording; synchronized playback across boards; the SD card as a first-class file store. - -## Browser and device - -One rule decides the split, and MoonLight already follows it where the device cannot do a job (the Firmware card's download for a board without TLS, MoonCloud's outbox, scripts fetched from GitHub by the page): **what must run with no browser open belongs on the device; what happens once, with a person at the screen, belongs in the browser.** In the stream model the browser is one more transport, and an authoring tool. - -| Tier | Where | Examples | -|---|---|---| -| Authoring: once, a person present | browser | crop, scale and convert images; video to GIF or to FSEQ; fonts; an xLights model imported as a layout | -| Live, a browser as the source | the browser samples, the device displays | a screen (`getDisplayMedia`), a webcam or a video in a tab, streamed over the existing WebSocket | -| Run time: autonomous | device | effects, playback from flash or SD, USB and CSI capture, sync, recording, a show running for days | - -**The browser can bake for this layout.** It already knows every light's position, since it draws them in the preview. So it can do the expensive part once, sampling a picture or a video per light with the area filter in linear light, and write the result as an FSEQ file. The device then plays it with the same cheap light-list path as an xLights show, on any layout, with no decoder. - -| | Baked in the browser (FSEQ) | Decoded on the device (GIF) | -|---|---|---| -| Device cost | lowest: read and copy | decode and sample every frame | -| Follows a layout change | no: bake again | yes | -| Storage | large: 8000 lights at 40 fps is about 1 MB/s, so SD only | small: fits in flash | - -A live browser source costs bandwidth, lights × 3 bytes × fps: about 60 KB/s for a 600-light TV frame at 30 fps, too much for 8000 lights over WiFi. USB and CSI capture run with no browser, so they belong on the device. A TV's picture from a set-top box or console never passes through a computer, so a backlight following it needs the grabber on the board (#139). - -## 3D layouts - -TouchMyLight's cube is the first user, so 3D works from the start. - -- **A light list is mapped by the sender, and carries no positions:** placed by the layout on 1D, 2D and 3D layouts alike ([MoonLight, frames from outside](../explanation/architecture/moonlight.md#frames-from-outside)), with the layout numbering its lights as the xLights model does ([the FSEQ player's card](../moonmodules/light/effects.md#fseqplayer)). -- **A picture reaches a 3D layout by projection:** each light samples the picture at its position seen along one axis, the planar projection media servers use. -- **A volume is the native 3D stream:** a frame per depth slice, copied straight into a 3D layer. - -## The P4 and its codecs - -The P4's hardware video codec encodes H.264 but does not decode it; its JPEG codec works both ways. So video arrives as MJPEG from a grabber, raw from a camera, or baked in the browser, while video leaves through the H.264 encoder HLS already drives. - -**One board carries every piece.** The Olimex ESP32-P4-PC (February 2026, on the PO's bench) has HDMI and MIPI-DSI out, a MIPI-CSI camera connector, four USB 2.0 host ports, a microSD slot, Ethernet with PoE and 32 MB of PSRAM: capture in, HDMI out and FSEQ from the card on one P4. - -## What each contribution becomes - -| Contribution | In the plan | -|---|---| -| `VideoService` (#138) | the picture source, renamed for what it is | -| #138's backlight effect | the picture effect's area sampler, edge depth among its settings | -| USB capture (#139, first commit) | a picture source's USB transport and MJPEG decoder, behind the platform seam, in the P4 images with room | -| Per-die trim (#139, second commit) | a separate idea: output calibration in fixture profiles and correction, judged against a 3×3 correction matrix | -| Backlight wiring (#140) | a layout: Hyperion's classic layout, xLights' frame models | -| FSEQ player (TouchMyLight) | its own thin effect beside Network Receive, both on one channel-data sink (staging, hold-last-frame, `mapped_by`, the placement, the counters); a third user later records a live stream to FSEQ | -| Network Receive (shipped) | kept as the pixel-protocol family, gaining a `mapped_by` setting: `receiver` as today, `sender` through the light-list placement | - -## What we do not do, and what we do differently - -The work follows the idea, not the pull requests: a contribution lands where it fits the plan, in the shape the plan gives it. - -**Not part of the idea:** - -| What | From | Why not | -|---|---|---| -| The backlight as an effect of its own | #138 | it is one sampler setting of the picture effect; a card of its own duplicates the picture path | -| PPM as a user format | #138 | chosen only for want of a software JPEG decoder; GIF covers that need with the tools users already own, and PPM stays a test fixture | -| HDR tone curves (PQ, HLG) | #114, partly #138 | no planned source delivers HDR; the descriptor carries sRGB or linear until one does | -| Video decoding beyond MJPEG on the device (H.264, software JPEG) | implied by "video" | the browser bakes video into GIF or FSEQ; the device decodes only what its hardware decodes | -| FSEQ extras up front: zstd, `.xlz` archives, FPP Connect upload, FPP MultiSync | the pattern of WLED's attempts | the bundling that stalled all five of them; uncompressed v2 first, each extra on a measurement or a user who needs it | -| One module for every transport and data type | the first draft of this plan | the standard is one module model with typed data, as GStreamer and TouchDesigner do; MoonModule is that model, and a single module would flatten the pixel protocols' semantics (universes, discovery, sync, priorities) | -| A cap on the current a frame draws, a WiFi TX-power cache | #114 riders | not this idea; already dropped | -| FSEQ as a decoder in a generic stream source | the first draft of this plan | FSEQ is recorded channel data, so the player is a thin effect beside Network Receive on one sink, each with its own settings (decided 2026-10-14) | -| Sender mapping through the inverse of the layer's mapping | G1's first form | composed with the layout the inverse cancelled the static modifiers too; the layout's own positions replace it | -| A setting that writes one feed straight into the drivers' output, bypassing the layer | WLED's realtime override | it gives up blending, modifiers and composition; G5 takes that path on its own where nothing composes | - -**In a different shape:** - -| What | As proposed | Instead | -|---|---|---| -| Who builds the groundwork | the contributor shapes `VideoService` and the descriptor | MoonLight builds the groundwork below, since core owns the hard constructs; contributors add sources on top in small pull requests | -| USB capture | linked into every P4 image | only into images with room, measured as the SD card was (#136), and after the browser tab source, which serves every board | -| Smoothing | a backlight setting | a picture effect setting, since temporal smoothing applies to any picture, or dropped | -| Letterbox detection | built in | a later sampler option, when a user asks; every setting names the user who needs it | -| The capture seat, one instance per board | the default for every source | only for transports with exclusive hardware (USB, CSI); files and the network feed several sources at once | -| Multi-board sync | FPP MultiSync, as WLED's attempts did | MoonLight's own clock sync from the core backlog first; FPP compatibility later, as an adapter, if users mix in Falcon Player | - -**As proposed:** #140's start corner, direction and offset (the textbook layout model, and small); area sampling in linear light (#138); the capture seam's shape, an opaque handle with a generation counter and formats learned at enumeration (#139); FSEQ playback, starting on the cube. - -## The groundwork - -What MoonLight builds first, so that every contribution after it is one source in a small pull request: - -| # | Piece | Where | What it does | -|---|---|---|---| -| G1 | **Light-list placement** ✅ built | `Layer::placeByLayout` | a sender-mapped frame placed by the layout's positions: [frames from outside](../explanation/architecture/moonlight.md#frames-from-outside) | -| G2 | **Network Receive's `mapped_by`** ✅ built | `NetworkReceiveEffect` | `receiver` or `sender`, the status naming the path: [its card](../moonmodules/light/effects.md#networkreceive), the cost in [Performance](../reference/performance.md#network-receive) | -| G3 | **The picture source** ✅ built | `PictureService` | from #138's `VideoService`, a test pattern its first source: [its card](../moonmodules/core/services.md#picture) | -| G4 | **The picture effect** ✅ built | `PictureEffect` | from #138's sampler, point or area in linear light, fit or stretch: [its card](../moonmodules/light/effects.md#picture) | -| G5 | **Sender mapping skips the board's mapping** ✅ built | `Layer::wiringSource`, `Drivers` | a sender-mapped frame alone on its layer goes to the output in one copy on any layout: [frames from outside](../explanation/architecture/moonlight.md#frames-from-outside) | -| G6 | **The channel-data sink** ✅ built | `ChannelDataSink` | extracted from Network Receive, so a file source is a thin effect on it: [its header](../../src/light/effects/ChannelDataSink.h) | -| G7 | **A contributor page** ✅ built | docs | [Adding a picture or channel-data source](../reference/adding-a-stream-source.md) | - -The picture sources still to come, the browser among them, are steps of [Video in, LED walls out](Plan-20261024%20-%20Video%20in%2C%20LED%20walls%20out%20(0%20of%2010).md). - -G3 and G4 took #138's core part, credited to Gohnnyman. - -## How contributions arrive - -Each later piece is a small pull request, one topic, based on current main, with only its own commits: - -| Pull request | Who | On | -|---|---|---| -| ✅ **Backlight wiring** (#140): start corner, direction, a count per edge and a gap, [the layout script](../../moonlive/layouts/backlight.mll) | Gohnnyman | nothing | -| ⏳ Color trim, split from #139 | Gohnnyman | nothing; a separate idea | -| ✅ **FSEQ playback** from the filesystem with a playlist, on the channel-data sink: [its card](../moonmodules/light/effects.md#fseqplayer) | TouchMyLight | G1, G2, G6 | -| ✅ **Border sampling**: edge depth, smoothing, [the Picture effect's card](../moonmodules/light/effects.md#picture) | Gohnnyman | G4 | -| ✅ **USB capture** with its MJPEG decoder on the P4: [the Picture service's card](../moonmodules/core/services.md#picture), the seam in [platform.h](../../src/platform/platform.h) | Gohnnyman | G3 | -| ⏳ Recording the drivers' output to FSEQ on SD, clock sync | open | FSEQ playback | - -Pull requests #138 and #139 close with thanks once G3 and G4 land, their good parts credited in the groundwork; a stacked pull request waits for the one below it, as [CLAUDE.md](../../CLAUDE.md) says. - -## Skeletons for the contributions - -What each pull request builds, settled before code so it arrives small and on design. The names follow the open naming question below. Each passes `check_specs` with a card, and its tests pass on the desktop before a board sees it. - -**FSEQ player (TouchMyLight), on G6** ✅: built as [the FSEQ player](../moonmodules/light/effects.md#fseqplayer). - -**The picture source (G3) and the picture effect (G4), from #138** ✅: built as [the Picture service](../moonmodules/core/services.md#picture), [`PictureFrame`](../../src/core/util/PictureFrame.h) and [the Picture effect](../moonmodules/light/effects.md#picture). - -**Backlight wiring (#140)** ✅: built into [the Backlight layout script](../../moonlive/layouts/backlight.mll), Hyperion's count per edge and gap included. - -**Color trim (#139's second commit).** Output calibration, so it lives in the fixture profile and the correction, judged against a 3×3 correction matrix before code. - -```cpp -// light/drivers/FixtureProfilesModule.h: per profile, beside the channel roles -uint8_t trimRed = 255, trimGreen = 255, trimBlue = 255; // gain per channel -// light/drivers/Correction.h: rebuild() folds the trim into each channel's table, so the hot path is unchanged -``` - -Tests: a trim scaling one channel and leaving the others; a trim of 255 being the identity; the trim surviving a profile save and load. - -## Where it stands - -Built: G1 to G7, the FSEQ player, border sampling, USB capture and the backlight wiring. Around them, measuring Network Receive on real boards built: - -- **Network Receive's receive task** on core 1 and its `packets_per_s` and `frames_per_s`: [its card](../moonmodules/light/effects.md#networkreceive). -- **TryLock's release by exchange**, which PSRAM's emulated read-modify-write needs: [its header](../../src/core/util/TryLock.h). -- **The receive defaults**, a UDP mailbox of 32 and 30 Ethernet receive buffers on the P4: the reasons beside them in [the base sdkconfig](../../esp32/sdkconfig.defaults) and [the P4's](../../esp32/sdkconfig.defaults.esp32p4rev1-eth). -- **Network Send's paced packets**, `packet_gap`: [its card](../moonmodules/light/drivers.md#networksend). -- **Scenarios over several boards** (`--board name=host`): [Testing, several boards](../reference/testing.md#several-boards). -- **The measurements**: [Performance, Network Receive](../reference/performance.md#network-receive). - -Open here: the color trim, recording and clock sync; the picture sources are in [Video in, LED walls out](Plan-20261024%20-%20Video%20in%2C%20LED%20walls%20out%20(0%20of%2010).md). The rest is in the backlog below. - -## Backlog, found while measuring - -- **12,288 lights received at 270 packets a second, 2026-10-22.** P4 Ethernet to shiffy over WiFi, DDP at 40 fps: about 270 of 1,040 packets a second arrive and 10 frames, where the table above has 616 and 23. Not the 3,840-light ring shiffy now drives (the same with ParallelLed off), not WiFi's code in flash (the same with it in IRAM), and 64x64 Art-Net arrives whole at about 1,000 small packets a second; about 400 KB/s against 900 then points at the link, the access point's channel that day, a hypothesis until a run on another channel or access point. The scenario's observed values were not updated from it. - -- **The UDP mailbox of 32 and the paced send task on boards without PSRAM.** `CONFIG_LWIP_UDP_RECVMBOX_SIZE=32` is in the base defaults for every board. It costs about 128 bytes per socket, and on WiFi the queued packets are WiFi's own receive buffers, capped at 32. On Ethernet without PSRAM they come from the internal heap at about 1.6 KB each, so a full queue holds about 50 KB on a classic board with 100 to 170 KB free, which could starve the HTTP server during a burst. Measure it with `scenario_NetworkReceive_two_boards` on a classic Ethernet board (the Olimex at 192.168.1.210) receiving from a P4, reading free heap and the HTTP answers; if it hurts, the 4 MB classic fragments override it with a smaller value. Measure Network Send's paced task there too, with the classic as the sender: about 5.5 KB of internal RAM per driver (a 4 KB stack, the 1.45 KB packet buffer, a timer), and on a single-core chip such as the C3 the send task outranks the frame loop, so each packet briefly interrupts it. -- **The two-board scenarios on every platform.** `scenario_NetworkReceive_two_boards` and `scenario_NetworkSend_one_sender_many_receivers` run today on S3 boards over WiFi and P4 boards over Ethernet, in all four pairings. Add the Teensy 4.1 (Ethernet), desktop builds over WiFi, and the NanoPi over Ethernet, as sender and as receiver, so every platform's receive cost and loss sits in the same table. A desktop or NanoPi board joins with `--board name=:8080`. The Teensy build runs bare-metal with no scheduler, so Network Send and Network Receive work inside their tick (`spawnPinnedTask` returns false); QNEthernet fills its receive buffers in an interrupt, so a late drain loses less there. Its runs show whether threads (TeensyThreads or a FreeRTOS port, with stacks in its tight RAM and shared structures made thread-safe) are worth it. -- **E1.31 multicast never arrives.** Network Receive binds 5568 but never joins the multicast groups, so both receivers in `scenario_NetworkSend_one_sender_many_receivers` read 0 packets while Network Send multicast; `UdpSocket::membership` exists in the platform layer and goes unused. Join the groups of the universes the layer covers, from `universe_start` on. -- **The Art-Net broadcast counts do not add up.** In the same scenario at 64x64 (about 1,000 packets/s sent, 40 frames), the S3 counted 1,212 packets/s and 96 frames/s while the P4 counted 488 and 39. Another bench board broadcasting, or duplicates over WiFi; a packet capture on port 6454 settles it. -- **A sending P4 reboots with 30 Ethernet transmit buffers.** With 30 x 1600 bytes for transmit a P4 rebooted about 15 s into sending; IDF takes every Ethernet DMA buffer from internal RAM, about 96 KB with 30 + 30, so internal-heap exhaustion is the suspect, unverified. troyhacks raised and reverted the same values within 26 minutes ([bdf6c17e](https://github.com/troyhacks/esp32-arduino-libs/commit/bdf6c17e), [c8a43b7e](https://github.com/troyhacks/esp32-arduino-libs/commit/c8a43b7e)). Transmit stays at 10. -- **The P4's HTTP server answers "incomplete request body" under a stream.** Two `set_control` writes and one `apply_state` failed this way while the P4 received DDP over Ethernet; the same short body window [backlog/core](https://github.com/users/MoonModules/projects/5?pane=issue&itemId=287075256) already names. -- **`CONFIG_ETH_IRAM_OPTIMIZATION=y` on the P4.** The one Ethernet change troyhacks kept; measure it with `scenario_NetworkReceive_two_boards`, P4 to P4. -- **A UDP mailbox of 64 on the P4.** Espressif's own P4 Arduino default; 32 against 64 measured the same on the S3 over WiFi, so measure it P4 to P4 over Ethernet, where bursts arrive at wire speed. -- **The receive task's priority on Ethernet.** Ours runs at 3 on core 1, below the driver task (`mmEncode`, 5); troyhacks' Art-Net receive runs at 20 on core 0, blocking in `recvfrom()`, above lwIP's own thread (`tcpip`, 18), which a busy drain could starve. On the S3 over WiFi 6 against 3 changed nothing, the loss being at the access point. Measure 6 (above the driver task) and 19 (above lwIP) P4 to P4. -- **Network Send paces its packets.** Back-to-back bursts are what the access point drops (a 0.5 ms gap recovered 96% on WiFi) and what loses the push packet at the end of an Ethernet burst. Spread a frame's packets over part of the frame period, and back off on a full transmit queue (`ENOMEM`) rather than adding buffers, as Espressif advises ([esp-idf#13602](https://github.com/espressif/esp-idf/issues/13602)) and troyhacks' raw Ethernet driver does. -- **The live runner restores no board when a board fails mid-run.** A two-board run whose receiver rebooted left the sender's scenario canvas and its Network Send on P4 .139, still streaming to the S3. The restore of every board belongs in a `finally`, so an exception or a dead board still puts the others back. -- **HTTP to an S3 fails under a stream from the same computer.** With a 40 fps DDP stream from the Mac into either S3 over WiFi, 4 to 6 of 20 HTTP probes from that Mac answered, while the render loop stayed healthy (log: 5.5 ms ticks, the HTTP server idle) and the Ethernet P4 took the same stream with every probe answered. A phone opening the S3's page during such a stream tells whether the S3's WiFi suffers or only the sending computer's link carrying both. - -## Open questions - -- The names: `PictureService` and `PictureEffect`, or names closer to the industry's (media source, pixel map)? -- Whether a layout reads xLights model exports, so a 3D model's node order and positions come from the file that designed the show. - -## Prior art - -- GStreamer and FFmpeg: transport, format and geometry as separate concerns; one element model with typed outputs. -- TouchDesigner: one operator model, separated by data type into channel operators (CHOPs: DMX In, Art-Net, MIDI, audio) and texture operators (TOPs: video, files, capture), the reference for the two families. -- Inverse mapping: a GPU fragment shader sampling a texture through a composed coordinate transform, and OpenCV's `remap` with a precomputed table; the reason MoonLight composes geometry instead of chaining frames. -- MADRIX: the closest tool, LED pixels with effects, layers, a 3D patch, media, capture and standalone playback. -- Resolume: video layers with pixel mapping to fixtures on a PC. -- Hyperion and HyperHDR: grabbers, LED layout, mean-color zones, smoothing, black-border detection. -- xLights and the FSEQ format (v2: header, channel count, frame count, step time, optional zstd and sparse ranges); Falcon Player for standalone playback and MultiSync; ESPixelStick for FSEQ from SD on an ESP32. -- WLED: the Image effect (PR #3835), PixelForge (PR #4982), DDP over WebSocket (PR #4997), and the unmerged FSEQ attempts (#2413, #4570, #5641). diff --git a/docs/work/Plan-20261015 - Processes, described once (18 of 23).md b/docs/work/Plan-20261015 - Processes, described once (18 of 23).md deleted file mode 100644 index 404760ce..00000000 --- a/docs/work/Plan-20261015 - Processes, described once (18 of 23).md +++ /dev/null @@ -1,117 +0,0 @@ -# Plan: processes, described once - -Plan, approved 2026-10-15. MoonLight has many processes, from a first reader to a release, and each was written in its own place and shape. This plan finds one model for all of them, so a process is described once and followed by everyone it concerns: a user in the app, a reader of the docs, a test, an AI agent and the product owner. It is called **MoonWalk**: the model, its stage files under `moonwalk/`, and the map that shows them. - -Where things are described: [the MoonWalk reference](../reference/moonwalk.md) is what exists and how to use and write it; [the model](../explanation/architecture/moonwalk.md) and [its schema](../../moonwalk/moonwalk.schema.json) are why it is shaped so, chosen against the textbooks (Patton's story map, Arazzo, Given-When-Then, Diátaxis). This plan holds what is still to build, and a line per step done. - -## Why - -The same steps lived in several places: installing on the desktop was a how-to, a getting-started section, a README paragraph ; the commit gates a diagram and prose in CLAUDE.md, the scripts in MoonDeck.md and the agents' memory; a scenario listed steps in its own format. Every copy drifted on its own. **What it removes**: the duplicated step lists, the separate step formats once the mapping shows which reduce to one, the hand-written gate results in commit messages once the gates record on GitHub, and `collect_kpi.py`'s own map of which script measures what, once each step declares its metrics. - -Neither a Confluence nor a Jira: the docs site is ours and GitHub's issues and Projects track work. What is built is a schema, the process files, one generator and an adapter in each runner. Out of scope: a process engine, an editor, a server, states or permissions beyond GitHub's. The map and its renderer download within 50 KB; the firmware carries none of it. - -## Still to design - -**The runners.** A runner performs the steps it knows and skips or describes the rest. Built: the app (Show me, the follow bar), MoonDeck (Run, the gates), the docs (the map and the site). To build: the scenario runner and the unit tests as runners of a process's steps (step 17), and agents reading a process for the task they are given. - -**The APIs a step calls.** Built: the device's REST API in OpenAPI 3.1. Candidates: the WebSocket's state stream, as AsyncAPI, once a process needs to wait on a pushed state; the GitHub REST and GraphQL APIs for releases, issues, check runs and Projects; MQTT and Home Assistant as tool steps. Two tools are worth a look once they replace code of ours: Redocly's Respect runs Arazzo workflows against an API, and Spectral lints them. - -**Flows.** Built: `dependsOn` orders processes, a `process` step runs another, and a choice (`inputs`, `given`) is a branch. Not built: a Mermaid flowchart per process with a lane per actor, which CLAUDE.md's diagrams were to become. BPMN is the move only if a process needs timers, events or swimlanes, which none does. - -**Metrics on steps.** A step declares what it measures, as Arazzo declares a step's `outputs`, and each run records the values on its instance, so a process carries its measurements rather than a report knowing which script produces which number. The measurements exist, spread over scripts: a scenario's tick time and heap, a build's flash size, the code ratchets, the gate times. `collect_kpi.py` gathers them per commit; the process file would name them per step, and the trend of a value is the series of its runs. The process itself is measured the same way: the DORA metrics (lead time, deployment frequency, change failure rate, time to restore), with a control chart to tell noise from a shift. - -**Across origins and MoonCloud.** Browser storage stays on its origin, so what crosses origins is carried. Built: the address (`?walked=install`, the installer's handover). Candidates: `postMessage` between a page and a window it opens; marks kept on the device's own config, which costs a persisted control the 2026-10-16 decision kept out; and MoonCloud, with consent. A MoonWalk member of MoonCloud, its own checkbox and off until switched on like Stats and Talk, keeps the marks under the installation id so they follow the device to every browser, and in aggregate shows where people stop, a funnel per process. It sends process and step ids and nothing typed, under the privacy policy's rules (step 22). One consent already covers what the app reads from the docs site, the help and MoonWalk (decided 2026-10-16). - -**GitHub: issues, Projects, CI, the backlog and notes.** MoonWalk in the repo is the definition; GitHub holds the work and the runs, each feature one job. **Issues** are the work items, and the issue forms are generated from MoonWalk (the steps of Log an issue become the bug form); a card's Process field names the process it serves, so the map shows each process's open work. **The backlog is the Project's draft items**, each carrying its fields (Kind, Platform, Trigger, Stage, Process), and `docs/work/backlog_mirror/` is their generated copy, one document per Area, so the repository versions every edit and a reader browses it as documents (step 21). A draft item becomes an issue when it is accepted, so the issue list holds only accepted work and reports. **A Project** orders them, one bag of items sorted by fields, since a draft item carries no labels and no sub-items and a view groups and filters on any field: - -| Field | Type | Values | -|---|---|---| -| Status | single select | Inbox, Backlog, Next, In progress, Waiting (blocked on someone outside the project: a reply, a part, a tester), In review, Done | -| Area | single select | Effects, Modifiers, Layouts, Drivers, Services, Core, Platform, UI, MoonLive, Process and docs: the `src/` tree and the module roles | -| Module | text | the module's class name, such as `AudioService`, empty for an item across modules, so a filter gives one module's backlog | -| Kind | single select | Bug, Feature, Restructure, Investigation | -| Trigger | text | what makes the item worth building, so a met trigger is found by scanning one column | -| Platform | single select | classic, S3, P4, C3, Teensy, desktop, Pi, all | -| Stage | single select | the MoonWalk stage an item serves | -| Process | text | the MoonWalk process id, text since the processes outnumber a select's options; the map lists a process's open cards from it, and a card naming no process fails the docs build | - -The views: a board by Status for the work in flight; a table of the Backlog grouped by Area, the overview the mirror's documents give; a saved view per Area, opened when a topic is picked; and the bugs alone, sorted by Area. An item a plan picks leaves the Project for the plan's own backlog, and what that backlog still holds when the plan ships returns as draft items, so the Project holds what no plan covers. **Notes reach the Inbox** as a Project draft item, from the phone, `gh project item-create` or the agent, triaged into an issue or deleted (step 20). **Actions run what is already executable**: a step names a workflow, the local gates post check runs (step 19). Access: the agents' `gh` needs the `project` scope. - -## The steps - -📖 marks what [the MoonWalk reference](../reference/moonwalk.md) describes. - -Steps 1 to 16 are done, each in a line with where its knowledge lives; git holds how. - -1. ✅ **Inventory:** every process that exists as content, 71 entries, with the duplicates counted; each became a stage file under `moonwalk/`. -2. ✅ **The model**, approved 2026-10-16: the journey, the process format and the step shape: [the model](../explanation/architecture/moonwalk.md) and [its schema](../../moonwalk/moonwalk.schema.json). -3. ✅📖 **A pilot: the whole journey on a map**: `gen_moonwalk.py` writes `moonwalk.json`, and `moonwalk.js` draws it with Cytoscape.js: [How the map is built](../reference/moonwalk.md#7-how-the-map-is-built). -4. ✅📖 **A pilot on the device**: the app hosts the map, and the device's state marks a step done: [On a device](../reference/moonwalk.md#on-a-device). -5. ✅📖 **The process view, live**: one renderer shows a process whole in every host: [Opening a process](../reference/moonwalk.md#3-opening-a-process). -6. ✅📖 **MoonWalk in the installer and MoonDeck**: the installer hands a done install to the device, MoonDeck marks script steps from its runs, MoonBase links the map: [In the installer](../reference/moonwalk.md#in-the-installer), [MoonWalk in MoonDeck](../reference/moonwalk.md#6-moonwalk-in-moondeck), [Opening MoonWalk](../reference/moonwalk.md#1-opening-moonwalk). -7. ✅ **The content reviewed**: one install process with a platform choice, Docker among its options; stages, dependencies and goals tuned: [Install MoonLight](../moonwalk.md#install). -8. ✅📖 **The help and MoonWalk, linked**: one key, checked in the build; the help lists a module's processes; the follow bar: [Help and MoonWalk, linked](../reference/moonwalk.md#11-help-and-moonwalk-linked). -9. ✅📖 **The developer process**: the commit, merge and release gates and a contributor's pull request are processes, which MoonDeck marks fresh, times and runs: [What MoonDeck serves](../reference/moonwalk.md#10-what-moondeck-serves-gates), [MoonDeck](../../moondeck/MoonDeck.md), [Contribute a pull request](../moonwalk.md#contribute-pr). -10. ✅ **The mapping**, 2026-10-17: every artifact mapped onto the model, with the copies to remove; step 11 carried it out, and [the stage files](../../moonwalk) are its result. -11. ✅ **The migration**, 2026-10-19: each old copy deleted as its process took over, `docs/how-to/` folded into the processes and `reference/`, within the caps and the download budget: [The folder is the type](../contributing/documentation-standards.md#what-we-document), [Prose and caps](../reference/moonwalk.md#prose-and-caps). -12. ✅ **The REST API in OpenAPI**: rendered on the REST API page and held to the server's routes by a test: [REST API](../reference/api.md). -13. ✅ **Tests link to the test list**: a module's Tests link opens its entry on the test page: [Turn help on in the app](../moonwalk.md#turn-help-on). -14. ✅ **The rules on AI, compared**: the contributor vouches and says what an agent wrote, a root cause is a hypothesis until traced, an agent posts only what the product owner read: [CLAUDE.md's Roles](../../CLAUDE.md#roles), [Contribute a pull request](../moonwalk.md#contribute-pr). - -**The order from step 15 on**, decided 2026-10-19: the steps run in their numbers. The checks (15) come first, since every later step leans on them. A step's `then` (16) is what the scenarios (17) converge on. The guides (18) are independent, and the standards and the test engine are [a plan of their own](https://github.com/MoonModules/MoonLight/issues/205). GitHub (19) needs the checks and the processes settled. The backlog moved into the Project (21) before the Project's trial (20) ran, so the trial is the cards in daily use. MoonCloud (22) is last. - -15. ✅ **The checks.** A step naming a script, scenario, page, anchor, operation, module or control that does not exist fails the build; a process no runner verifies is listed, not counted; a check that could not run keeps its counts: [What the generator checks](../reference/moonwalk.md#9-what-the-generator-checks), [check_docgen](../../moondeck/MoonDeck.md#check_docgen). -16. ✅📖 **Steps that know they are done.** A step's `then`, derived for an `add` and a `set` and written by hand on a `call` that changes the device, marks the step done and shows under it: [Writing a process](../reference/moonwalk.md#8-writing-a-process), [On a device](../reference/moonwalk.md#on-a-device). -17. ✅📖 **Examples of a process: the tests**, 2026-10-25: a scenario names the steps it proves in `steps_proved` and a unit test in `// @step`, a step that sets a control is covered by any scenario setting or expecting it, a test naming no step fails the build, and the test page's Gaps lists the device-changing steps no test covers. A step has no loop, so the two network scripts stay scripts: [Opening a process](../reference/moonwalk.md#3-opening-a-process), [Writing a process](../reference/moonwalk.md#8-writing-a-process), [the test page](../reference/tests.md). - - **The test index and the test engine** are [their own plan](https://github.com/MoonModules/MoonLight/issues/205). - - ⏳ **Backlog: the seven steps no test covers.** - - `control-surface/add-midi`, `source-usb`, `source-net`, `share-source`, `share`: the MIDI tests cover the USB-MIDI and RTP-MIDI protocols, not MidiService's `source` and `share`. A desk on the bench proves them; a fake transport behind MidiService would let the desktop do it. - - `multi-device/effect`: boards running one layout's script together, bench work. - - `watch-output/add-rtsp`: RtspDriver listens on port 554, which a CI runner without root cannot open, and the recording encoder stand-in yields no H.264 frames to packetize. A port the test sets and a stand-in that hands out a frame make it a desktop test. -18. ⏳ **Guides that add, and `@technical` trimmed**, from the help plan. A control's `@guide` line says only what its line cannot hold, and a module with depth gets a guide about itself, how it works and how to get a look, rather than one walking its controls. An `@technical` appendix keeps only what neither the code, the test names, the base class nor the help says, one or two sentences each; a fact every module of a role shares is said once in the role's base class. Judgment work over hundreds of headers, read here for the processes it holds: a guide's "how to get a look" is a process's steps, so the inventory of step 1 reads the guides and appendices, and what is procedural moves into a process file rather than being trimmed twice. What is reference rather than reasoning, control tables, protocols and wire formats, moves to `reference/`, so it is the one folder for detail; why the code is built as it is stays at the code, where it changes with it, and the appendix line count falls rather than relocates. -19. 🚧 **Executions on GitHub.** (Built 2026-10-25: `CONTRIBUTING.md`; the pull request template, generated from the questions `contribute-pr`'s steps ask in their new `asks` field; the pull request workflows as thin callers of the scripts `ci-on-pr` names, held so by `test_workflows.py`. Built 2026-10-29: `release.yml`'s build and staging steps as calls of [release_stage](../../moondeck/MoonDeck.md#release_stage), named by the [CI on a release tag](../moonwalk.md#ci-on-release) process, its publishing steps exempt by name; ThreadSanitizer and every Linux job on a Mac through [act](https://github.com/nektos/act), which runs the workflows themselves in Linux containers; and [run_process](../../moondeck/MoonDeck.md#run_process), which runs a process's script steps locally, `ci-on-pr` among them. Missing: a release-candidate tag proving the new `release.yml`, and recorded runs and their state.) A process file is the definition and one run of it is an instance, the split workflow engines make. GitHub keeps the instances; the definition stays in the repo as the one copy. **Decided 2026-10-25:** `CONTRIBUTING.md` is written by hand as one sentence linking [Contribute a pull request](../moonwalk.md#contribute-pr), since a link copies nothing and needs no generator. `.github/pull_request_template.md` is generated from `contribute-pr`: it asks what the process says a pull request states (one topic within the size cap, based on main, what an agent wrote, what was tested) and links the steps, without a checklist of them. Steps come in two kinds: - - **Volatile steps** act on a device or in the app, such as a user choosing an effect. Nothing records them, since nothing outside the device changes. - - **Recorded steps** change the repo or what ships: a plan step, a commit gate, a merge, a release, a verification on hardware, a gallery submission. Each run leaves a record. - - A run's state is Started, Done or Stuck. Stuck is a run waiting at a prompt for minutes, which `walk_status` reports as it reports a stale gate, and which reaches the product owner where they read, Discord included. Seen 2026-10-21 in a community member's setup: a bot posting each agent run's Started, Done and Stuck to Discord. - - A run's measurements, the step metrics above, ride on its record: a check run's output on GitHub, and the value series repo-health draws. - - Automated recorded steps record as check runs on the commit or pull request, the local gates included through `gh api`, so a pull request shows which gate passed on which commit. Manual recorded steps record in an issue created from an issue form generated from the process file, a step per sub-issue, and a GitHub Project shows every run and the step it stands at. **Decided 2026-10-25:** a step's kind follows from its process, not its action, since `manual` and `run` go both ways ("Test it on real hardware" in Release is recorded, "Adjust its controls" in first-light-show is not). Every step of a gate process MoonDeck lists, plus `pick-spec-plan` and `contribute-pr`, is recorded; every other step is volatile. A per-step override waits for a step that needs one. **Decided 2026-10-29:** a shipped or stopped plan is a closed issue labeled `plan`, its body what no commit holds, and the 43 past plans are such issues (#165 to #207); the plans being built follow ([Backlog](#backlog)). - - **Every CI job also runs locally**, so a GitHub outage, or a CI service that changes or stops, costs a runner rather than the gates. A workflow is a thin caller of the MoonDeck script its process step names, and GitHub is one runner of the process files among others. Today each job reproduces by its own script, as `ci-on-pr` lists them, but not as one command, and four gaps remain: ThreadSanitizer needs Linux (`check_sanitizers` skips it on macOS), the MSVC build needs Windows, CodeQL needs its command-line tool, and the release's publishing (the release upload, Pages, the container registry) is GitHub's. `run_process.py` runs `ci-on-pr` locally through `walk_status`, as the commit checks run; `act` runs the Linux jobs in containers, ThreadSanitizer among them; every workflow line that is not a script call is in one, publishing aside; and `test_workflows.py` fails on a job that calls no script a process step names. -20. 🚧 **The Project and the inbox on GitHub.** (Built 2026-10-29: [the Project](https://github.com/users/MoonModules/projects/5) with its fields and the board, Backlog-by-Area and Bugs views; the board mirrored by [project_mirror](../../moondeck/MoonDeck.md#project_mirror); the bug-report form generated from Log an issue (`gen_moonwalk.py --issue-form`); and each process's open cards on the map, from the cards' Process field. Missing: the cards in daily use, and the Inbox trial.) Apple Notes stays the scrapbook it is, the quickest place for a thought. The Inbox is tried as the step after it: a note worth acting on moves there and is triaged. Whether the Inbox can be the scrapbook too is learned by using it, and Apple Notes goes only if it can. Independent of the other steps, so it can run whenever the product owner picks it. The trial is the moved backlog in daily use: queried and edited with `gh`, a card converted to an issue with the Project's own Convert to issue on acceptance, the mirror read by agents; what does not hold up changes the Project's fields or views. - - - ⏳ **Issues in other projects stay current.** A MoonDeck check asks `gh` for the state of each row of [Issues in other projects](../reference/issues-in-other-projects.md) and of the release that carries its fix, and flags a row whose issue closed or whose fix shipped, so a workaround does not outlive its reason. Its rows can become Project items with the rest. - -21. ✅ **The backlog moves to the Project**, 2026-10-29. The 214 items of the Area files are draft items on the public [Project](https://github.com/users/MoonModules/projects/5), uploaded by a one-time script that git keeps, and the Area files are gone. `project_mirror.py` renders the board back to `docs/work/backlog_mirror/`, one document per Area grouped by module, so git versions every card edit, and the commit checks fail when a document differs from the board ([project_mirror](../../moondeck/MoonDeck.md#project_mirror)). A draft item becomes an **issue** when the product owner moves it to Next, the acceptance and the point where an agent may pick it up. An **analysis** is a design study in `docs/design/`, as an RFC is, which the cards and plans it informs cite. Past plans are closed issues labeled `plan`, and the lessons live in `docs/contributing/lessons.md`, so `docs/work/` holds only the plans being built and the mirror. Along the way: each plan holds its own backlog, with the round trip in [Pick, spec and plan a topic](../moonwalk.md#pick-spec-plan); the future docs were swept of what the rest of the docs already say; and ten analyses whose topic shipped dissolved into the explanation, the code and the lessons. Step 20's week of living with the cards comes after the move rather than before it. - -22. ⏳ **MoonWalk in MoonCloud.** The member above: its card and consent text, a route and a table in the MoonCloud worker, the privacy policy's entry, and the funnel shown back on the card as Stats shows its totals. It follows step 4, since only the app has an installation id. - -23. ⏳ **Close the plan.** Once 15 to 22 are done: the lessons the steps taught go to `docs/contributing/lessons.md`, [the MoonWalk model](../explanation/architecture/moonwalk.md) is read against the built system, and the plan closes as an issue, the product owner's call. - -## Backlog - -- **Present plans to GitHub.** A plan with work running becomes an open issue labeled `plan`, its steps a task list whose progress the Project shows; a plan not started waits as a Project card holding it, and becomes an issue when work starts; a fully built plan closes as an issue, the product owner's call. A mirror like the backlog's versions the open plans, and once both mirrors are all `docs/work/` holds, the folder goes. -- **Docs link issues, not draft cards.** About 40 links in the docs, to 23 cards, use the Project's `?pane=issue&itemId=` deep link, a UI address GitHub does not document as stable. The textbook fix: convert those 23 cards to issues with the card's own Convert to issue and link the issue, which the backlog check then verifies; about 20 more open issues. The alternative keeps work documents on cards and has the published pages link the Project filtered to an Area instead. - -## Decided - -- **One install process with a platform choice** over a process per platform (step 7): the steps they share are written once. -- **MoonBase links the map rather than hosting it** (step 6), since a board in recovery often runs offline. -- **MoonWalk decides and MoonDeck serves**, with no runner script of MoonWalk's own (step 9): a step names a script and MoonDeck runs it. -- **A process no runner verifies is listed, not counted** (step 15), since a count of them could never fall: they are the ones a person alone can judge. -- **A `call` gets no derived `then`** (step 16), since its result is not in its action; one is written by hand where the call changes the device. -- **`CONTRIBUTING.md` links, the pull request template is generated** (step 19): only what copies the process's content is generated. -- **A step's kind follows from its process** (step 19): a gate process records its runs, any other is volatile. -- **A step has no loop** (step 17), as Gherkin has none: a format with variables and repetition is a programming language. Work that repeats over boards or measures a spread stays a script a `run` step names. -- **`onSuccess` and `onFailure` left the schema**, 2026-10-20, as no process used them. -- **A card's Process field, not a `walk:` label per process**, links work to its process (step 20, 2026-10-29): the field is on every card already and the mirror carries it, where 73 labels would crowd the repository's label list. -- **The backlog is the Project's cards, mirrored into the repository** (step 21, 2026-10-29), over Markdown files edited by hand: GitHub's workflow does the tracking, and the mirror keeps every edit in git and the backlog readable as documents. A design study stays a document in `docs/design/`, reviewed in a diff. -- **A plan that ships or stops is a closed issue labeled `plan`** (step 19, 2026-10-29), over a file in `docs/work/past/`: its state, close reason and task progress are GitHub's, and its body keeps only what no commit holds. - -## To decide - -- The process of step 17, the first through every runner, and whether it is the one step 5 renders. -- **A support process**: a question on Discord or GitHub, an agent drafting the answer, the product owner approving and sending it. It turns the rule that an agent posts only what the product owner has read into a process. -- **An orchestrator**: one agent session per issue or pull request, started from the Project and reporting its state (step 19). It changes CLAUDE.md's roles, since the product owner initiates every event: whether an agent may start work on an issue the product owner moved to Next, and stop at a draft pull request for review. diff --git a/docs/work/Plan-20261023 - JSON in MoonLive services (6 of 6).md b/docs/work/Plan-20261023 - JSON in MoonLive services (6 of 6).md deleted file mode 100644 index a3a9559a..00000000 --- a/docs/work/Plan-20261023 - JSON in MoonLive services (6 of 6).md +++ /dev/null @@ -1,154 +0,0 @@ -# Plan: JSON in MoonLive services - -Plan of 2026-10-23, a Feature, for approval. A service script fetches JSON from the internet (weather, sports, stocks, news), reads values out of it, and sends JSON to a device, itself included, which is how a script builds its own effects. hpwit's show and the StadBeest autopilot create their layers instead of assuming them, the autopilot on the eyes and the mouth as well, and `jsondemo.mls` shows four live data feeds as scrolling text. - -## What exists - -- **JSON in core:** `JsonUtil.h` parses into a node tree (`parse`, `member`, `element`, `readInt`, `readString`). Numbers are integers only (`JsonType::Int`, a `long`), so `12.3` cannot be read as it is. `JsonSink` writes JSON. -- **Applying JSON to the device:** `applyStateDocument` applies a state document in-process, the same engine `PATCH /api/state` and the presets run. A document's changes are saved; only a service's control writes are live (the Scheduler's live-write scope), so neither the API nor `StateSource` has a live document yet. -- **HTTP in the platform layer:** `httpRequest` sends cleartext HTTP to a LAN host and returns the body; `httpsPost` posts over HTTPS and returns only the status. ESP32 has `esp_http_client` with the certificate bundle (used by the OTA); the desktop has libcurl or WinHTTP. **There is no HTTPS GET that returns a body**, and every public data API needs one. -- **MoonLive strings:** a `string` is only one of the script's own literals, passed to a builtin; "building, joining or comparing text is out of scope". The script's literals share **128 bytes** (`CompileResult::kStringPool`), and four URLs alone run past that. -- **Services** run `tick20ms` on the render loop and write only control-surface slots (`setControl("fader2", v)`), as integers. -- **TextEffect** shows a `text` control (a text area), reachable from a state document. - -## The rule this plan follows - -The architecture says a script's vocabulary follows the compiled one: every builtin is a thin wrapper around a function compiled code can call. The request puts the JSON functions in the power functions library. That library is the light domain's drawing and math toolbox, and JSON and HTTP are domain-neutral. So the compiled home is core (`core/util` and a core service), as CLAUDE.md asks of a hard construct, and power-functions.md links to it. A compiled module gets every capability a script gets. - -## Design - -### The `json` type is a handle - -MoonLive has no runtime strings, so a JSON document lives on the host and a script holds a **handle** to it: a 4-byte slot like `int`, typed so it cannot be used in arithmetic. Text never passes through the script: it moves from document to document on the host (`jsonCopy`), and a number moves as an `int` or `fixed`. - -### Paths are JSON Pointers (RFC 6901) - -`"/current/temperature_2m"`, `"/results/0/title"`: the request's own `/x/y/z/temp`, and the textbook. Setting a value is JSON Patch's `replace` (RFC 6902) at a pointer. - -### Fetching never blocks the render loop - -A DNS lookup and a TLS handshake take seconds on an ESP32. So `getJson` registers a source and answers a handle at once, and a worker task, unpinned at the lowest priority, fetches it and refreshes it every `seconds`. The script reads the last good document (stale-while-revalidate), and `jsonChanged` says a new one arrived. `pushJson` queues likewise. Pushing to the device itself applies the document at the next frame boundary through `applyStateDocument`, as a **live** source, so a show's writes are never saved, the rule the scoped-prepare plan set. Pushing to another MoonLight device sends `PATCH /api/state?live=1`, which applies the document the same way there; any other REST endpoint gets the document as it is. - -A script that builds its look this way is the look's one source: the layers it pushes are gone after a reboot, and the script pushes them again at start. - -### The script vocabulary (proposal) - -| Builtin | Does | -|---|---| -| `json d = R"({"Effects": {...}})";` | a document from a literal, parsed once at start: assigning a string to a `json` is the parse | -| `json d = getJson("https://…", seconds)` | a fetched document, refreshed in the background | -| `bool jsonChanged(d)` | a new fetch arrived since the last ask | -| `int jsonInt(d, "/ptr")`, `fixed jsonFixed(d, "/ptr")` | a number, 0 when absent | -| `jsonSetInt(d, "/ptr", v)`, `jsonSetFixed(d, "/ptr", v)` | write a number | -| `jsonCopy(dst, "/ptr", src, "/ptr")` | move a value, text included, from one document to another | -| `json r = pushJson(d)`, `json r = pushJson("host", d)` | apply to this device, or send to another one (its `/api/state`) or any REST endpoint by URL; `d` is a document or a literal, and `r` is the reply | - -A JSON document is full of `"`, and a MoonLive literal today has no escapes and ends at the line. The literal gains C++'s raw form, `R"( … )"`: it spans lines and keeps quotes as they are, so a REST body pastes in unchanged. A script is then standalone: everything it sets up is in its own text. - -### A missing catalog script is the UI's to fetch - -A pushed document can name a catalog script the device does not hold yet, as a preset or a restored backup can. Today only the picker downloads one, in the browser (`mlDownloadScript`), so a push would leave the module empty. Instead the device reports the script as **missing** in its state, and any open UI downloads it with the same function and writes it to the device; the module starts when the file lands. That is a reconciliation loop: the state says what is wanted, the device says what is there, and a client closes the gap. It holds on every device, with HTTPS or without, and adds no download code to the firmware, as the firmware update already lets the browser carry the image for a device without TLS. Until a UI is open the module shows that it is missing. A script stays on the device once downloaded, so an installation that started once with a UI open runs offline. - -The request's `weather.getValue("/x")` method syntax would need member calls on a value type in the compiler; the free functions above give the same reach without that, and the sugar can follow. - -### The data sources (to verify in step 6: key, size, rate limit, HTTPS) - -| Feed | Endpoint (no key unless noted) | Pointer | -|---|---|---| -| Weather | Open-Meteo `api.open-meteo.com/v1/forecast?latitude=40.79&longitude=-119.21¤t=temperature_2m` | `/current/temperature_2m` | -| Stocks | Yahoo Finance's chart endpoint, unofficial: `query1.finance.yahoo.com/v8/finance/chart/ASML.AS?interval=1d&range=1d` | `/chart/result/0/meta/regularMarketPrice` | -| Sports | TheSportsDB test key `123`: `…/api/v1/json/123/eventslast.php?id=133604` | `/results/0/strEvent`, `/results/0/intHomeScore` | -| News | Spaceflight News `api.spaceflightnewsapi.net/v4/articles/?limit=1` | `/results/0/title` | - -Each answer is a few KB or less; a feed that returns hundreds of KB (an ESPN scoreboard) is refused by the size cap rather than parsed. - -## Extensions - -**Core (compiled, the home):** -- `JsonUtil`: JSON Pointer get, `replace` at a pointer, copy between documents, decimals read as `fixed` (a number type the parser gains), and serializing a document back to text. -- `JsonFetch`, a core service with a worker task, unpinned at the lowest priority: a fixed table of sources (URL, period, last document, generation), a push queue, a response size cap (16 KB with PSRAM, 4 KB without), HTTPS on every ESP32 and the desktop, as OTA and MoonCloud use it today. -- `StateSource::Live`: a document applied under the live-write scope, never saved; `PATCH /api/state?live=1` selects it. - -**Platform:** `httpsGet(url, out, cap, timeout)` returning status and body: `esp_http_client` with the certificate bundle on the ESP32, libcurl or WinHTTP on the desktop, unavailable on Teensy (it says so). - -**MoonLive:** the `json` keyword as a typed handle (compiler only; the backends see an `int` slot), raw string literals in the lexer, the builtins above in the service table, and a string pool sized to each script's literals instead of the fixed 128 bytes every engine reserves, so a 3 KB template costs 3 KB in the one script that has it. - -**UI:** a missing script in the state, from any module, is downloaded by `mlDownloadScript`, the picker's own path, which then has one caller more rather than a second copy. - -**Scripts:** -- hpwit-show creates its six layers, windows and effects with one `pushJson` of a literal at start, then drives them as now. -- `stadbeest-autopilot.mls` creates the whole beast at start: on the legs, itself, the three looks (walk, orb, thrusters) and the stadbeest-fire palette; on each eye the frog eye, eye orb and Sauron; on the mouth its one effect. Each device's document is a literal in the script, sent to the eyes and mouth by name, such as `stadbeest-eye-left.local`. -- **One layer per look** on the legs and on each eye, instead of three effects in one layer switched by `enabled`. A scene change crossfades: the autopilot ramps the old look's layer opacity down and the new one's up, as hpwit-show alternates voronoi and mandala. A layer at opacity 0 is skipped, so outside a crossfade the beast renders one look, as now. `switch3` to `switch5` move from `.enabled` to the look layers' opacity, so the eyes still follow over OSC; the StadBeest page's surface table changes with them. -- `jsondemo.mls` fetches the four feeds and pushes a TextEffect layer per feed, re-pushing the text when `jsonChanged`. - -## Impact and challenges - -- **Memory.** A TLS handshake needs about 40 KB of heap for as long as the fetch lasts, freed after (`MBEDTLS_INTERNAL_MEM_ALLOC`, a 16 KB receive buffer); the worker's stack, 6 to 8 KB, is internal. Today OTA and MoonCloud start a handshake without looking at the heap: a failed allocation fails the request cleanly, but a handshake that succeeds on a near-full heap holds 40 KB for seconds that WiFi and the web server then miss, rare today and every minute with four feeds. So the client setup that `httpsPost` and `httpsGet` share refuses to start while free internal heap is under the floor plus the handshake, and the caller sees a failure it already handles: a feed skips a round and says so, a MoonCloud post waits for the next. The worker fetches one source at a time. Documents live in PSRAM where there is PSRAM, capped per source. -- **Render path.** Nothing network-bound runs on it. A push to the device itself applies at the frame boundary; a new layer is a structural change, so the first push of a show rebuilds the tree once and its text updates afterwards prepare nothing. -- **Security.** A pushed document can reshape the whole device, as the API can: a script is trusted code, the same as today. API keys sit in plain text in `/moonlive`. Fetches go only where the script names. -- **Several devices.** A push to an eye that is off or rebooting fails; the autopilot pushes again when that device answers, so the beast assembles in any boot order. -- **No internet at the venue.** The feeds stop and missing scripts stay missing; a show whose scripts were downloaded before runs as it is. -- **Language.** A new type touches the compiler's type checking and the IR golden table, not the five backends. The string pool change touches every script's memory budget. -- **Testing.** The desktop gets a stand-in fetch (canned documents per URL) so every builtin is pinned without a network; the real feeds are verified on the bench. - -## Estimate - -| Step | Work | -|---|---| -| 1. JSON Pointer, replace, copy, decimals, serialize, in `JsonUtil`, with tests | 1 day | -| 2. `httpsGet` on ESP32 and desktop, with a desktop stand-in | 1 to 1.5 days | -| 3. `JsonFetch`: worker, sources, refresh, push queue; `StateSource::Live` and `?live=1` | 2 days | -| 4. MoonLive: the `json` handle type, raw string literals, the builtins, the per-script string pool; a missing script in the state, downloaded by the UI | 3 days | -| 5. hpwit-show and the StadBeest autopilot create their effects; `jsondemo.mls` | 2 days, plus bench time | -| 6. Verify the four feeds, document (moonlive.md, core services, power-functions link, the StadBeest page, a how-to), MoonWalk | 1 day | -| **Total** | **about 9.5 to 10 days** | - -## Steps - -1. ✅ Core JSON: pointer get and replace, copy, `fixed` numbers, serialize; unit tests per RFC 6901's own examples. -2. ✅ Platform `httpsGet` beside `httpsPost`, sharing its client setup, which gains the heap check both get; its desktop stand-in; a test against the stand-in, the real one on the bench. -3. ✅ `JsonFetch`: sources, refresh, size cap, push queue; `StateSource::Live` and `?live=1`; tests through the stand-in. -4. ✅ MoonLive: `json` keyword and type checks, raw string literals, the builtins, the per-script string pool; compile tests, IR golden table. A module naming a script the device lacks reports it missing in its state, and the UI downloads it; a UI test with the file absent. -5. ✅ hpwit-show builds its own layers, measured on shiffy and the Waveshare; the StadBeest autopilot builds the legs, eyes and mouth, a layer per look, and crossfades its scenes, measured on the beast (the frame time during a crossfade, when two looks render); `jsondemo.mls` with the four feeds and TextEffect. -6. ✅ Feeds verified, docs, MoonWalk process "Show live data from the internet". - -## Decisions (recommended) - -- **The compiled home in core** (`JsonUtil`, `JsonFetch`), which power-functions.md links to. JSON and HTTP serve every domain, and a compiled effect (a weather display) gets the same calls. -- **Free functions now** (`jsonInt(d, "/p")`). They are one builtin each; `d.getValue("/p")` is sugar the compiler can add later as a rewrite to the same call, once scripts show they want it. -- **Documents as raw literals in the script, with a per-script string pool.** A script that sets up its look is standalone, with no data file to ship beside it, and the pool sized to a script's literals costs no byte it does not use. -- **A missing script is downloaded by the UI, not the device.** One download path for the picker, a push, a preset and a backup, on every device, Teensy included. -- **HTTPS on every ESP32**, as OTA and MoonCloud use it today: a standalone device shows internet data with no other machine running. An HTTPS request, MoonCloud's included, waits for heap rather than taking the internal RAM floor. -- **Every push is live, to the device itself and to another one.** The script is the look's one source and builds it again at start, so a show writes no flash and a reboot returns the device to its saved state. - -## What it removes - -Nothing in code: it adds a type, a service, a state source and a platform call. hpwit-show no longer needs its layers set up by a scenario first, so `scenario_MoonLive_hpwit_show`'s layer steps shrink to staging the scripts. The StadBeest page's per-device setup by hand becomes "install the autopilot on the legs". - -## Where it stands (2026-10-24) - -Steps 1 to 6 are built, desktop-tested and run on shiffy, the Waveshare and the StadBeest: the autopilot on the legs builds the legs, both eyes and the mouth's two bulbs (`MM-mouth`, `MM-mouth2`), and the eyes and the mouth follow it over OSC. - -The StadBeest's frame time, the median of one-second averages over four minutes of 20-second scenes on 2026-10-24: - -| Look (legs / eyes) | Legs | Eye1 | Eye2 | -|---|---|---|---| -| walk / frog eye | 10.1 ms | 8.6 ms | 8.4 ms | -| orb / eye orb | 15.4 ms | 10.1 ms | 11.3 ms | -| thrusters / Sauron | 23.9 ms | 20.6 ms | 20.1 ms | -| a crossfade, two looks rendering | 30.9 ms | 23.7 ms | 26.1 ms | - -A crossfade costs about the heavier look plus the lighter one, and the slowest, thrusters into walk, holds 32 fps on the legs. One second during a crossfade averaged 293 ms on eye1 and 48 ms on eye2, a stall in that second. - -What the build changed from the plan: -- **The rule for a busy target.** A second push to a target with one on its way waits and replaces any that waited before it, so no update is lost; dropping it lost text updates. -- **Text and numbers from services.** Many services quote their numbers (`"price":"252.11"`), so `jsonInt` and `jsonFixed` read a quoted number, and a script joins text with `jsonAppendText` and `jsonAppend`. `\uXXXX` escapes decode to UTF-8. Every call a script has: [the MoonLive language](../moonmodules/light/moonlive.md#json-documents-in-a-service). -- **Certificates.** Every Google Trust Services site (Coinbase, TheSportsDB, Spaceflight News) failed on the ESP32 with "No matching trusted root certificate found": it serves GTS Root R4 cross-signed by GlobalSign. `CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY` fixes it, for every board. -- **`.local` names.** `esp_http_client` resolves through DNS alone, so the ESP32's `httpFetch` looks a `.local` host up over mDNS first. -- **TheSportsDB's test key** is `123`; `3` is retired. -- **The StadBeest's looks** fade through `encoder6` to `encoder8`, since a switch writes true or false and an opacity needs 0 to 255; `switch3` to `switch5` are freed. -- **`httpsPost`** is gone: MoonCloud posts through `JsonFetch` on the worker, over `httpFetch`, the one client every request shares, so the heap check covers it too. -- **HTTPS needs PSRAM.** On a classic ESP32 without PSRAM (`MM-Olimex`, 2026-10-24) the request waits for 145 KB of internal RAM, the 100 KB floor plus a 45 KB TLS session, where the board has 55 KB free at idle and 19 KB with `jsondemo.mls`'s four text layers; the bands kept their placeholders. Such a board gets its data pushed from one with PSRAM ([the MoonLive language](../moonmodules/light/moonlive.md#json-documents-in-a-service)); smaller TLS sessions are in the backlog. A plain-HTTP feed runs there: `weather.mls`, Open-Meteo over `http://`, showed "Amsterdam 12.2 C" on `MM-Olimex` with 60 KB still free. Of the demo's other feeds only Open-Meteo answers without TLS; the rest redirect to HTTPS, which a fetch refuses. -- **The share price** is ASML's in Amsterdam, from Yahoo Finance's chart endpoint, keyless and unofficial, where the demo first read bitcoin's. - -Seen on hardware (`MM-Olimex`, 2026-10-24): a saved service naming `chase.mls`, which the board lacked, listed it as missing; an open page downloaded it into `/.moonlive`, and after a reboot with no page open the service compiled it from there. diff --git a/docs/work/Plan-20261024 - One resumable send in the HTTP server (9 of 9).md b/docs/work/Plan-20261024 - One resumable send in the HTTP server (9 of 9).md deleted file mode 100644 index 3338dacb..00000000 --- a/docs/work/Plan-20261024 - One resumable send in the HTTP server (9 of 9).md +++ /dev/null @@ -1,31 +0,0 @@ -# Plan: one resumable send in the HTTP server - -Plan of 2026-10-24, a Restructure, from the Reviewer's general review of `HttpServerModule` (3,175 lines) on the branch that made a late state document, large controls and low-RAM sends resumable. The server serves a classic ESP32 with 40 to 66 KB of internal RAM free, ESP32-C3 bulbs, an S3 driving thousands of LEDs on the same task, a P4 on Ethernet, the Teensy and the desktops, and it is tuned for the S3 with PSRAM and one browser. - -- **Half resumable, half blocking.** Embedded files, the state stream, the preview and a late `/api/state` body resume across ticks; patches, every REST answer written through a socket sink, flash-file downloads and firmware uploads still block the render loop on the client: a patch to a client that stopped reading freezes the LEDs up to 2 s before the close, a GET `/api/state` on a large S3 tree waits on each ACK, and a firmware upload without MoonBase freezes rendering up to 3 minutes. -- **Five mechanisms move bytes to a socket**, each with its own slots, budget, stall guard and low-RAM policy: the preview drain, the state stream, the transfer slots (upload, download, document), the embedded sends, and the blocking firmware pull. Three WebSocket header writers. -- **Every other bodied route still answers 400 to a late body on hardware.** The body waits are iteration counts of `delayMs(1)`, which a 100 Hz FreeRTOS tick rounds to a yield: 50 ms on the desktop, microseconds on the ESP32. `/api/control`, `/api/modules`, the list, WLED and firmware-URL routes fail when a client sends headers and body apart. - -The goal: every send and every request body resumes across ticks through one mechanism, no client can stall the render loop, and the server's sizes follow from the boards and the clients it serves. - -## What exists - -- `FileTransfer` slots (upload, download, document) with `driveTransfers`, a 3 ms budget and a 10 s stall guard; `EmbeddedSend` slots; `StateStream` per control client with a `TreeCursor` that resumes inside a control; `PreviewSend` with a per-client cursor. -- The low-RAM lease (`maySend`, `endSend`, `sent`): below `kSendHeadroom` one send queues at a time, a stalled holder hands it on. Files, streams and the preview take it; patches, REST answers and downloads do not. -- `uploadPull` drains a firmware body under `kFirmwareIdleMs` and `kFirmwareUploadHardMs`. - -## Steps - -1. ✅ **One rule for every bodied request.** A body not all in yet waits in a Document slot, with the route's handler deferred until it is whole, as `/api/state`'s does; both iteration-count waits go. Unit tests: a state document and a control set sent after their headers apply and answer 200, with the tick unblocked meanwhile. -2. ✅ **One resumable send.** One `Send` slot, a connection plus a source (a flash span, a file read stream, a tree cursor with its fragment, a borrowed preview frame with its WebSocket header) and an offset, drained by one loop with one budget, one stall guard and the lease. The four drains, their slot tables and `sendWsTextFrame` go; a patch is a one-fragment stream; a REST state answer is a cursor; every download takes a slot. Unit tests: a client that stops reading never holds a tick past the budget and is reaped by the stall guard; each source arrives whole. -3. ✅ **Firmware as a push writer.** `platform::otaWrite(handle, bytes)` in place of the pull callback makes a firmware upload an Upload leg; `UploadSource`, `uploadPull` and both firmware timeouts go, and with them the render freeze. Verified on a board without MoonBase. -4. ✅ **Sizes from what the device serves.** One fact, at most two browsers at once, sizes the rest: the embedded-file slots from what two browsers fetch in parallel; the control and preview caps stay at eight and four, since quick refreshes fill four with connections not yet reaped and refuse the page, for about 0.6 KB; the state fragment scales with the largest free block as the drain chunk does; `FileTransfer::path` lives in the slot's own buffer; the leaf baseline walks once. A document is never refused because downloads hold the slots, and a slow body meets a minimum rate per stall window rather than a byte every few seconds. -5. ✅ **The desktop's UI folder in the platform layer.** `serveFile` probes `src/ui/` before the embedded copy on every device; the desktop substitutes its disk files into the embedded table and core keeps one path. -6. ✅ **`/api/types` without building every module.** It constructs, defines and releases every registered type per request. Measured on a classic first; if it matters, a build-time table of defaults. -7. ✅ **A control client's partial frame is its own.** One carry buffer serves eight clients, so two clients split in the same tick lose a frame: a per-client carry, or a close with status 1002 on an unparseable read (RFC 6455 section 7.1.7) so the client reconnects clean. -8. ✅ **Split the file by concern**, one class over three translation units: the server (parse, route, responses, sends), the state wire (subscriptions, cursor, stream, diff, patch), and the WLED shim. Then measure on a classic (MM-Olimex), an S3 with a large show (the StadBeest legs), a P4 and the desktop: tick time with a page load and with a client that stops reading. -9. ✅ **The state push spread over frames, and told what changed.** Each second the push walks every value a client views on the render task, in one frame: measured on `MM-Olimex` (314 values), about 16 ms with one card viewed and 45 ms with everything, where a frame takes 2.6 ms, so one frame a second runs 6 to 17 times long. The cached hash is found by a linear scan, quadratic in the values. A control write the system already sees (the API, the UI, `setControl`) marks its module changed, as `notifyValuesChanged` does for the Control card; a status rides the walk, at most a second late, since `setStatus` may run on the encode task where the early-patch list has no lock. What modules change by themselves (telemetry, displays, a module writing its own variable) is walked by a cursor a slice per frame, as the state stream sends, matching the cache by position rather than by search. Measured on `MM-Olimex` with a client viewing everything: the slowest frame of the second rose from 196 to 251–285 fps. The StadBeest legs vary by ±30 fps between runs while their show plays, too much to tell before from after. - -## What it removes - -Four of the five drains with their slot tables, budgets and stall guards; `UploadSource`, `uploadPull`, `kFirmwareIdleMs`, `kFirmwareUploadHardMs`; `sendWsTextFrame`; the two iteration-count body waits; the once-a-second walk of every viewed value in one frame; the desktop path probe in core; per-module construction on `/api/types`; about 1.3 KB of control-client slots. It adds one send slot type and one drain. diff --git a/docs/work/Plan-20261024 - Video in, LED walls out (0 of 10).md b/docs/work/Plan-20261024 - Video in, LED walls out (0 of 10).md index 6e9bba95..22bbdc59 100644 --- a/docs/work/Plan-20261024 - Video in, LED walls out (0 of 10).md +++ b/docs/work/Plan-20261024 - Video in, LED walls out (0 of 10).md @@ -4,7 +4,7 @@ Plan of 2026-10-24, a Feature, for approval. **Recommended (2026-10-25):** appro ## What exists -- **The picture family** ([Streams plan](Plan-20261013%20-%20Streams%2C%20every%20frame%20from%20outside%20(11%20of%2013).md)): a source is a transport plus a decoder delivering RGB888 frames, and the Picture effect samples them per light. A live source paces itself and the newest frame wins. +- **The picture family** ([streams design study](../design/streams.md)): a source is a transport plus a decoder delivering RGB888 frames, and the Picture effect samples them per light. A live source paces itself and the newest frame wins. - **The Picture service** (`PictureService.h`) has two sources, an enum: `test pattern` and `usb capture`. A capture source runs its own task, which awaits a frame, decodes it into the service's buffer and publishes it. `adding-a-stream-source.md` names "a source per file behind a small registry" as the next step. - **The capture seam** (`platform.h`, `videoCapture*`) is "decode into RGB888 at most this size". Only the P4 implements it, for a UVC grabber's MJPEG through the hardware JPEG decoder. The desktop has a stand-in that synthesizes frames for the tests; `usbvideo_frames.h` holds the shared mode pick, JPEG header parse and area shrink. - **No JPEG decoder on the desktop**, and no YUV conversion anywhere. @@ -41,7 +41,7 @@ PipeWire is the Linux media graph: a camera, an application's video and a consum ### The browser, as a source and as an authoring tool -The split follows the Streams plan's [browser and device](Plan-20261013%20-%20Streams%2C%20every%20frame%20from%20outside%20(11%20of%2013).md#browser-and-device) rule: what runs with no browser open belongs on the device, what happens once with a person at the screen belongs in the browser. +The split follows the streams study's [browser and device](../design/streams.md#browser-and-device) rule: what runs with no browser open belongs on the device, what happens once with a person at the screen belongs in the browser. - **The browser as a picture source**, `browser`: a page draws a video file, a webcam or a shared screen (`getDisplayMedia`) onto a canvas at the picture's size and sends the RGB frames over the existing WebSocket into the service's pixels, as WLED streams a tab (DedeHai, PR #4997). A `