Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet.
Not just another Art-Net node — a network DMX node and a live diagnostic tool.
Watch all 512 channels update in real time in your browser, get warned the instant
two consoles fight over a universe, see per-sender FPS and frame jitter, and drive
galvanically-isolated DMX out. Builds for a few dollars.
⚡ Flash from your browser · ▶ Watch the demo · 🛠 Custom PCB · Install
| Status page | Settings page |
|---|---|
![]() |
![]() |
A guided tour of every control — manual channel control, labels, sparkline history, identify, blackout, and all settings:
▶ Watch the full walkthrough on YouTube · regenerate with docs/screenshot.mjs
| Feature | Details |
|---|---|
| Art-Net → DMX512 | Full 512-channel, unicast or broadcast, universe configurable (0–15) |
| sACN / E1.31 → DMX512 | Multicast receive, universe configurable, runs alongside Art-Net |
| Protocol selection | Art-Net only / sACN only / Both — configurable in web UI |
| Live Web UI | Bootstrap 5 dark theme, WebSocket push (~10/s), all 512 channels visible |
| Sender list | Shows all active Art-Net / sACN senders with per-sender FPS |
| Conflict detection | Warning banner when multiple senders are active simultaneously |
| Source merging | Per-output HTP (highest takes precedence) / LTP (latest wins) / off, honouring the sACN priority field |
| Jitter stat | Real-time inter-frame timing deviation (EMA) |
| Change log | Live log of DMX value changes with top-N changed channels per frame |
| Sparkline | Per-channel history sparkline in the channel detail modal |
| Channel labels | Name any channel (e.g. "Front Wash L") — shown in grid, modal, and change log |
| Identify | Flash a channel to full for ~1.5 s to physically locate the fixture |
| Manual DMX control | Click any channel in browser, set value via slider |
| Blackout button | Zero all channels instantly from browser |
| Art-Net / Manual toggle | Switch between protocol passthrough and manual override |
| Failsafe output | Continuous 40 Hz DMX refresh holds the last frame through brief input gaps |
| Static IP or DHCP | Configurable static IP/gateway/subnet/DNS, or automatic DHCP |
| Mesh-aware WiFi | Scans all channels and joins the strongest AP (multi-AP/mesh friendly) |
| WiFi Config Portal | First-boot AP + captive portal via WiFiManager |
| Selectable network mode | Pick the interface (WiFi or wired Ethernet) and WiFi mode (client/STA or standalone AP) in the web UI, on boards that have both |
| Standalone AP mode | LumiGate hosts its own WiFi network so a phone/tablet/console connects directly and sends Art-Net with no router (reachable at 192.168.4.1) |
| Ethernet → AP fallback | Auto-starts the WiFi AP if wired Ethernet is selected but the link is down, so the device stays reachable |
| Versioned OTA | Pick & install any past release from a table, or auto-update to latest |
| OTA Updates | ArduinoOTA (IDE/CLI) + manual .bin upload + one-click GitHub update |
| mDNS | Reachable as dmx-gateway.local (hostname configurable) |
| REST API | GET /dmx.json, /senders.json, /log.json, /version.json, /labels.json |
| Status LED | Plain GPIO, WS2812 RGB NeoPixel, or 5-LED panel (v3) — codes network/idle/DMX/conflict/identify state |
| Up to 2 DMX outputs | Two independent universes, each its own UART + RS485 transceiver (same universe on both = splitter) |
| Status display | Optional I²C OLED (SSD1306 / SH1106) or colour SPI OLED (SSD1351) — IP, universe, FPS, sources + auto-rotating conflict/identify/manual banners |
| Configurable DMX pins | Per output: universe, UART port, TX / RX / RTS GPIO — set at runtime via web UI, no recompile |
| NVS persistence | Universe, protocol, IP config, labels, hostname, OTA password, LED/DMX pin config survive reboots |
| Config reset | Hold BOOT button 3 s on startup, or via /reset page |
| Ethernet support | WT32-ETH01 (LAN8720) and v3 (W5500) run wired LAN or WiFi, switchable at runtime; plus any ESP32 / ESP32-S3 + an external W5500 module (enable & pin it in /config); DHCP or static |
| Dual/triple target | Builds for ESP32 (WROOM-32), ESP32-S3 (DevKitC-1), WT32-ETH01 |
Want the real thing instead of breadboard + modules? There's a complete open-source 4-layer PCB: ESP32-S3 with both WiFi and wired Ethernet (W5500), galvanically-isolated DMX output, USB-C power + flashing, status LEDs, BOOT/RST buttons, and an optional OLED/TFT display header — all palm-sized and fabricable at JLCPCB for a few dollars.
→ Full design, component rationale, BOM, gerbers & JLCPCB fab guide:
hardware/
The rest of this section covers the simpler breadboard / module build (an off-the-shelf ESP32 DevKit
- an isolated RS485 module) — perfect for getting started without fabricating a board.
| # | Component | Description | Link |
|---|---|---|---|
| 1 | ESP32 DevKit v1 | ESP32-WROOM-32, 30-pin, any CH340/CP2102 variant | Amazon.de search |
| 2 | Waveshare TTL to RS485 (C) | Galvanically isolated RS485 transceiver, auto-direction | Amazon.de – B0D4TZQYVG |
| 3 | XLR-5 female panel socket | Standard DMX output connector (XLR-3 also works) | Amazon.de search |
| 4 | Jumper wires / dupont cables | Male–male for breadboard, or direct solder | any |
| 5 | USB-A to Micro-USB cable | Power + serial flash | any |
| # | Component | Description | Link |
|---|---|---|---|
| 1 | WT32-ETH01 | ESP32 + LAN8720 Ethernet PHY, built-in RJ45 | Amazon.de search |
| 2 | Waveshare TTL to RS485 (C) | Same as WiFi build | Amazon.de – B0D4TZQYVG |
| 3 | XLR-5 female panel socket | Same as WiFi build | any |
| 4 | Ethernet cable | Cat5e or better | any |
| 5 | 5V power supply (500 mA+) | WT32-ETH01 has no USB port — use a 5V supply on the VIN pin | any |
The WT32-ETH01 has a built-in RJ45 jack and LAN8720 PHY soldered on-board. No additional Ethernet module needed.
Optional for enclosure:
- Project box / DIN-rail enclosure
- 120 Ω termination resistor across DMX A/B at the cable end (required for long runs)
This module is the key interface between the ESP32's UART and the DMX RS485 bus.
| Property | Value |
|---|---|
| Product name | Waveshare TTL to RS485 (C) |
| Isolation | Galvanic, 2500 V RMS (SP3485 + isolation transformer) |
| Direction control | Automatic — no DE/RE pin needed |
| TTL voltage | 3.3 V or 5 V (VCC selects level) |
| Max baudrate | ≥ 250 kbps (verified working at DMX rate) |
| Connector (TTL side) | 6-pin 2.54 mm header: VCC / GND / RXD / TXD / (unused) / (unused) |
| Connector (RS485 side) | Screw terminal: A / B / GND |
| Dimensions | ~38 × 14 mm |
Why galvanic isolation?
DMX fixtures are often powered from separate circuits or have ground loops. The isolated module prevents ground noise from corrupting the signal and protects the ESP32 from voltage spikes.
Why auto-direction?
Standard RS485 requires toggling a DE/RE pin to switch between transmit and receive. The Waveshare (C) variant handles this internally based on line activity — no GPIO needed, firmware is simpler. Trade-off: RDM (which requires half-duplex arbitration) is not supported by this module.
Waveshare TTL to RS485 (C) — TTL header (left side):
Pin 1 VCC → ESP32 3.3V
Pin 2 GND → ESP32 GND
Pin 3 RXD → ESP32 GPIO17 (default TX pin — configurable in web /config)
Pin 4 TXD → ESP32 GPIO16 (default RX pin — configurable in web /config)
Pin 5 — (not connected)
Pin 6 — (not connected)
Different hardware? If your RS485 module or ESP32 board uses different GPIO pins, change TX/RX under Settings → DMX Output Pins in the web UI. No recompile needed.
Waveshare TTL to RS485 (C) — RS485 screw terminal (right side):
A → DMX XLR pin 3 (Data+)
B → DMX XLR pin 2 (Data–)
GND→ DMX XLR pin 1 (Shield/Ground) [optional but recommended]
Connection table:
| ESP32 pin | → | Module pin | → | XLR pin |
|---|---|---|---|---|
| GPIO17 (TX) | → | RXD | — | — |
| GPIO16 (RX) | ← | TXD | — | — |
| 3.3V | → | VCC | — | — |
| GND | → | GND | — | — |
| — | — | A (RS485+) | → | Pin 3 |
| — | — | B (RS485–) | → | Pin 2 |
| GND | — | GND | → | Pin 1 (optional) |
| WT32-ETH01 pin | → | Module pin | → | XLR pin |
|---|---|---|---|---|
| GPIO4 (TX) | → | RXD | — | — |
| GPIO5 (RX) | ← | TXD | — | — |
| 3.3V | → | VCC | — | — |
| GND | → | GND | — | — |
| — | — | A (RS485+) | → | Pin 3 |
| — | — | B (RS485–) | → | Pin 2 |
Do not use GPIO16 or GPIO17 on the WT32-ETH01 — they are connected to the LAN8720 Ethernet PHY (power and RMII).
No soldering required if using a DevKit with pre-soldered headers. Place it on a breadboard or in your enclosure.
Using dupont jumper wires:
| Color convention | From | To |
|---|---|---|
| Red | ESP32 3.3V | Module VCC |
| Black | ESP32 GND | Module GND |
| Yellow | ESP32 GPIO17 (TX) | Module RXD |
| Green | ESP32 GPIO16 (RX) | Module TXD |
Use 3.3V, not 5V — the ESP32's GPIOs are not 5V tolerant.
The DMX standard uses XLR-5 (5-pin), but most DMX fixtures also accept XLR-3. Pin numbering is the same:
| XLR Pin | Signal | RS485 module |
|---|---|---|
| 1 | Shield / GND | Module GND (optional) |
| 2 | DMX Data– | Module B |
| 3 | DMX Data+ | Module A |
| 4 | (unused in DMX) | — |
| 5 | (unused in DMX) | — |
Screw the wires into the RS485 module's terminal block firmly.
For DMX cable runs over ~10 m, solder a 120 Ω resistor between pins 2 and 3 (A and B) at the far end of the DMX chain (i.e., inside the last fixture's XLR input). Most professional fixtures include this internally.
The ESP32 DevKit is powered via its Micro-USB port. Any 5V USB power supply works (500 mA is sufficient). The RS485 module draws its power from the ESP32's 3.3V rail.
- Do not connect DE/RE — the Waveshare (C) handles direction automatically. Connecting them will break transmission.
- Keep TTL wires short (< 20 cm). Long unshielded wires on the TTL side pick up noise; the RS485 side handles long distances natively.
- XLR gender: Use a female XLR panel socket for the DMX output. DMX sources (transmitters) use female XLR; receivers use male XLR. LumiGate is a source.
| Library | Purpose |
|---|---|
someweisguy/esp_dmx ^4.1 |
DMX512 transmit via UART |
rstephan/ArtnetWifi ^1.5 |
Art-Net UDP receiver (port 6454) |
tzapu/WiFiManager ^2.0 |
WiFi config portal |
ESP32Async/ESPAsyncWebServer |
Non-blocking HTTP server + WebSocket (port 80) |
ESP32Async/AsyncTCP |
Async TCP backend (runs networking off the main loop) |
adafruit/Adafruit NeoPixel ^1.12 |
WS2812 RGB status LED support |
ArduinoOTA |
OTA firmware updates |
ESPmDNS |
mDNS (dmx-gateway.local) |
Preferences |
NVS persistent config |
| (built-in UDP) | sACN / E1.31 multicast receive (port 5568) |
Open the web flasher in desktop Chrome or Edge, plug in your board over USB, pick your model, and click flash. That's it — no Python, no esptool, no toolchain. It always installs the latest release.
The manual / scripted methods below still work if you prefer them, or for the WT32-ETH01 (which has no USB port).
No toolchain needed. GitHub CI builds the firmware on every push to master.
Download latest release — includes firmware.bin, bootloader.bin, partitions.bin, boot_app0.bin.
You must manually enter download mode before flashing:
- Hold BOOT button
- Press and release EN (or RST) while keeping BOOT held
- Release BOOT — the chip is now in download mode
- Run the flash command
ESP32-S3 DevKitC-1: use the USB-UART port (labeled on the board), not the native USB port. The native USB port cannot be used with esptool.
Downloads and runs flash.ps1, which installs Python + esptool automatically:
Set-ExecutionPolicy -Scope Process Bypass; irm https://raw.githubusercontent.com/tombueng/LumiGate/master/flash.ps1 | iexOr save and run it manually:
# Download
Invoke-WebRequest https://raw.githubusercontent.com/tombueng/LumiGate/master/flash.ps1 -OutFile flash.ps1
# Run
Set-ExecutionPolicy -Scope Process Bypass
.\flash.ps1The script: installs Python 3 via winget if missing → installs esptool via pip → downloads the four firmware blobs from the latest GitHub release → lets you pick a COM port → guides you through boot mode → flashes.
pip install esptool
REPO=tombueng/LumiGate
for f in firmware.bin bootloader.bin partitions.bin boot_app0.bin; do
curl -sL "$(curl -s https://api.github.com/repos/$REPO/releases/tags/latest \
| python3 -c "import sys,json; assets=json.load(sys.stdin)['assets']; \
print(next(a['browser_download_url'] for a in assets if a['name']=='$f'))")" -o $f
done
# Enter boot mode first (see above), then:
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 \
--before default_reset --after hard_reset \
write_flash -z --flash_mode dio --flash_freq 80m \
0x1000 bootloader.bin 0x8000 partitions.bin \
0xe000 boot_app0.bin 0x10000 firmware.binImportant: bootloader goes at
0x0000on S3 (not0x1000like the original ESP32).
pip install esptool
REPO=tombueng/LumiGate
for f in firmware-esp32s3.bin bootloader-esp32s3.bin partitions-esp32s3.bin boot_app0.bin; do
curl -sL "$(curl -s https://api.github.com/repos/$REPO/releases/tags/latest \
| python3 -c "import sys,json; assets=json.load(sys.stdin)['assets']; \
print(next(a['browser_download_url'] for a in assets if a['name']=='$f'))")" -o $f
done
# Enter boot mode first, use the USB-UART port, then:
esptool.py --chip esp32s3 --port /dev/ttyUSB0 --baud 921600 \
--before default_reset --after hard_reset \
write_flash -z --flash_mode dio --flash_freq 80m \
0x0000 bootloader-esp32s3.bin 0x8000 partitions-esp32s3.bin \
0xe000 boot_app0.bin 0x10000 firmware-esp32s3.binReplace
/dev/ttyUSB0with your port (/dev/tty.usbserial-*on macOS).
pip install esptool
REPO=tombueng/LumiGate
for f in firmware-wt32eth01.bin bootloader-wt32eth01.bin partitions-wt32eth01.bin boot_app0.bin; do
curl -sL "$(curl -s https://api.github.com/repos/$REPO/releases/tags/latest \
| python3 -c "import sys,json; assets=json.load(sys.stdin)['assets']; \
print(next(a['browser_download_url'] for a in assets if a['name']=='$f'))")" -o $f
done
# Enter boot mode (hold BOOT + press RST), then:
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 \
--before default_reset --after hard_reset \
write_flash -z --flash_mode dio --flash_freq 80m \
0x1000 bootloader-wt32eth01.bin 0x8000 partitions-wt32eth01.bin \
0xe000 boot_app0.bin 0x10000 firmware-wt32eth01.bin- PlatformIO with VS Code extension
- ESP32 connected via USB (CH340 or CP2102)
LumiGate/
├── src/
│ ├── main.cpp ← firmware logic
│ ├── pages/ ← edit web UI here (plain HTML)
│ │ ├── index.html
│ │ ├── config.html
│ │ ├── config_saved.html
│ │ ├── reset.html
│ │ └── reset_done.html
│ ├── assets/ ← images served by the ESP32
│ │ └── logo.png ← 96×96 px, replaces itself on rebuild
│ └── generated/ ← auto-created at build time, gitignored
├── docs/ ← documentation assets (README images)
├── extra_scripts.py ← PlatformIO pre-build hook
└── platformio.ini
Before every pio run, PlatformIO executes extra_scripts.py, which:
- Reads every
src/pages/*.htmlfile - Reads every
src/assets/*.pngfile - Converts them to C
PROGMEMarrays / string literals and writes them tosrc/generated/*.h main.cpp#includes those headers — the HTML and images become part of the firmware binary
To change the web UI, edit the HTML files in src/pages/ and rebuild — no C++ changes needed.
To replace the logo, drop a new 96×96 PNG into src/assets/logo.png and rebuild.
Dynamic values (IP address, universe number, etc.) use {{PLACEHOLDER}} tokens in the HTML; main.cpp substitutes them at request time with String::replace().
pio run --target uploadIf upload fails ("Wrong boot mode"): Hold BOOT button, tap EN/RST, release BOOT — chip enters download mode. Then retry upload.
- From browser: open
http://dmx-gateway.local/config→ Firmware Update section → upload a.binfile or click "Update from GitHub"
| Downloading update | Back online |
|---|---|
![]() |
![]() |
- From IDE: uncomment in
platformio.ini:
upload_protocol = espota
upload_port = dmx-gateway.local
upload_flags = --auth=dmxotaThe OTA Password (Settings → Device) guards only this IDE / PlatformIO
espotapath. The browser "Firmware Update" above (.binupload + GitHub install) is not protected by it.
Out of the box, connect an Ethernet cable and power on — DHCP assigns an IP automatically. Open http://dmx-gateway.local or check your router for the assigned IP. WT32-ETH01 also has WiFi: you can switch it to WiFi client or a standalone access point in /config → Network (see Network mode below).
On first boot (or after WiFi reset), a board in WiFi client mode opens a WiFi access point:
- SSID:
DMX-Gateway(no password) - Connect with phone or PC → browser auto-opens portal (or go to
192.168.4.1) - Select your network, enter password, set Art-Net Universe (default:
0) - Click Save → LumiGate connects and reboots
Mesh / multi-AP WiFi: LumiGate scans all channels and joins the strongest AP for your SSID (and re-picks it on reconnect), so it won't latch onto a distant node. The ESP32 is 2.4 GHz only — make sure the AP nearest the device broadcasts 2.4 GHz. Check
rssion the status page: a healthy link is roughly −40 to −65 dBm. If it sits near −80 dBm right next to a router, the nearby node likely isn't offering 2.4 GHz — use a dedicated 2.4 GHz IoT SSID, or the WT32-ETH01 (Ethernet) build for a wired, rock-solid connection.
Choose how LumiGate connects in /config → Network. Changes apply after a reboot.
- Interface (boards with wired Ethernet — WT32-ETH01, v3, or any board with a W5500 module enabled under Wired Ethernet (W5500)): WiFi or wired Ethernet. WT32-ETH01/v3 default to Ethernet/DHCP; turn off "Use wired Ethernet" to run on WiFi. On a plain ESP32/ESP32-S3, first turn on "Use a W5500 Ethernet module" (off by default) and set its pins, then enable "Use wired Ethernet".
- WiFi mode:
- Client (STA) — join your existing 2.4 GHz network (the default; set credentials via the config portal above).
- Standalone AP — LumiGate hosts its own WiFi network, so a phone, tablet, or console joins it directly with no router required. SSID = the device hostname (
dmx-gatewayby default); set a password of 8+ characters for WPA2, or leave it empty for an open network. The device is reachable at192.168.4.1.
- Ethernet → AP fallback (dual boards): if wired Ethernet is selected but the link is down at boot, LumiGate automatically brings up the WiFi AP so it stays reachable.
Using AP mode with an iPad / console app (Luminair, Photon, etc.): set WiFi mode to Standalone AP and reboot, join the dmx-gateway network, then point the app's Art-Net output at 192.168.4.1 (or broadcast 192.168.4.255).
AP-mode caveats: the joined device has no internet while on LumiGate's AP; it is 2.4 GHz only with a small client limit; and Art-Net (unicast/broadcast) is more reliable than sACN multicast over a SoftAP, so prefer Art-Net in this mode.
Open http://dmx-gateway.local (or the IP shown in serial monitor at 115200 baud):
- Live stats: framerate, RSSI, uptime, free heap, jitter
- Conflict warning when sources clash on a universe, or a "merging" indicator when HTP/LTP merging is enabled for that output
- Active sender list with per-sender protocol and FPS
- All 512 DMX channels as a live grid — each cell shows the channel number large, its current value small, a value-proportional brightness, and a bottom-to-top gauge fill so levels read at a glance
- Hover any cell for an instant (no-delay) 2.5× zoomed preview of that cell (channel number, value, gauge and label)
- Click any cell → slider + sparkline history appear → set value directly
- Change log with recent DMX activity
To move LumiGate to a different WiFi network, clear its stored credentials — it will reopen the setup portal on next boot.
| Method | Steps |
|---|---|
| Web (easiest) | Open http://dmx-gateway.local/reset → click Reset WiFi → device reboots into AP mode |
| Hardware | Power off → hold BOOT → power on → keep holding for 3 seconds → release → device reboots into AP mode |
After reset, connect to the DMX-Gateway access point (no password) and follow the First Setup steps to join the new network.
The HTTP server and WebSocket are served by ESPAsyncWebServer (non-blocking), so the web UI never stalls DMX output. Pages are gzip-compressed and dynamic values are fetched as JSON.
| URL | Method | Function |
|---|---|---|
/ |
GET | Live status + 512-channel DMX grid (gzip) |
/config |
GET / POST | Change universe, protocol, per-output merge mode (off/HTP/LTP), static IP, hostname, OTA password, LED config, DMX pins (gzip) |
/reset |
GET / POST | Clear WiFi credentials, reboot to AP mode |
/info.json |
GET | Current settings + status (SSID, IP, universe, version, board/mcu id, etc.) |
/dmx.json |
GET | All 512 values, fps, rssi, uptime, heap, manual mode flag |
/senders.json |
GET | Active Art-Net / sACN senders (also pushed over the WebSocket) |
/log.json |
GET | Recent DMX change log entries (also pushed over the WebSocket) |
/labels.json |
GET | Channel labels object |
/labels |
POST | Store the full labels object (JSON body) |
/version.json |
GET | Current firmware version + update-available flag |
/autoupdate |
POST | Toggle auto-update (enabled=0/1) |
/ota/upload |
POST | Upload and flash a local firmware.bin |
/ota/github |
POST | Install a release from GitHub (version=latest or 1.0.N) |
Binary status/DMX frame pushed ~10×/s (528 + 2 bytes per output):
Bytes 0–1 fps × 10 uint16 big-endian (aggregate, all inputs)
Bytes 2–3 RSSI (dBm) int16 big-endian
Bytes 4–7 free heap uint32 big-endian
Bytes 8–11 uptime (s) uint32 big-endian
Byte 12 active sender count uint8
Byte 13 source status uint8 (0 = normal, 1 = conflict, 2 = merging)
Bytes 14–15 jitter × 10 (ms) uint16 big-endian
Bytes 16–527 DMX ch 1–512 uint8[512] (the viewed output)
Bytes 528… per-output fps × 10 uint16 big-endian × number of outputs
The web UI shows the viewed output's frame rate in the navbar and each output's own
rate on its selector button. GET /dmx.json also carries "outfps":[…] alongside "fps".
When more than one console targets the same universe, pick a per-output merge mode in /config:
- Off — last frame wins; a clash raises the conflict warning.
- HTP (highest takes precedence) — each channel is the maximum across the active sources.
- LTP (latest takes precedence) — the most recently seen source drives the output.
The sACN priority field is honoured first: a higher-priority source wins outright, and equal priority falls back to the mode above (Art-Net joins at the default priority of 100). A source that stops sending drops out of the mix after ~2.5 s. While an output is actively merging, the status page shows a green "merging" indicator instead of the conflict banner.
A JSON text frame with the sender list + change log is pushed every 2 s
({"meta":1,"senders":[…],"log":[…]}) so the browser never has to poll.
Browser → ESP32 (JSON text):
{ "type": "set", "ch": 1, "val": 200 }
{ "type": "mode", "manual": true }
{ "type": "blackout" }
{ "type": "identify", "ch": 5 }Channel labels are managed over REST (GET /labels.json, POST /labels).
LumiGate has a lot of GPIOs to set (status LED, 5-LED panel, OLED, two DMX outputs). To make this idiot-proof, Settings → Hardware board offers:
- Templates — pick your board and click Apply template to fill every LED / display / DMX pin with the tested map in one step. Selecting the LumiGate v3 board applies the exact pin map of the open-hardware PCB.
- Click pins on a board diagram — the pick button next to each GPIO field opens an interactive board so you click the actual pin instead of guessing GPIO numbers. The diagram colour-codes pins (free / caution / do-not-use) and shows your current assignments right on the board.
- Live validation — duplicate pins, strapping/flash/input-only pins and Ethernet-reserved pins are flagged in red/amber before you can save.
Five boards are built into the firmware and work fully offline, covering the common variants (which are not all the same pinout):
| Board | Notes |
|---|---|
| LumiGate v3 | our board (ESP32-S3 + W5500); preset generated from the PCB source |
| ESP32 DevKitC (WROOM-32, 38-pin) | breaks out the flash pins too |
| ESP32 DevKit v1 (DOIT, 30-pin) | narrower, no flash pins on the header |
| ESP32-S3 DevKitC-1 (44-pin) | GPIO33-37 only free on no-PSRAM modules |
| Seeed XIAO ESP32-S3 | tiny board, D0-D10 silk |
The full catalog covers 33 boards spanning every esp32 / esp32s3 board the firmware runs on (DevKit / DOIT / NodeMCU, WEMOS LOLIN, Adafruit Feather / QtPy / HUZZAH32 / Metro, Heltec OLED, Olimex PoE / Gateway / wESP32 Ethernet, Unexpected Maker, M5Stack, LilyGO, SparkFun ...). The boards beyond the five built-ins are pulled lazily from the online catalog (web/boards/, served via GitHub Pages) and cached in your browser; no network means you still get the built-ins plus manual entry. You can switch boards from a dropdown inside the pin-picker popup itself.
All descriptors are generated from authoritative pinout data
(hardware/gen_board_descriptor.py): hardware/lumigate.py for v3, published header
pinouts for the hand-tuned dev boards, and the Arduino core variants/<dir>/pins_arduino.h
for the rest, so the GPIO numbers and reserved/strapping/flash flags are accurate per
variant (e.g. the PICO-based Feather V2 frees GPIO6-11 and reserves 16/17 instead). The
picker draws its own horizontal diagram for every board; there are no board photos or
realistic graphics.
See docs/pin-picker.md for the design, the descriptor schema and how to add a board.
- Open Eingänge/Ausgänge tab
- Click in the output area right of Universe 1 → select ArtNet → interface = your PC's IP
- Configure: Output IP = LumiGate's IP, Art-Net Universe =
0 - Set transmission mode to Full (sends all 512 channels continuously at ~40 fps)
- Test with Einfaches Mischpult → move fader → grid lights up cyan
Universe mapping: QLC+ "Universe 1" = Art-Net Universe
0. LumiGate defaults to Universe0.
Tip: if "Standard" mode shows ~0.4 fps and values don't update, restart QLC+ or switch to "Full" mode.
Two QLC+ workspaces are included to test fixtures and show off the gateway — open with File → Open, map Universe 1 → ArtNet (above), switch to Operate mode, and press the Virtual Console buttons:
| File | Description |
|---|---|
docs/qlcplus-demo.qxw |
Small 6-channel demo: fade, blink, running light |
docs/qlcplus-show.qxw |
30-minute, 20-channel show — sine waves, comets, sparkle, VU bounce, crossfades, strobes, build-ups and more, sequenced into an intro→build→peak→groove→outro arc. Press GREAT SHOW. |
The big show is generated by docs/make_show.py — edit the arc list and re-run to customize.
LumiGate supports three LED types, configurable in the web UI:
| State | WS2812 RGB | Plain GPIO |
|---|---|---|
| Booting | white | on |
| Connecting to WiFi | blue blink | blink |
| Config portal / AP active | purple | on |
| No WiFi (lost) | red blink (fast) | off |
| Idle (connected, no DMX) | amber heartbeat (½ s on/off) | pulse |
| DMX active | solid green | solid |
The "DMX active" green is held through brief input gaps (continuous 40 Hz output), so momentary multicast loss doesn't flicker the LED. The LED runs on its own task, so serving the web UI never freezes it.
5-LED status panel (ledType 3, the LumiGate v3 board) — five discrete LEDs that show
independent states simultaneously, instead of one LED time-sharing colours:
| LED | Meaning |
|---|---|
| Red | no network (blink) |
| Green | network up (solid) |
| Yellow | DMX activity (fast blink while frames flow) |
| Blue | source conflict — two senders on one universe (slow blink) |
| White | identify active (blink) / booting |
Default GPIO: 2 (ESP32 DevKit on-board LED). ESP32-S3 DevKitC-1 uses GPIO 48 (built-in WS2812).
The v3 board uses R=1 G=2 Y=6 B=7 W=15.
LumiGate can drive an optional status display that shows the gateway's live state without a browser — IP, output universe(s), protocol, frame rate, source count and link / DMX activity — and auto-switches to a full-screen alert when something needs attention (two consoles fighting over the universe, identify, or manual override). It's off by default; enable and pin it under Settings → Display.
Rendered from the firmware's own layout code with the real display font · regenerate
with docs/make_display_preview.py. Top: 0.96″ /
1.3″ mono OLED (status, then the conflict / identify / manual banners). Middle: the same
panel in blue and yellow/blue-split, and the 0.91″ 128×32 compact layout. Bottom: the
1.5″ SSD1351 colour panel, which mirrors the RGB status-LED language (green = live,
amber = idle, red = conflict).
The status screen adapts to the number of outputs: a single output shows Uni 0 with one frame
rate, while two enabled outputs get a row each (A/B) with their own universe and own
FPS (shown above). The source count is labelled Sources (active Art-Net / sACN senders).
| Panel | Controller | Bus | Pins | Boards |
|---|---|---|---|---|
| 0.96″ / 1.3″ OLED 128×64 | SSD1306 / SH1106 | I²C | 2 | all |
| 0.91″ OLED 128×32 | SSD1306 | I²C | 2 | all |
| 1.5″ colour OLED 128×128 | SSD1351 | SPI | ~5 | ESP32 / ESP32-S3 (not the Ethernet boards) |
Mono panels are 1-bit — white, blue and yellow/blue-split versions all behave identically
(the colour is the physical emitter, not addressable). The I²C address is auto-detected
(0x3C / 0x3D). SPI colour panels need ~5 pins, so they only fit the non-Ethernet boards.
| Field | Notes |
|---|---|
| Display type | off / SSD1306 128×64 / SSD1306 128×32 / SH1106 128×64 / SSD1351 colour |
| SDA, SCL | I²C pins (mono panels). Avoid strapping pins like GPIO12; WT32-ETH01 → 14 / 15 |
| CS, DC, RST, SCK, MOSI | hardware-SPI pins (colour panel) |
| Orientation | normal / flipped 180° |
See docs/display.md for the full design — per-board pin tables and layout details.
LumiGate drives up to 2 independent DMX outputs — each its own universe, UART port and RS485 transceiver — configurable at runtime under Settings → DMX Outputs (no recompile). The 2-output ceiling is a hardware limit: the ESP32 / ESP32-S3 expose 3 UARTs and UART0 is the serial console, leaving UART1 + UART2. Output B ships disabled, so single-universe setups are unchanged, and devices updated from older firmware keep their existing output as Output A.
Per output:
| Setting | Default (Output A / B) | Description |
|---|---|---|
| Enabled | A on / B off | Whether this output drives a DMX line |
| Universe | 0 / 1 |
Art-Net universe (sACN universe = this + 1). Range 0–15 |
| UART port | 1 / 2 |
Which ESP32 UART drives the line. Each enabled output needs a distinct port |
| TX pin | 17 (GPIO17) |
ESP32 GPIO that drives the RS485 module's DI (data-in) |
| RX pin | 16 (GPIO16) |
ESP32 GPIO connected to the RS485 module's RO (data-out); needed for RDM |
| RTS / DE pin | −1 (disabled) |
RS485 direction-control pin. −1 for auto-direction modules (Waveshare C). Required for RDM. |
Setting both outputs to the same universe turns LumiGate into a 1-in / 2-out DMX splitter. RDM runs on the first enabled output with an RTS pin set (one output at a time). For multi-universe input, Art-Net is recommended (single UDP socket, no IGMP); sACN works too (one multicast group joined per universe).
WT32-ETH01: the Ethernet PHY consumes most GPIOs, so a second output is best run output-only (TX, no RX/RDM).
Applied on first boot; everything is overrideable in the web UI (no recompile).
| Board | Build env | Net | Outputs | Output A — TX / RX / RTS | Suggested Output B | Notes |
|---|---|---|---|---|---|---|
| ESP32 DevKit (WROOM-32) | esp32dev |
WiFi | 2 | GPIO17 / 16 / −1 | UART2, TX 32, RX 33 | Plenty of free GPIO; RDM possible on both |
| ESP32-S3 DevKitC-1 | esp32s3dev |
WiFi | 2 | GPIO17 / 16 / −1 | UART2, TX 18 (RX −1) | LED = WS2812 on GPIO48; v3 build disables the brownout detector (CONFIG_ESP_BROWNOUT_DET=n, a from-source build) to avoid an S3 boot-loop |
| WT32-ETH01 | wt32eth01 |
Ethernet | 2 | GPIO4 / 5 / −1 | UART2, TX-only | GPIO16 = LAN8720 PHY power, so pins are shifted; 2nd output best TX-only (no RX/RDM) |
| LumiGate v3 (ESP32-S3 + W5500) | lumigate_v3 |
Ethernet (W5500 SPI) | 2 | GPIO17 / 18 / 8 | UART2 | Open-hardware board (hardware/). 5-LED status panel; W5500 on SPI3 (CS=10/INT=14/RST=9); RTS/EN=8 for RDM direction |
Any ESP32 / ESP32-S3 build can add wired Ethernet with an external W5500 module — the W5500
driver is compiled into every build. In /config → Wired Ethernet (W5500) turn on
Use a W5500 Ethernet module (off by default), set its pins, then enable Use wired Ethernet.
Pins default to the classic-ESP32 VSPI set (CS=5 / SCK=18 / MOSI=23 / MISO=19 / INT=4 / RST=25) and
are fully configurable (with the on-board pin-picker); lower the SPI clock there if a long-wired
module isn't detected. No special build is needed.
All boards drive two outputs (UART1 + UART2). A 2nd output (UART2) used to panic on the ESP32-S3 — a latent esp_dmx 4.1.0 bug where the UART2 entry was guarded by an enum the preprocessor reads as 0, so it was compiled out (null peripheral pointer). That, plus an ESP-IDF 5.x fix (the removed
uart_periph_signal[].modulefield) needed once the firmware moved to arduino-esp32 v3, is applied by a build-time patch inextra_scripts.py.
ESP32-S3 — safe GPIOs for a 2nd output: free choices are 5, 6, 7, 8, 15, 18, 21. Avoid 26–37 (SPI flash / octal PSRAM — will crash), 19/20 (USB), 43/44 (serial console), 0/45/46 (strapping), 48 (WS2812 LED), and Output A's 16/17.
Using a different RS485 module? Wire your module's DI (data-in) to the TX GPIO and RO (data-out) to the RX GPIO, then set the pins under Settings → DMX Outputs.
- Existing single-universe devices are unaffected by the update. Migration maps the old single-output config to Output A and leaves Output B disabled, so exactly one driver is installed on UART1 — byte-for-byte the same path as before.
- No configuration can brick the device. A safe-boot guard persists a crash counter around DMX init; if init ever panics (a bad pin/port), the next boots progressively disable outputs (keep A → all off) until the web UI is reachable. An enabled output with no TX pin is sanitized off automatically (firmware + a client-side check).
| Key | Default | Change via |
|---|---|---|
| Art-Net Universe | 0 |
Web /config or portal |
| Protocol | Both (Art-Net + sACN) |
Web /config |
| Static IP / gateway / subnet / DNS | DHCP | Web /config (Network) |
| Auto-update | off | Web /config (Firmware) |
| Channel labels | — | Status page (channel modal) |
| Hostname | dmx-gateway |
Web /config |
OTA Password (IDE espota only) |
dmxota |
Web /config |
| LED type / GPIO pin | board default | Web /config (Status LED) |
| Per-output: enabled / universe / UART port / TX / RX / RTS | A on (uni 0, UART1, TX=17, RX=16, RTS=−1); B off | Web /config (DMX Outputs) |
| Display type / pins | off | Web /config (Display) |
| WiFi credentials | — | Config portal or /reset |
End-to-end Playwright tests drive a live device — they inject real Art-Net and sACN/E1.31
packets over the network and assert the REST API, WebSocket and web UI react correctly (Art-Net/sACN
→ DMX, sender tracking, conflict banner, change log, manual override + blackout, labels, and the
2-output feature). See docs/tests/README.md.
cd docs && npm install && npx playwright install chromium
LUMIGATE_HOST=dmx-gateway.local npm testMIT — do whatever you want, attribution appreciated.








