Streams the light buffer over UDP in one of three industry protocols, selected by a control: Art-Net, E1.31 / sACN (the ANSI E1.31 streaming-ACN standard), or DDP (Distributed Display Protocol). Reads the Drivers container's buffer, applies the shared Correction (brightness / channel order / RGBW white) per light, chunks the corrected bytes per the selected protocol, and sends the whole frame as one burst at the configured rate. The single-node-multiple-protocols shape follows MoonLight's D_NetworkOut (architecture studied, not copied). Compatible with industry receivers — pixel controllers (Falcon, Advatek), xLights, LedFx, and ArtNet-controllable software.
protocol(select: ArtNet / E1.31 / DDP, default ArtNet) — the wire protocol; the destination port follows it automatically (6454 / 5568 / 4048). Changing it re-targets the socket live, no reboot (§ Live reconfiguration) — switch output protocol on a running device mid-show.ip(IPv4, default 255.255.255.255) — destination address. The default is the limited-broadcast address, so a fresh sender reaches every receiver on the LAN with no IP to type; set a unicast address to target one device. Changing it re-binds live. E1.31 multicast is deliberately not implemented (see Interop below).universe_start(uint16_t, default 0) — first universe for ArtNet and E1.31; DDP is byte-addressed and ignores it.light_count(uint16_t, default 0 = the whole buffer) — how many lights this sink sends, from the start of the buffer. >0 sends only the first N, so one sink covers just its slice — e.g. drive some lights over LEDs and the rest over ArtNet, or run two senders for different ranges — and each frame packs and sends exactly the lights this sink owns. The slice begins at light 0.fps(uint8_t, default 50, range 1-120) — frame rate limit. Without it the loop would re-send on every render tick; receivers expect a steady frame cadence.
| Protocol | Port | Chunk | Lights/packet (RGB) | Frame at 128×128 |
|---|---|---|---|---|
| ArtNet | 6454 | 510-channel universes | 170 | 97 packets |
| E1.31 | 5568 | 510-channel universes | 170 | 97 packets |
| DDP | 4048 | 1440-byte chunks, byte offset + push on last | 480 | 35 packets |
ArtNet and E1.31 split at 510 channels per universe — whole RGB lights, the xLights/Falcon convention; consecutive universes from universe_start. DDP is the fast path: per-packet cost dominates the wire time (~280 µs/packet Ethernet, ~1140 µs WiFi), so 480 lights per packet cuts a 128×128 WiFi frame from ~110 ms to ~40 ms.
E1.31 framing facts an integrator needs: CID is stable per device (derived from the MAC), source name projectMM, priority 100, one frame-level sequence stamped on every universe of a frame. The full byte layouts live in E131Packet.h, DdpPacket.h and ArtNetPacket.h — shared with the receiver so the two sides cannot drift.
- Universe rule (both ends): buffer offset = (universe −
universe_start) × 510, and the sender emits fromuniverse_startverbatim — no hidden 1-based adjustment for E1.31. Strict sACN gear reserves universe 0, so setuniverse_start ≥ 1on both ends when talking to it; the matching default of 0 on our own receiver keeps device↔device pairs aligned out of the box. - Unicast or broadcast; not multicast. The destination can be a single device (unicast) or the limited-broadcast address
255.255.255.255(the default), which sprays the frame to every device on the LAN — the platform socket setsSO_BROADCASTso this works for all three protocols. The standard Art-Net convention is broadcast; a device↔device pair works out of the box with no IP typed. E1.31 multicast (group 239.255.x.x) is not implemented — the platform has no IGMP join; MoonLight ships without it too. See the backlog entry for the planned work.
The whole frame goes out inline in loop() — ~35 ms over Ethernet / ~90 ms over WiFi at 128×128 with ArtNet (DDP proportionally less). The dedicated send task that decouples the wire from the render tick is a backlog item gated on PSRAM (backlog). FPS limiting plus the all-universes-in-one-burst shape is what receivers expect.
The driver is added as a child of the Drivers container at runtime — not boot-wired, but added per board through the catalog (POST /api/modules, the board's deviceModels.json modules entry), so a device only carries the outputs its board actually has. Once added it receives setSourceBuffer / setCorrection from Drivers::passBufferToDrivers (which wires every child generically, boot-added or runtime-added) and applies the shared const Correction* before every send — the same correction the RMT LED driver applies, so network and wired outputs show identical colours. The added child persists across reboot via FilesystemModule.
Unit tests: NetworkSendDriver — exact wire layouts for all three protocols (the byte offsets strict receivers validate), universe splitting, and the no-allocation-in-loop contract per protocol path.
Live tier: uv run scripts/scenario/run_network_live.py (MoonDeck.md § run_network_live) relays between real boards with the protocol control cycled round-robin.
MoonLight's D_NetworkOut (ArtNet/E1.31/DDP in one node) and the v1/v2 ArtNet senders; protocol specs: Art-Net 4 (Artistic Licence), ANSI E1.31-2016, DDP (3waylabs). Studied for the lessons, never copied.
