Skip to content

PanelCardDriver

Source: PanelCardDriver.h

PanelCardDriver

class PanelCardDriver
src/light/drivers/PanelCardDriver.h:106

Inherits: DriverBase

Output driver: streams the buffer to LED panel cards over raw Ethernet frames, below IP.

These cards take a sender-card feed rather than a pixel protocol — row-addressed data plus a sync frame that latches — so they need an L2 seam (platform::ethSendRaw) rather than a socket. The wire format lives in [ColorLight5A75Packet.h]; this driver owns the window, the correction and the chunking, exactly as NetworkSendDriver does for ArtNet/E1.31/DDP.

The board renders and sends.Effects, layers and MoonLive run here, so a device with this driver is a complete panel controller. Taking ArtNet in and forwarding it is one application of the same driver (add a NetworkReceiveEffect), not a prerequisite.

The vendor vocabulary, since the datasheets and LEDVision use it: an LED wall is driven by a sending card (a PC's PCI-E board, or a standalone box) that feeds receiving cards, one in each cabinet, which decode the signal and drive the panels. This driver puts the board in the sending card's place, and the cards it talks to are receiving cards. "Panel card" is the plainer name for the same hardware, and the one that keeps reading correctly when a second vendor's format lands.

No geometry controls. The panel arrangement belongs to the Layout — PanelsLayout states how many panels there are, their size, their wiring order and snaking, and maps every light to an (x, y). This driver reads the finished picture and cuts it into card rows, so a wall is described in exactly one place.

No IP involved. No address, no port, no DHCP lease: the frames carry their own destination MAC and EtherType. A board whose DHCP never completes still drives panels, which is also why the driver reports the link state itself rather than trusting ethConnected().

The deep dives are under More info, below the attribute/method lists: why these cards need a gigabit link, running this on a desktop or a Pi, other card vendors, and how to add one.

PanelCardDriver card

Public Attributes

uint8_t format = 0 : Wire format (index into kFormatOptions).

uint8_t firmware = 0 : Card firmware generation (index into kFirmwareOptions): 0 is v12-and-older, 1 is v13+.

uint8_t interfaceSel_ = 0 : Host NIC to send from: a Select over the DETECTED interfaces (platform::rawInterfaces), row 0 = capture-only.

char chosenIf_ = {} : The LABEL behind interfaceSel_, so a re-enumeration that reorders the list can restore the same NIC rather than whatever now sits at that index.

uint8_t lastResolvedSel_ = 0 : The row the last rebuild settled on.

uint8_t fps = 40 : Send-rate ceiling (Hz); [tick()] rate-limits so a fast render tick doesn't saturate the link.

Public Methods

inline PanelCardDriver() : Panel cards are RGB, so this references the "RGB" preset rather than the strips' "GRB" — same per-driver default the network sinks use.

virtual inline void defineDriverControls() override : This driver's controls, after the correction block DriverBase places at the top of every driver card: format, panel geometry, the host interface, the shared window, then the cap.

virtual inline bool affectsPrepare(const char * name) const override : Geometry and the window change how much of the buffer is corrected, so both re-run the prepare sweep; interface re-binds the raw socket, which also happens off the hot path.

virtual inline void release() override : Drop back to capture mode so a disabled driver holds no raw socket, then chain to the base.

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

virtual inline void prepare() override : Pure build: bind the raw interface (host only), size corrected_, and publish the status the card shows.

virtual inline void tick1s() override : Refresh the link status once a second: a cable plugged in after [prepare()] ran would otherwise leave the card reporting "no ethernet link" on a link that is up.

virtual inline void onCorrectionChanged() override : A preset toggle changes correction_.outChannels without a structural rebuild.

virtual inline void tick() override : Rate-limit, apply this driver's correction, then emit the window row by row and latch it with one sync frame.

inline void writeLinkStatus() : Report what the wire is doing: no link, a link too slow for the cards, or the packet rate reaching it.

inline const Buffer & correctedBuffer() const : Test-only accessor for the correction-applied buffer, pinning the no-allocation-in-loop contract (same public-for-tests convention as NetworkSendDriver::correctedBuffer).

Public Static Attributes

constexpr uint8_t kCardGain = 0xFF : The card's own gain, held at full.

constexpr const char * kFormatOptions = {"ColorLight 5A-75"} : Wire formats.

constexpr uint8_t kFormatCount = 1

constexpr const char * kFirmwareOptions = {"v12 and older", "v13 and newer"} : Card firmware generation.

constexpr uint8_t kFirmwareCount = 2

More info

The cards require a 1000 Mbps link, and the reason is wire time rather than bandwidth. Even a 256×256 panel at 40 fps is only ~65 Mbit/s of payload, which 100 Mbit would seem to carry. But the cards are dumb receivers with no buffering and no flow control: they latch on the sync frame, so an entire frame must arrive inside the inter-frame window. At 100 Mbit the same bytes take ten times as long on the wire — a 256×256 frame is ~16 ms of transmission against ~1.6 ms at gigabit — which overruns the frame budget and breaks the timing the sync depends on.

The failure mode is the confusing part: nothing errors. Frames go out, the link is up, and the panels tear, show wrong rows, or never latch. That is why this driver reads the NEGOTIATED speed (platform::ethLinkSpeedMbps) and says so in its status rather than letting a slow link look like a format bug. It still SENDS at 100 Mbit — a small panel may be fine, and the measurement is more useful than a refusal.

Running this on a host

The desktop build sends real frames too, via platform::ethBindRawInterface — so a Raspberry Pi, a Mac or a Windows PC running projectMM is a panel controller, which is the deployment this replaces. Linux uses AF_PACKET and macOS BPF, both needing root or CAP_NET_RAW; Windows has no kernel path for raw L2 at all and goes through Npcap, resolved at run time so the binary still builds and runs without it. Without the privilege or the driver, or with interface left blank, the host records frames instead of sending them, which is what lets the unit tests pin the wire format with no hardware and no privileges.

interface names the NIC: the kernel name on Linux and macOS (eth0, en0), and on Windows any distinctive part of the adapter description (Realtek), because a capture device there is spelled \Device\NPF_{GUID} and does not fit a control a human types into.

On ESP32 interface is ignored: the chip has one MAC.

Other card vendors

ColorLight is one of several receiver-card makers, which is why this driver is named for the category and carries a format selector rather than being a ColorLight driver:

vendor position in the market
NovaStar the global leader; large-scale displays and stage events
ColorLight cost-effective, strong outdoor and fine-pitch support (the format implemented here)
Linsn affordable and stable, common on budget installations
Mooncell full-color high-refresh niche
Huidu small and mid projects, storefront signage, mostly ASYNCHRONOUS
DBstar, Xixun also in the field

Contributions welcome. Adding one is a *Packet.h beside [ColorLight5A75Packet.h] plus an entry in kFormatOptions: the window, the correction, the chunking and the platform seam are already shared, and the desktop capture path lets the byte layout be pinned by unit tests with no hardware. What it actually costs is the research, not the code.

Two things to know before starting. Each vendor speaks its own proprietary L2 protocol, so a ColorLight frame will not drive a NovaStar card and the byte layout has to be obtained per vendor. And whether another format fits this driver's row-plus-sync model is unverified: the shape is an invitation, not a promise, and a format that addresses panels differently may need the driver to grow rather than just gain a packet file.

Huidu is the one to approach with care: its controllers are largely asynchronous, playing from onboard storage rather than being fed live, which is a different product category from a real-time sender.