Firmware for a custom ESP32-S3 fixture-controller board (REV1) that turns Art-Net and serial DMX into real-world output: addressable LED strands, PWM/MOSFET loads, temperature-regulated fog, and thermostatic case fans. It exposes a web UI for live monitoring and configuration, and merges a wired DMX input with the Art-Net stream for redundancy.
- Art-Net receiver over wired Ethernet (WIZnet W5500), serving multiple universes.
- Serial DMX input with HTP/LTP merge against Art-Net, with configurable failover hold.
- Addressable pixels — up to 4 WS2811/WS2812 strands (NeoPixelBus), each with optional per-pixel over-power (thermal) protection: every physical pixel has its own leaky-bucket time-above-threshold limit, and a trip blacks the strand out until the accumulated heat has drained. A live per-pixel heat strip in the web UI visualises how close each pixel is to tripping. See docs/temp-protection-tuning.md for a measuring guide to determine the right settings for your strip.
- PWM / MOSFET outputs — DMX-driven low-side FET channels for generic loads.
- Fog module — PID-regulated heater (MAX6675 K-type thermocouple) plus pump, with ready-temperature gating and overheat fault latch. See docs/fog-pid-tuning.md for a PID tuning guide.
- Thermals — two thermostatic case fans driven by a temperature curve, plus SoC and case (LM75B) temperature reporting.
- Web UI — single-file Alpine SPA for live status and full configuration, with hot-apply for safe changes and reboot-on-demand for the rest.
- Networking — wired Ethernet + optional WiFi STA, mDNS, and an automatic setup soft-AP (captive portal) fallback when no link comes up.
- MCU: ESP32-S3-WROOM-1U-N8 (8 MB flash, no PSRAM)
- Ethernet: WIZnet W5500 over SPI (Art-Net transport)
- Thermocouple: MAX6675 (16-bit, K-type) on a read-only SPI bus — fog temp
- Case temp: LM75B over I²C
- Outputs: 6 low-side MOSFETs (fog heater, fog pump, 2× PWM, 2× fan), 4 addressable-pixel data lines, RS-485 DMX (half-duplex)
The full, schematic-verified pin map lives in src/pins.h — that
file is the ground truth for REV1 wiring.
src/ firmware (controller app + bring-up test)
main.cpp controller entry point (env:controller)
test_main.cpp interactive peripheral bring-up (env:peripheral-test)
pins.h REV1 pin map (schematic-verified)
net.* Ethernet / WiFi / AP / mDNS
dmx_*.* dmx_merge.* Art-Net + serial DMX stores and HTP/LTP merge
pixels.* pixel_* addressable strands, mapping, power protection
pwm.* fet_alloc.* FET/PWM outputs and role allocation
fog.* fog_control.* PID fog heater + pump state machine
thermals.* fan curves + temperature sensing
config_*.* config model, JSON codec, persistence, live hot-apply
reporting.* async HTTP server + JSON API
web/ Alpine SPA source (bundled into src/web_ui.h at build time)
data/ LittleFS image: config.json
tools/ build_web.py (UI bundler), mock_api.py (UI dev server)
test/ native Unity unit tests for platform-agnostic logic
docs/ design specs and implementation plans
This is a PlatformIO project. Three environments are
defined in platformio.ini:
| Env | Purpose |
|---|---|
controller |
The fixture controller — the default app (src/main.cpp) |
peripheral-test |
Interactive board bring-up (src/test_main.cpp) |
native |
Host unit tests for the platform-agnostic logic |
pio run -e controller -t upload # build + flash firmware
pio device monitor # serial console @ 115200The UI is bundled into src/web_ui.h (a gzipped PROGMEM byte array) and served
straight from flash — no LittleFS dependency. tools/build_web.py runs as a
PlatformIO pre-build hook, so editing anything in web/ and rebuilding the
firmware (pio run -e controller) regenerates the bundle automatically; there is
no separate UI upload step.
For UI development without a board, tools/mock_api.py serves the UI against a
mocked API.
Device config lives on the LittleFS image as data/config.json:
pio run -e controller -t uploadfs # flash the LittleFS image
⚠️ uploadfsoverwritesdata/config.jsonon the device.config.jsonis git-ignored (device-specific), so keep a copy of any live config you care about.
pio test -e native # run host unit testsDevice config is a single JSON document (see data/config.json
for the schema by example) covering device identity, network, pixel strands, PWM
channels, the fog module, fans, and DMX-merge settings.
Edit it from the web UI, or via the HTTP API:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/status |
Live telemetry (net, thermals, FETs, fog, channels, pixels) |
GET |
/api/pixelHeat |
Per-pixel over-power "heat" (0–255) per strand, for the UI heat strip |
GET |
/api/config |
Current config (WiFi password masked) |
POST |
/api/config |
Replace config (Content-Type: application/json); returns rebootRequired |
POST |
/api/reboot |
Restart the device |
POST |
/api/fog/reset |
Clear the fog fault latch |
Safe config changes hot-apply on the next task cycle; structural changes report
rebootRequired: true and take effect after a reboot.
On boot the controller brings up wired Ethernet and, if configured, WiFi STA.
If no usable link appears within 30 s, it starts a setup soft-AP with a captive
portal so you can connect and configure WiFi/Ethernet over the web UI. mDNS is
advertised under the configured hostname (e.g. artnet-fog.local).