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¶
src/platform/platform.h:610One 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¶
src/platform/platform.h:649One 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¶
src/platform/platform.h:700A 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¶
src/platform/platform.h:127One 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¶
src/platform/platform.h:146An opaque handle to a spawned task, keeping FreeRTOS types out of the header.
Public Attributes¶
void * impl = nullptr
GpioCapability¶
src/platform/platform.h:170What 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¶
src/platform/platform.h:190What 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¶
src/platform/platform.h:376What 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¶
src/platform/platform.h:433One 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¶
src/platform/platform.h:593What 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¶
src/platform/platform.h:729One configured RMT TX channel; the driver never inspects impl.
Public Attributes¶
void * impl = nullptr
RmtLoopbackResult¶
src/platform/platform.h:758What 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¶
src/platform/platform.h:780One configured i80 bus with its one or two DMA frame buffers.
Public Attributes¶
void * impl = nullptr
MoonI80Ws2812Handle¶
src/platform/platform.h:820One configured MoonI80 bus, in whole-frame or ring mode.
Public Attributes¶
void * impl = nullptr
MoonI80RingStats¶
src/platform/platform.h:878What 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¶
src/platform/platform.h:919One configured Parlio TX unit with its one or two DMA frame buffers.
Public Attributes¶
void * impl = nullptr
Hub75Pins¶
src/platform/platform.h:958The 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¶
src/platform/platform.h:970One running HUB75 port.
Public Attributes¶
void * impl = nullptr
AudioMicHandle¶
src/platform/platform.h:1014One 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.