Skip to content

NetworkSendDriver

Source: NetworkSendDriver.h

NetworkSendDriver

class NetworkSendDriver
src/light/drivers/NetworkSendDriver.h:80

Inherits: DriverBase

Output driver: streams the buffer over UDP — one driver, three industry protocols selected by a control.

The single-node-multiple-protocols shape follows MoonLight's D_NetworkOut (architecture studied, not copied). Byte layouts live in [ArtNetPacket.h] / [E131Packet.h] / [DdpPacket.h], shared with the receiver so the two sides cannot drift.

Addressing: one driver feeds N receivers (ips), each taking a contiguous run of the window (lightsPerIp, the same broadcasting idiom as an LED driver's ledsPerPin) and each addressed only by itself. So a wall of tubes is one driver, not N — the driver-level twin of one LED driver fanning out to N GPIO lanes. Unicast is the default, not an option among equals; broadcast survives only as legacy compatibility.

Synchronous send: this driver's WINDOW (start/count, not necessarily the whole buffer) goes out inline in [tick()] — 38 ms Ethernet to 155 ms WiFi for a full-buffer window at 128×128 ArtNet (the spread is the lwIP/WiFi buffer pool, not the protocol: performance.md § ArtNet send), less for DDP and proportionally less for a narrower window. A decoupling send task is a PSRAM-gated backlog item. Added per board via the catalog like the LED drivers; applies the same shared Correction, so network and wired outputs show identical colors.

The deep dives are under More info, below the attribute/method lists: why unicast is the default, liveness, multicast and E1.31 framing.

NetworkSendDriver card

Public Attributes

char ips = {} : Receiver addresses, comma-separated (192.168.1.10,192.168.1.11).

char lightsPerIp = {} : Lights per destination — the same idiom as an LED driver's ledsPerPin, and literally the same helper (assignCounts): blank = even split of the window, one number = that many each, a list = one per destination by position.

uint8_t protocol = 0 : Wire protocol (index into kProtocolOptions).

uint16_t universeStart = 0 : First universe the slice maps onto (ArtNet / E1.31; DDP is byte-addressed).

uint8_t fps = 50 : Send-rate ceiling (Hz); [tick()] rate-limits to this so a fast render tick doesn't flood the LAN.

Public Methods

inline NetworkSendDriver() : DMX-over-network fixtures (ArtNet / E1.31 / DDP) are RGB by convention — the xLights/Falcon default — so a network sink references the "RGB" preset by default, unlike the WS2812 LED drivers that default to the strips' physical "GRB" order.

virtual inline void defineDriverControls() override : This driver's own controls, added AFTER the per-driver correction block the base places at the top of every driver card: protocol, destination IP, universe offset, the shared window (start/count), then the rate cap.

virtual inline bool affectsPrepare(const char * name) const override : A start/count change resizes the window this sink sends, and a Custom channel-count change grows outChannels; both route through the prepare sweep so resizeCorrected() re-sizes corrected_ — otherwise growing past the old buffer silently drops to passthrough.

virtual inline void setup() override : One-time wiring only: derive the stable E1.31 component id (CID) from the MAC once — no UUID machinery needed for a deterministic, unique-enough id.

virtual inline void release() override : Close the socket on release, then chain to the base to clear any status this driver set.

virtual inline void setSourceBuffer(Buffer * buf) override : Take the shared source buffer and (re)size the corrected_ buffer for it.

virtual inline void prepare() override : Pure build (see MoonModule::prepare): open the UDP socket (idempotent), re-resolve the destination, and resize corrected_ off the hot path ([tick()] never allocates).

virtual inline void onCorrectionChanged() override : A preset toggle (RGB↔RGBW) changes correction_.outChannels without a structural rebuild; rebuildCorrection() calls this hook so corrected_ tracks the new channel count.

virtual inline void tick() override : Rate-limit to fps, apply this driver's correction into corrected_ (passthrough if it emits no channels), then chunk the window slice into protocol packets and send inline.

inline const Buffer & correctedBuffer() const : Test-only accessor for the correction-applied buffer, letting the unit tests pin the no-allocation-in-loop contract (sized in prepare / onCorrectionChanged, never in the send loop).

inline uint8_t destinationCount() const : The destination table [prepare()] derived: how many receivers, each one's address, and how many lights of the window it owns.

inline const uint8_t * destinationAt(uint8_t i) const

inline nrOfLightsType lightsAt(uint8_t i) const

Public Static Attributes

constexpr const char * kProtocolOptions = {"ArtNet", "E1.31", "DDP", "E1.31 multicast"} : Protocol names, index-aligned with the constants used in [tick()]'s switch (0 = ArtNet, 1 = E1.31, 2 = DDP).

constexpr uint8_t kProtocolCount = 4

constexpr uint8_t kProtoE131Multicast = 3

constexpr uint8_t kMaxDestinations = 32 : The receivers.

Public Static Methods

static inline void e131MulticastAddr(uint16_t universe, uint8_t out) : sACN's own group for a universe: 239.255.

More info

Why unicast is the default

The Art-Net 4 spec leaves no room here: "ArtDmx packets must be unicast to subscribers of the specific universe contained in the ArtDmx packet… There are no conditions in which broadcast is allowed." Broadcast survives only as legacy compatibility (type a broadcast address and it still works, SO_BROADCAST is set) — it is never the default.

The reason is receive cost, and it is asymmetric. An Art-Net universe number lives in the PAYLOAD, not in any header a switch or NIC can act on, so a receiver can only discard a universe it does not own AFTER its stack has carried the packet all the way up and parsed it. Broadcast therefore makes every host on the segment pay the receive cost of every universe, including hosts with nothing to do with lighting. Artistic Licence (the protocol's authors) say it plainly: "Broadcast data floods the entire network and appears at every node whether it needs it or not. Too much broadcast data overloads switches and nodes alike" — and Art-Net II added node discovery precisely so a controller could switch to unicast, which they credit with a "massive" reduction in network loading. Two independent sources put the practical broadcast ceiling at ~15 universes (Quasar Science; WLED issue #3297). A 128x128 grid is ~97 universes x 50 fps ~= 4,850 pkt/s (~20 Mbit/s) — measured on the bench starving an ESP32's network stack until its HTTP stopped answering. On WiFi it is worse: broadcast goes out at the lowest basic rate, unACKed, to every station, waking them all from power-save (RFC 9119).

What the choice is NOT about. "Unicast means sending the same data N times" only bites when several nodes need the SAME universe (mirroring). When each node owns a DIFFERENT slice — the normal case, and what this driver models — unicast duplicates nothing: the sender emits exactly as many packets as a broadcast stream would, and each reaches only its owner. So per-node unicast costs the sender the same and costs every other receiver nothing. Mirroring is the one case where a broadcast address is genuinely the better tool.

Liveness, and what is not here

UDP is fire-and-forget, so a dead receiver is invisible to the sender: the send loop simply tolerates it (a failed send drops that packet and moves on, so one dark tube cannot stall the others) rather than pretending to detect it. Real detection is ArtPoll/ArtPollReply discovery — the spec's own mechanism, and the next increment (backlog).

"E1.31 multicast" sends to sACN's native per-universe group (239.255.{hi}.{lo}) instead of the configured host: opt-in, because a switch WITHOUT IGMP snooping floods it exactly like broadcast (and on WiFi it goes out at the lowest basic rate to every station), and firmware cannot detect which kind of switch it is on. Unicast stays the portable default. E1.31 framing: CID stable per device (from the MAC), source name projectMM, priority 100, one frame-level sequence per frame.