
Luxdmx carrier
tombueng/LuxDMX
Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet. Live web UI shows all 512 channels in real time, flags console conflicts, and drives galvanically-isolated DMX.

Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet. Live web UI shows all 512 channels in real time, flags console conflicts, and drives galvanically-isolated DMX. Flash from your browser, no toolchain.
Luxdmx is an open-source printed circuit board by tombueng, from the tombueng/LuxDMX repository on GitHub. It is a 4-layer board of 119 × 79 mm with 97 parts on 86 nets, routed with 795 tracks and 173 vias.
It is built around the DP9900M-5V, ESP32-S3-WROOM-1-N8R2 and B0505S-1W, with the SY8089A, USBLC6-2SC6 and W5500 among its other parts. Its parts include 29 capacitors, 23 resistors, 9 diodes, 8 ICs and 8 connectors. It belongs with the interfaces and adapters and home automation and IoT boards and the ESP32 boards of this library.
[!NOTE] v6 has not been fabricated yet. The board is on
master, design-complete, and passes the full scripted validation (DRC, isolation, SPICE, Ethernet skew, header parity; see VALIDATION.md). An earlier spin of this design was built and came up working (rails, galvanic isolation, PoE standalone, both DMX universes bit-exact, RDM 64/64 on both lines), but v6 carries changes that have not been on physical copper yet: native USB instead of a CH340 bridge, TPS2116 priority mode, a 3225 crystal package, and the J4/J6 header re-pin. Those are what the first v6 article has to prove. Treat it as a prototype: the firmware on a plain ESP32 + an isolated RS-485 module is still the longest-proven path (see the main README). The ruggedization pass (USB ESD, PTC fuse, +5V TVS, a TPS2116 priority power mux, DMX common-mode chokes, ferrite supply filters), plated GND mounting holes, wider power traces, and the DMX512-A "Protected" series-TBU front end are all in.
A compact, open-source Art-Net / sACN → galvanically-isolated DMX512 gateway, built around an ESP32-S3 with both WiFi and wired Ethernet. Designed entirely as code (SKiDL netlist) and routed by a fully-scripted, placement-driven pipeline — so the board regenerates itself from your component placement, isolation barrier and all.

Lighting consoles and media servers speak Art-Net or sACN (E1.31) over a network. Fixtures — dimmers, moving heads, hazers, LED bars — speak DMX512 over an isolated RS-485 bus. LuxDMX sits between them: it receives Art-Net/sACN over WiFi or Ethernet and emits a clean, opto-isolated DMX512 signal. It's the box that lets you drive a rack of conventional stage gear from QLab, a grandMA, or any Art-Net source — without dragging a laptop and a USB-DMX dongle around backstage.
The whole device runs on a single ESP32-S3, fits on a board you can hold in your palm, and costs a few dollars to fabricate at JLCPCB.
| Decision | Why |
|---|---|
| ESP32-S3 as the MCU | Plenty of GPIO, fast dual-core, native USB, mature Arduino/IDF support, and cheap. The N8R2 brings 8 MB flash + 2 MB PSRAM, and because that PSRAM is quad, it leaves GPIO35–37 free for the expansion header. Only the octal-PSRAM parts (R8 and up) wire GPIO33–37 to the memory and lose them. |
| W5500 SPI Ethernet (not the ESP's RMII) | The S3 has no built-in Ethernet MAC (unlike the original ESP32), so RMII/LAN8720 is physically impossible. The W5500 is a self-contained TCP/IP-offload Ethernet controller on SPI — the only practical wired-Ethernet path for the S3. Wired Ethernet matters: a packed venue's 2.4 GHz band is hostile, and a show can't drop frames. |
| Galvanically-isolated DMX | DMX runs long cables between gear sitting on different mains circuits with different ground potentials. Without isolation you get ground loops (noise, flicker) and, worse, fault currents that can destroy the gateway when something downstream shorts. Pro DMX gear is always isolated. We isolate both the data (ISO3086) and the power feeding it (B0505S DC-DC), so the DMX domain shares no copper with the logic side. |
| USB-C | Reversible, modern, and carries both power and native flashing — one cable to power and program. |
| 4-layer PCB | A solid inner ground plane gives the W5500's Ethernet pairs a 100 Ω reference, tames EMI, and makes the dense routing actually fit. (More below.) |
Each universe is a self-contained galvanic island; the two share no copper with each other or the logic side.
E1.11_COMPLIANCE.md.VISO/GNDISO/DMX_A/DMX_B; universe 2 = VISO2/GNDISO2/DMX2_A/DMX2_B.VPOE+/VPOE- →
U7 — SDAPO DP9900M-5V (C5380106), an isolated PD + DC-DC module (36–57 V in, regulated 5 V out,
1.5 kV isolation, class 0 ≈ 13 W — far more than this board's ~2–3 W). D10 — SMAJ58A (C110521) clamps
surges on the rectified rail. The module's -VDC becomes board GND; its +VDC is the PoE 5 V source.+5V rail from either the PoE 5 V
(+5V_POE, on VIN1) or the USB-C 5 V (+5V_USB, through the F1 PTC, on VIN2), with no backfeed. It runs
in priority mode: MODE is tied to VIN1, so PoE wins whenever it is present and the board falls back
to USB only when PoE goes away. The PR1 divider (R24 = 30 kΩ / R25 = 10 kΩ) sets the VIN1 switchover
threshold to Vsw = 4.0 V (VREF 1.0 V × (30 + 10) / 10). Vsw is deliberately high, because VOUT tracks VIN1
down until the switch actually fires: at 4.0 V the +5V rail stays far above the ESP's ~2.8 V hang threshold
while PoE collapses. The pass-FET drop is only ~30 mV (vs ~0.4 V for a Schottky), so +5V stays ≈ 4.9 V on
USB and the B0505S / ISO3086 VCC2 rail clears its 4.5 V minimum across the whole USB range. Input caps
C30 / C31 (1 µF), bulk C29 (22 µF).
(The first article tied MODE/PR1 to GND, which is the default "higher voltage wins" mode. With two
near-equal 5 V sources that hunts, and a PoE→USB handover hung the board. Priority mode is that fix.)USB_DM → IO19, USB_DP → IO20). The S3's USB-Serial-JTAG peripheral sits in ROM, so a bare USB-C cable
gives you the flashing port, the serial console and JTAG debug with no bridge chip. esptool resets
the chip over USB by itself, so there is no auto-reset circuit either.S3_TX / S3_RX), since nothing spends it on a bridge.
(The first article carried a CH340C (U3) plus a Q1/Q2 2-transistor auto-reset. Its collectors were
swapped, so EN and IO0 sat on the wrong transistors: flashing needed the manual BOOT-button dance, and a tool
asserting DTR or RTS alone could yank EN or IO0. Rather than re-wire it, the bridge and the reset circuit are
gone entirely, 6 parts fewer and 2 fewer part numbers to source.)B3U-1000P tact switches (C231329). BOOT doubles as the config-portal /
factory-reset button in firmware; RST is a hard reset.J4 and J6 are the same connector, so a cable fits either one. Their power pins are therefore
identical, and a validator (scripts/validate_header_parity.py, hard gate) keeps them that way:
| pin | J4 — display | J6 — expansion |
|---|---|---|
| 1 | +3V3 | +3V3 |
| 2 | GND | GND |
| 3 | SDA (IO4) | IO35 |
| 4 | SCL (IO5) | IO36 |
| 5 | SCK (IO39) | IO37 |
| 6 | MOSI (IO40) | IO48 |
| 7 | CS (IO41) | TX0 (IO43) |
| 8 | DC (IO42) | RX0 (IO44) |
| 9 | RST (IO38) | GND |
Plug a cable into the wrong header and nothing dies: the module still gets correct power, and only 3V3 CMOS signals land in the wrong places. A mis-plugged display sees GND on its RST and just sits quietly in reset.
There is no +5V on J6 — feed 5 V loads from your own supply. Dropping it is what lets both headers share one power layout, and there was no free non-strapping GPIO left for a 7th signal anyway, so pin 9 became a second ground. See docs/display.md → Header safety.
Stack-up: 4-layer, JLCPCB JLC04161H-7628 (1.6 mm; L1–L2 prepreg 0.2104 mm, Er 4.05 — the dielectric the Ethernet pairs are impedance-tuned to). The two inner layers are ground-filled signal layers — not classic "power planes". That's deliberate: as signal layers the autorouter will happily route ground (and fine-pitch escapes) to the inner copper, while the fill still gives every trace a solid 0.21 mm-away reference. The result: clean ~100 Ω Ethernet pairs and good EMI.
Isolation: three barriers, all enforced by net-based rules in luxdmx.kicad_dru:
GNDISO/VISO/DMX_A/DMX_B) and universe 2 (GNDISO2/VISO2/DMX2_A/DMX2_B)
are each held ≥ 4 mm from all other copper. Their barrier parts — PS1/PS2 (B0505S, 2.5 mm pitch)
and U5/U6 (ISO3086) — are courtyard-exempted (isolation internal/rated), like the USB-C pads.VPOE+/VPOE-, the magjack→module rectified rail) is held ≥ 2.5 mm from everything else;
the 1.5 kV PD isolation itself is internal to U7 (DP9900M) and J3 (magjack), so their courtyards
are exempt. The board's only galvanic tie to the PoE line is through the module + magjack magnetics.The inner ground planes are carved (keepout rule-areas) around every isolated/hot region so no inner
copper crosses any barrier — scripts/rebuild_iso.py regenerates this for all three domains from
the live part positions.
[!CAUTION] Do not run the pipeline steps casually on the committed board. Everything in the list below that touches the
.kicad_pcb(sync_board,rebuild_iso,escape_connectors,autoroute_fr2,cleanup_pads,normalize_silk,widen_eth,tighten_poe_void,finish_partial,route_tbu,build_v3) rewrites it. The board as committed carries hand-made placement and silk edits that those steps would wipe out. They are the tools for a deliberate re-route from a new placement, not a "refresh". Re-running them by reflex is how you lose a day.Always safe (read the board, never write it):
scripts/luxdmx.py(writes only the netlist),gen_schematic/gen_bom_from_board/gen_cpl/gen_gerbers(write only outputs), and everyvalidate_*gate.
The board is generated, not hand-drawn. scripts/luxdmx.py is a SKiDL
netlist — the single source of truth for every part and connection. From a placement, the rest is scripted
and adapts to wherever you put the parts:
0. python scripts/luxdmx.py # SKiDL → luxdmx.net (only after editing the netlist source)
0b python scripts/sync_board.py # add NEW/changed parts to the board, KEEPING existing placement,
# gridded in the enlarged area (scripts/build_v3.py = full from-scratch grid)
1. place / move parts in KiCad → save
2. python scripts/rebuild_iso.py # regenerates inner GND planes + the two GNDISO pours + the three
# isolation keepouts (DMX1/DMX2/PoE) from LIVE positions — no hardcoded coords
3. python scripts/escape_connectors.py # escapes the few fine-pitch connector pins to LOCKED vias
4. python scripts/autoroute_fr2.py # Freerouting 2.2.4 routes the whole board, keeping locked escapes
5. python scripts/cleanup_pads.py # mounting posts → NPTH, widen tight THT annular ringsRun everything with the KiCad 10 bundled Python (it ships pcbnew). scripts/autoroute_fr2.py drives
Freerouting 2.2.4, which needs Java 25+; the jar and a portable JDK live in tools/ (git-ignored).
Locked tracks survive every re-route (KiCad exports them as DSN (type fix)), so connector escapes only
get done once. Move a part, re-run — done.
Adding the dual-universe + PoE parts to an existing board:
scripts/sync_board.pyis the incremental path — it keeps every already-placed footprint where it is, refreshes pad nets (e.g. J2's VBUS moving onto+5V_USB), swaps J3 to the PoE magjack, and drops the brand-new parts (U6/PS2/J5/D7, U7/D10, the new caps) in a grid on the right of the enlarged outline for you to place. Then run the normal 2→5 pipeline.scripts/build_v3.pyremains the full from-scratch grid build if you'd rather re-place everything.
Everything you upload is already generated in this folder.
luxdmx_gerbers.zip.luxdmx_BOM_jlcpcb.csvluxdmx_CPL.csvscripts/gen_cpl.py, so parts
should sit correctly. The ESP32-S3's antenna intentionally overhangs the left edge.The XLRs (J1, J5), USB-C (J2), PoE MagJack (J3), B0505S modules (PS1, PS2) and the DP9900M PoE module (U7) are through-hole / module parts. Either add JLCPCB's through-hole assembly or hand-solder them — they're all large, easy joints.
4-layer, this size, 5 pcs ≈ a few dollars for the bare boards; SMT assembly adds the parts + a setup fee. A complete, assembled prototype lands well under typical hobby budgets.
| File | What |
|---|---|
scripts/luxdmx.py | SKiDL source — authoritative netlist (python scripts/luxdmx.py → luxdmx.net) |
luxdmx.kicad_pcb / .kicad_pro | the board + KiCad project |
luxdmx.kicad_dru | custom design rules — the two 4 mm DMX isolations, the 2.5 mm PoE isolation + exemptions |
scripts/sync_board.py | incremental netlist→board: keep existing placement, grid the new parts (dual-universe + PoE) |
scripts/build_v3.py | full from-scratch grid build (clears + re-drops every part) |
scripts/rebuild_iso.py · scripts/escape_connectors.py · scripts/autoroute_fr2.py · scripts/cleanup_pads.py | the routing pipeline |
route.py · autoroute.py | older Freerouting-1.9 fallbacks (no Java 25 needed) |
scripts/gen_bom_from_board.py → luxdmx_BOM_jlcpcb.csv | JLCPCB assembly BOM |
scripts/gen_cpl.py → luxdmx_CPL.csv / .xlsx | JLCPCB pick-and-place (corrected) |
luxdmx_gerbers.zip | 4-layer gerbers + PTH/NPTH drill — the fab upload |
easyeda/ | LCSC/easyeda footprints + 3D models for the specific parts |
tools/ | Freerouting 2.2.4 jar + portable JDK 25 (git-ignored — see Toolchain) |
board-pcb-1.png · board3d-1.png · board3d-2.png | layout + 3D renders |
Design-complete, fully validated, and the first spin is being fabricated. Not yet bench-tested on real hardware, so treat this first spin as a prototype.
The board (two isolated DMX universes + PoE, 119 × 79 mm, 4 corner plated M3 mounting holes) is fully routed (0 unrouted, 0 unconnected) and passes the scripted validation:
luxdmx.kicad_dru).See VALIDATION.md for the full matrix. Open items to confirm before fab (none electrical-blocking):
Both board-specific firmware features ship in the released esp32s3dev build (the board has no
dedicated env — it is an ESP32-S3 + W5500 with a fixed pin map; flash that build and pick the
LuxDMX v6 board template in /config). See platformio.ini. Built on
arduino-esp32 v3 / ESP-IDF 5.5:
ETH.begin(ETH_PHY_W5500, …) registers the W5500 as an lwIP
netif, so the existing AsyncWebServer / Art-Net / sACN / OTA stack runs over wired Ethernet
unchanged — on SCLK=IO12, MOSI=IO11, MISO=IO13, CS=IO10, INT=IO14, RST=IO9 (SPI3 host),
selected by the USE_ETH_SPI build flag. This requires arduino-esp32 v3 (v2.x has no
ETH_PHY_W5500), which is why the whole firmware moved to the v3 framework via the
pioarduino platform.ledType=3 model drives all five LEDs simultaneously —
R=IO1 no network · G=IO2 network up · Y=IO6 DMX activity ·
B=IO7 source conflict · W=IO15 identify. Pins are configurable in /config.MAX_OUTPUTS = 2). Universe 1 is on UART/port 1
(TX=IO17, RX=IO18, DE=IO8); the new universe 2 is on UART/port 2 — wire to TX=IO16, RX=IO21,
DE/RTS=IO47. Output #2 ships disabled by default; enable it and set those pins in /config
(out#2: port=2 tx=16 rx=21 rts=47). All pins are runtime-configurable, so no firmware rebuild is needed.The RJ45 MagJack's link/act LEDs are driven by the W5500 itself — no firmware needed.
Build caveats (both handled, but worth knowing for a clean checkout / CI):
- esp_dmx 4.1.0 on ESP-IDF 5.5 — needs a small fix (the removed
uart_periph_signal[].modulefield, plus the older UART2 guard). Applied automatically at build time byextra_scripts.py— no manual step.- pioarduino toolchain installer — release
55.03.39's Xtensa toolchain archive tripsidf_tools.py(do_strip_container_dirs: "expected 1 entry, got ['package.json', …]"), which leaves the compiler uninstalled. This is an installer bug, not a repo issue; it needs a one-time host patch (ignore the straypackage.json) or a different platform pin. CI on this pinned release needs the same handling.
python + kicad-cli)tools/)tools/pip install skidl) to regenerate the netlist| Qty | Value | Footprint | References |
|---|---|---|---|
| 12 | 100nF | C_0402_1005Metric | C1, C5, C8, C9, C10, C11, C14, C18, C19, C22, C23, C24 |
| 5 | LED | LED_0603_1608Metric | D2, D3, D4, D5, D6 |
| 4 | 10k | R_0402_1005Metric | R1, R2, R11, R25 |
| 4 | 10uF | C_1206_3216Metric | C20, C21, C25, C26 |
| 4 | 470R | R_0402_1005Metric | R20, R21, R22, R23 |
| 4 | M3 GND | MountingHole_3.2mm_M3_Pad | MH1, MH2, MH3, MH4 |
| 4 | TBU 200mA 650V | DIO-SMD_3P-L6.5-W4.0-BI | F2, F3, F4, F5 |
| 3 | 150R | R_0402_1005Metric | R14, R16, R17 |
| 3 | 22uF | C_0805_2012Metric | C16, C17, C29 |
| 3 | 600R@100MHz | L_0805_2012Metric | FB1, FB2, FB3 |
| 2 | 10uF | C_0805_2012Metric | C2, C28 |
| 2 | 120R | R_0805_2012Metric | R12, R19 |
| 2 | 18pF C0G | C_0402_1005Metric | C12, C13 |
| 2 | 1k | R_0402_1005Metric | R13, R15 |
| 2 | 1uF | C_0603_1608Metric | C30, C31 |
| 2 | 1uF | C_0402_1005Metric | C3, C4 |
| 2 | 330R | R_0402_1005Metric | R4, R5 |
| 2 | 5k1 | R_0402_1005Metric | R8, R9 |
| 2 | ACM2012-201-2P | L_CommonModeChoke_Coilank_ACM2012 | L2, L3 |
| 2 | B0505S-1W | PWRM-TH_B0505S-1W | PS1, PS2 |
| 2 | ISO3086DWR | SOIC-16_L10.3-W7.5-P1.27-LS10.3-BL | U5, U6 |
| 2 | SM712 | SOT-23 | D1, D7 |
| 2 | XLR-5 NC5FAH | CONN-TH_NC5FAH | J1, J5 |
| 1 | 1.5A 16V PPTC | Fuse_1206_3216Metric | F1 |
| 1 | 100uF | CP_Elec_6.3x7.7 | C27 |
| 1 | 12.4k | R_0402_1005Metric | R3 |
| 1 | 2.2uH | IND-SMD_L4.0-W4.0 | L1 |
| 1 | 25MHz | Crystal_SMD_3225-4Pin_3.2x2.5mm | Y1 |
| 1 | 30k | R_0402_1005Metric | R24 |
| 1 | 4.7uF | C_0402_1005Metric | C6 |
| 1 | 45.3k | R_0402_1005Metric | R10 |
| 1 | 49.9R | R_0402_1005Metric | R18 |
| 1 | BOOT | SW_SPST_B3U-1000P | SW1 |
| 1 | DISP SH9 | JST_SH_SM09B-SRSS-TB_1x09-1MP_P1.00mm_Horizontal | J4 |
| 1 | DMX-OUT A | JST_SH_SM03B-SRSS-TB_1x03-1MP_P1.00mm_Horizontal | J7 |
| 1 | DMX-OUT B | JST_SH_SM03B-SRSS-TB_1x03-1MP_P1.00mm_Horizontal | J8 |
| 1 | DP9900M-5V | PWRM-SMD_DP9900M-5V | U7 |
| 1 | ESP32-S3-WROOM-1-N8R2 | ESP32-S3-WROOM-1 | U1 |
| 1 | EXP SH9 | JST_SH_SM09B-SRSS-TB_1x09-1MP_P1.00mm_Horizontal | J6 |
| 1 | HY931147C | RJ45-TH_HY931147C | J3 |
| 1 | RST | SW_SPST_B3U-1000P | SW2 |
| 1 | SMAJ5.0A | D_SMA | D11 |
| 1 | SMAJ60A | D_SMA | D10 |
| 1 | SY8089 | SOT-23-5_L3.0-W1.7-P0.95-LS2.8-BR | U4 |
| 1 | TPS2116DRLR | SOT-583-8 | U9 |
| 1 | USB-C | USB-C_SMD-TYPE-C-31-M-12_1 | J2 |
| 1 | USBLC6-2SC6 | SOT-23-6 | U8 |
| 1 | W5500 | LQFP-48_L7.0-W7.0-P0.50-LS9.0-BL | U2 |

tombueng/LuxDMX
Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet. Live web UI shows all 512 channels in real time, flags console conflicts, and drives galvanically-isolated DMX.

tombueng/LuxDMX
Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet. Live web UI shows all 512 channels in real time, flags console conflicts, and drives galvanically-isolated DMX.

tombueng/LuxDMX
Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet. Live web UI shows all 512 channels in real time, flags console conflicts, and drives galvanically-isolated DMX.

tombueng/LuxDMX
Open-source Art-Net / sACN (E1.31) → DMX512 gateway for ESP32 / ESP32-S3 / Ethernet. Live web UI shows all 512 channels in real time, flags console conflicts, and drives galvanically-isolated DMX.

JonasE99/HCP-Bridge
🚧 WIP – not yet manufactured or tested. KiCad PCB (one per door) connecting Hörmann series 4 garage door operators (ProMatic/SupraMatic, BUS/RJ12) to Loxone via ESP32-S3 and Wi-Fi.

beleata/fpga-led-panel-sram
iCE40HX1K 64x32 ICND1065L RGB panel with double-buffered SRAM and 2 Mbaud UART image upload

llextv/coreLight
CoreLight is an open-source DIY lighting controller built around an STM32H743.





It measures 119 × 79 mm and has 4 copper layers, 1.6 mm thick.
97 parts, built around the DP9900M-5V, ESP32-S3-WROOM-1-N8R2, B0505S-1W and SY8089A. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the tombueng/LuxDMX repository on GitHub, at the commit this page was made from: the board is hardware/luxdmx.kicad_pcb and its schematic hardware/luxdmx.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository states no licence, so its author keeps all rights: ask tombueng before reusing the design.