Skip to content

Source: platform.h

platform

The one interface every module reaches hardware through, so the same source drives an ESP32, a Teensy, a Raspberry Pi and a desktop.

Core and the light domain call these names and never a vendor SDK. A module that needs something this interface does not offer gets a new function here rather than a target check at the call site.

Classes

Name Description
UdpSocket One UDP socket: the wire every light protocol sends and receives on.
TcpConnection One TCP connection, non-blocking so a client never stalls the render loop.
TcpServer A listening TCP socket: what the web UI and the preview stream are served from.
TaskInfo One RTOS task, as [TasksModule] reports it.
WorkerTask An opaque handle to a spawned task, keeping FreeRTOS types out of the header.
GpioCapability What one GPIO is, so the pin ownership map can flag a claim landing on an unsafe pin.
GpioLiveState What one GPIO is doing now, the pin map's second axis beside gpioCapability's static view.
EncoderConfig What to encode; geometry and rate are the frame contract.
EncodedFrame One encoded frame as the encoder produced it, valid until the encoder writes the next.
ImprovDeviceInfo What Improv tells the client about this device; the task copies the strings, so pass static storage.
RmtWs2812Handle One configured RMT TX channel; the driver never inspects impl.
RmtLoopbackResult What a loopback self-test observed.
I80Ws2812Handle One configured i80 bus with its one or two DMA frame buffers.
MoonI80Ws2812Handle One configured MoonI80 bus, in whole-frame or ring mode.
MoonI80RingStats What the ring has been doing, for a read-only control; every field is 0 on a whole-frame handle.
ParlioWs2812Handle One configured Parlio TX unit with its one or two DMA frame buffers.
Hub75Pins The pins one HUB75 port needs; e serves 1/32-scan panels alone.
Hub75Handle One running HUB75 port.
AudioMicHandle One live audio input: an I2S channel on a board, an OS capture device on desktop.

UdpSocket

class UdpSocket
src/platform/platform.h:610

One UDP socket: the wire every light protocol sends and receives on.

Public Methods

UdpSocket() = default : An unopened socket.

~UdpSocket() : Close whatever is open.

UdpSocket(const UdpSocket &) = delete : Sockets are not copied; a file descriptor has one owner.

UdpSocket & operator=(const UdpSocket &) = delete : Sockets are not copy-assigned, for the same reason.

bool open() : Open the socket, answering whether it came up.

bool connect(const char * ip, uint16_t port) : Bind a fixed destination; false on a bad address.

bool sendTo(const uint8_t * data, size_t len) : Send to the connected destination.

bool bind(uint16_t port) : Listen on a port on any interface; false when it is taken.

int recvFrom(uint8_t * buf, size_t maxLen, uint8_t srcIp = nullptr) : Receive one datagram without blocking: bytes copied, or -1 when nothing is pending.

bool sendToAddr(const uint8_t ip, uint16_t port, const uint8_t * data, size_t len) : Send once to an explicit address.

bool joinMulticast(const char * group) : Join a multicast group on a bound socket; false is retried rather than fatal.

void close() : Close it, which the destructor also does.

TcpConnection

class TcpConnection
src/platform/platform.h:649

One TCP connection, non-blocking so a client never stalls the render loop.

Public Methods

TcpConnection() = default : An unconnected socket.

inline explicit TcpConnection(int fd) : Adopt an already-open descriptor, which is what accept hands over.

~TcpConnection() : Close whatever is open.

TcpConnection(const TcpConnection &) = delete : Connections are not copied; a file descriptor has one owner.

TcpConnection & operator=(const TcpConnection &) = delete : Connections are not copy-assigned, for the same reason.

inline TcpConnection(TcpConnection && other) noexcept : Move the descriptor, leaving the source empty.

inline TcpConnection & operator=(TcpConnection && other) noexcept : Close what this holds, then take the other's descriptor.

bool connectStart(const char * host, uint16_t port) : Start a connect and return at once; false is an immediate DNS or socket failure.

ConnectResult connectPoll() : Check that connect without blocking; call it after connectStart.

inline bool valid() const : Whether this holds an open socket.

int read(uint8_t * buf, size_t maxLen) : Read without blocking: bytes copied, 0 when the peer closed, -1 when nothing is pending.

bool peerIPv4(uint8_t out) const : The connected peer's IPv4 address, which a second channel back to it is addressed by.

bool write(const uint8_t * data, size_t len) : Write every byte, blocking until it is sent, which an HTTP response needs.

int writeSome(const uint8_t * data, size_t len) : Write what the socket accepts now: the count written, 0 when full, -1 on error.

void close() : Close it, which the destructor also does.

Public Types

enum ConnectResult : How an in-flight connect is going.

Value Description
Pending
Connected
Failed

TcpServer

class TcpServer
src/platform/platform.h:700

A listening TCP socket: what the web UI and the preview stream are served from.

Public Methods

TcpServer() = default : A server that is not yet listening.

~TcpServer() : Stop listening.

TcpServer(const TcpServer &) = delete : Servers are not copied; a listening socket has one owner.

TcpServer & operator=(const TcpServer &) = delete : Servers are not copy-assigned, for the same reason.

bool open(uint16_t port) : Listen on a port.

TcpConnection accept() : Take the next pending connection without blocking; an invalid one means none waited.

void close() : Stop listening.

TaskInfo

struct TaskInfo
src/platform/platform.h:127

One RTOS task, as [TasksModule] reports it.

Public Attributes

char name = {} : the task's own name

TaskState state = TaskState::Unknown : running, blocked, suspended

int8_t core = -1 : 0, 1, or -1 for no affinity

uint8_t priority = 0 : its RTOS priority

uint32_t stackFreeBytes = 0 : high-water mark: the least free stack seen

uint32_t cpuPermille = : 0..1000, or unmeasured when stats are off

WorkerTask

struct WorkerTask
src/platform/platform.h:146

An opaque handle to a spawned task, keeping FreeRTOS types out of the header.

Public Attributes

void * impl = nullptr

GpioCapability

struct GpioCapability
src/platform/platform.h:170

What one GPIO is, so the pin ownership map can flag a claim landing on an unsafe pin.

@moreinfo The SDK answers the first three fields; strap and reserved come from a per-chip table, being datasheet knowledge. Desktop reports everything valid and nothing reserved, a host build having no real pins to protect.

Public Attributes

bool validGpio = true : a real, usable GPIO on this chip

bool outputCapable = true : has an output driver (classic ESP32 34-39 are input-only)

bool rtc = false : an RTC pin, usable for deep-sleep wake and RTC I/O

bool strap = false : a boot-strapping pin: driving it at reset can change boot mode

bool reserved = false : wired to flash, PSRAM or native USB: routing I/O here corrupts the device

GpioLiveState

struct GpioLiveState
src/platform/platform.h:190

What one GPIO is doing now, the pin map's second axis beside gpioCapability's static view.

@moreinfo The pad reads on any pin, even one a peripheral drives. So a driver's output must toggle while it renders, and a mic clock while the mic runs.

Public Attributes

bool valid = false : pin is readable; false out of range, and on desktop, which omits the columns

bool level = false : current pad level, true being HIGH

bool output = false : the pad's output driver is enabled right now

bool input = false : the pad's input buffer is enabled right now; a pin can be both

uint8_t driveCap = 0 : output drive strength 0..3 = WEAK / MEDIUM / STRONG / STRONGEST

EncoderConfig

struct EncoderConfig
src/platform/platform.h:376

What to encode; geometry and rate are the frame contract.

Public Attributes

uint16_t width : frame width

uint16_t height : frame height

uint8_t fps : also the GOP, since a cut needs a keyframe and a long GOP lengthens every segment

uint16_t bitrateKbit : target bitrate

const char * encoderName : a desktop ffmpeg encoder; ignored where the platform has only one

const char * outDir : absolute directory for the playlist and segments

EncodedFrame

struct EncodedFrame
src/platform/platform.h:433

One encoded frame as the encoder produced it, valid until the encoder writes the next.

Public Attributes

const uint8_t * nal : the frame's NAL units, Annex B, start codes included

size_t len : bytes at nal

uint32_t pts90 : presentation time in the RTP clock's 90 kHz units

bool keyframe : an IDR, which a joining client decodes from

ImprovDeviceInfo

struct ImprovDeviceInfo
src/platform/platform.h:593

What Improv tells the client about this device; the task copies the strings, so pass static storage.

Public Attributes

const char * name : device hostname, such as "MM-3A7F"

const char * chipFamily : "ESP32", "ESP32-S3" and the rest

const char * firmwareVersion : the running version

RmtWs2812Handle

struct RmtWs2812Handle
src/platform/platform.h:729

One configured RMT TX channel; the driver never inspects impl.

Public Attributes

void * impl = nullptr

RmtLoopbackResult

struct RmtLoopbackResult
src/platform/platform.h:758

What a loopback self-test observed.

@moreinfo The verdict says why it failed rather than only that it did: an empty capture is a different fault from a full one that decodes wrong. Without those numbers both collapse into the same "bad bit 0 of 0" and isolate nothing.

Public Attributes

bool jumperDetected = false : the plain-GPIO continuity pre-check passed

bool pass = false : every captured bit matched what was sent

uint8_t sent = {} : the per-light test pattern transmitted

uint8_t got = {} : the light holding the first mismatch, light 0 when clean

uint32_t bitsChecked = 0 : WS2812 bits verified, 24 for the short test.

uint32_t firstBadBit = 0 : the first wrong bit, or bitsChecked when all pass

uint32_t capturedSymbols = 0 : symbols captured, which should reach bitsChecked

int8_t rxIdleLevel = -1 : RX level after the capture window, -1 when unknown.

uint32_t txWallUs = 0 : wall time of the first timed transmit

uint32_t txExpectUs = 0 : expected wire time; far below the wall time means a stalled transfer

I80Ws2812Handle

struct I80Ws2812Handle
src/platform/platform.h:780

One configured i80 bus with its one or two DMA frame buffers.

Public Attributes

void * impl = nullptr

MoonI80Ws2812Handle

struct MoonI80Ws2812Handle
src/platform/platform.h:820

One configured MoonI80 bus, in whole-frame or ring mode.

Public Attributes

void * impl = nullptr

MoonI80RingStats

struct MoonI80RingStats
src/platform/platform.h:878

What the ring has been doing, for a read-only control; every field is 0 on a whole-frame handle.

@moreinfo Best-effort volatile reads without a lock, so this is a diagnostic rather than a contract.

Public Attributes

bool isRing = false : whether this handle runs a ring

uint32_t nSlices = 0 : slices a frame takes, the light count over rowsPerBuf

uint32_t ringBufs = 0 : pool size; buffers are reused once nSlices passes it

uint32_t eofTotal = 0 : lifetime end-of-frame interrupts

uint32_t doneGiven = 0 : lifetime frame completions

uint32_t lastDrain = 0 : the drain count the last interrupt saw, which should reach nSlices

uint32_t numItems = 0 : descriptor pool capacity

uint32_t consumedItems = 0 : nodes the mount loop used, which equals numItems when sized right

uint32_t descErr = 0 : descriptor errors, where anything above 0 means the chain was corrupted

uint32_t maxEncodeUs = 0 : worst refill-encode time, the producer's jitter

uint32_t avgEncodeUs = 0 : average refill-encode time, which decides whether the ring keeps up

uint32_t maxIsrGapUs = 0 : worst gap between interrupts, which is the drain deadline

uint32_t late = 0 : slices refilled after their drain began, each one stale on the wire

uint32_t itemsPerBuf = 0 : descriptor nodes a ring buffer takes, 1 since the clamp

int32_t termNodeDiag = -1 : the mount-time terminator node, -1 on a lapping chain

uint32_t cacheOffDefers = 0 : interrupts that refilled nothing because the flash cache was off

uint32_t cacheOffMaxRun = 0 : worst run of those, which is how many buffers drained un-refilled

uint32_t stallAbandons = 0 : frames the wait backstop finalized after the DMA self-terminated

ParlioWs2812Handle

struct ParlioWs2812Handle
src/platform/platform.h:919

One configured Parlio TX unit with its one or two DMA frame buffers.

Public Attributes

void * impl = nullptr

Hub75Pins

struct Hub75Pins
src/platform/platform.h:958

The pins one HUB75 port needs; e serves 1/32-scan panels alone.

@moreinfo Every pin defaults to unset because a soldered line must never be guessed. A default would pick the user's wiring, and on an S3 could land on PSRAM or a strapping pin.

Public Attributes

uint16_t r1 = 0xFFFF

uint16_t g1 = 0xFFFF

uint16_t b1 = 0xFFFF : upper half-panel color

uint16_t r2 = 0xFFFF

uint16_t g2 = 0xFFFF

uint16_t b2 = 0xFFFF : lower half-panel color

uint16_t a = 0xFFFF

uint16_t b = 0xFFFF

uint16_t c = 0xFFFF : row address, 1/8 scan

uint16_t d = 0xFFFF : and 1/16 scan

uint16_t e = 0xFFFF : and 1/32 scan

uint16_t clk = 0xFFFF

uint16_t lat = 0xFFFF

uint16_t oe = 0xFFFF : shift clock, latch, blank

bool clkFalling = false : The panel's shift registers sample on the falling clock edge rather than the rising one. Some chips do, and driven on the wrong edge every pixel lands one column over.

Hub75Handle

struct Hub75Handle
src/platform/platform.h:970

One running HUB75 port.

Public Attributes

void * impl = nullptr

AudioMicHandle

struct AudioMicHandle
src/platform/platform.h:1014

One live audio input: an I2S channel on a board, an OS capture device on desktop.

Public Attributes

void * impl = nullptr

More info

Ethernet transmit can wedge

The driver's internal link state can diverge from both the PHY and our own event-driven flag. Observed on an S31 under sustained transmit, with the link genuinely lost, no disconnect event delivered, and nothing recovering short of a reboot. A stop and start re-runs link negotiation, which is the only supported way back, and it blocks for up to four seconds while autonegotiation polls the PHY to its timeout. The housekeeping tick calls it only where every frame is being refused anyway, so a stalled render loop for one tick costs nothing a user can see.

DMA cannot read PSRAM at the expander's clock

Measured fine at 2.67 MHz and never completing at 26.67 MHz, which is why a frame above the expander's cap is never materialized. The streaming ring loops a small pool of internal buffers instead, and the CPU encodes the next slice into each as it drains.

Internal RAM for what an interrupt reads

A PSRAM-resident encode source measured about 595 microseconds per slice refill against a 151 microsecond drain budget. So a buffer an interrupt reads per byte comes from allocInternal rather than the PSRAM-first alloc.