Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8841a88
DMXOutput: Track initialized state of instance. Add comments about up…
Mdbelen Apr 21, 2026
4996e17
DMXOutput: Use uart_ll to get satisfying timing results.
Mdbelen Apr 21, 2026
3a67e38
DMXOutput: Also replace DMXESPSerial by this class.
Mdbelen Apr 21, 2026
1607445
Add runtime config of DMX output pin
netmindz Mar 28, 2026
fb8cb5f
Provide a unified interface for DMX output
netmindz Mar 28, 2026
5087478
Update EspDmxOutput to work with new DMXOutput interface
Mdbelen Apr 19, 2026
ee3141a
fix(DMX): Fix sync settings page element hiding.
Mdbelen Apr 18, 2026
bc80c4a
DMX: Rename define from WLED_ENABLE_DMX to WLED_ENABLE_DMX_OUTPUT
Mdbelen Apr 18, 2026
da582e9
DMX: Introduce DMX_TXPIN_DEFAULT for compile time default DMX TX pin …
Mdbelen Apr 18, 2026
2ef4209
DMX: Fix critical buffer overrun
Mdbelen Apr 19, 2026
df76e37
DMX: Make DMX transmission async to speedup main loop for ESP32x.
Mdbelen Apr 12, 2026
fe41232
DMX: Move fcn declarations to dmx_output.h. Remove outdated dmxInput …
Mdbelen Apr 19, 2026
32806ae
DMX: Output DMX debug timing stats.
Mdbelen Apr 12, 2026
0d0a646
DMX: Rewrite DMXOutput.
Mdbelen Apr 21, 2026
f2edd8d
DMXOutput: Small improvements and move doc to header.
Mdbelen Apr 21, 2026
c3fc87a
DMXOutput: Minor fixes
Mdbelen Apr 23, 2026
8b8cc07
Revert "DMX: Rename define from WLED_ENABLE_DMX to WLED_ENABLE_DMX_OU…
Mdbelen Apr 23, 2026
8c658ce
DMXOutput: More AI review fixes.
Mdbelen Apr 23, 2026
be1c030
DMX: Fix some review findings.
Mdbelen Apr 30, 2026
d5e1354
Merge remote-tracking branch 'upstream/main' into dev/dmxAllTogether
Mdbelen Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
tdd = if_live[F("timeout")] | -1;
if (tdd >= 0) realtimeTimeoutMs = tdd * 100;

#ifdef WLED_ENABLE_DMX
CJSON(dmxOutputPin, if_live_dmx[F("dmxOutputPin")]);
#endif
#ifdef WLED_ENABLE_DMX_INPUT
CJSON(dmxInputTransmitPin, if_live_dmx[F("inputRxPin")]);
CJSON(dmxInputReceivePin, if_live_dmx[F("inputTxPin")]);
Expand Down Expand Up @@ -1124,6 +1127,9 @@ void serializeConfig(JsonObject root) {
if_live_dmx[F("addr")] = DMXAddress;
if_live_dmx[F("dss")] = DMXSegmentSpacing;
if_live_dmx["mode"] = DMXMode;
#ifdef WLED_ENABLE_DMX
if_live_dmx[F("dmxOutputPin")] = dmxOutputPin;
#endif
#ifdef WLED_ENABLE_DMX_INPUT
if_live_dmx[F("inputRxPin")] = dmxInputTransmitPin;
if_live_dmx[F("inputTxPin")] = dmxInputReceivePin;
Expand Down
7 changes: 1 addition & 6 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -652,12 +652,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");

// Defaults pins, type and counts to configure LED output
#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3)
#ifdef WLED_ENABLE_DMX
#define DEFAULT_LED_PIN 1
#warning "Compiling with DMX. The default LED pin has been changed to pin 1."
#else
#define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board
#endif
#define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board
#else
#if defined(WLED_USE_ETHERNET)
#define DEFAULT_LED_PIN 4 // GPIO4 seems to be a "safe bet" for all known ethernet boards (issue #5155)
Expand Down
3 changes: 2 additions & 1 deletion wled00/data/settings_dmx.htm
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
<div class="helpB"><button type="button" onclick="HW()">?</button></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button><hr>
</div>
<h2>Imma firin ma lazer (if it has DMX support)</h2><!-- TODO: Change to something less-meme-related //-->
<h2>DMX Output Settings</h2>
<p style="padding-top: 0; margin-top: 0; font-size: 0.8em; font-style: italic;">For pin settings, go to <a class="lnk" href="sync#dmxOutput">Sync</a> settings.</p>

Proxy Universe <input name=PU type=number min=0 max=63999 required> from E1.31 to DMX (0=disabled)<br>
<i>This will disable the LED data output to DMX configurable below</i><br><br>
Expand Down
17 changes: 12 additions & 5 deletions wled00/data/settings_sync.htm
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
}
function hideDMXInput(){gId("dmxInput").style.display="none";}
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
function hideNoDMXOutput(){gId("dmxOnOffOutput").style.display="none";}
function hideNoDMXOutput(){
gId("dmxOutputOff").style.display="none";
gId("dmxOutput").style.display="block";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</script>
</head>
<body>
Expand Down Expand Up @@ -174,17 +177,21 @@ <h3>Realtime</h3>
Disable realtime gamma correction: <input type="checkbox" name="RG"><br>
Realtime LED offset: <input name="WO" type="number" min="-255" max="255" required>
<div id="dmxInput">
<h4>Wired DMX Input Pins</h4>
<h3>Wired DMX Input Pins</h3>
DMX RX: <input name="IDMR" type="number" min="-1" max="99">RO<br/>
DMX TX: <input name="IDMT" type="number" min="-1" max="99">DI<br/>
DMX Enable: <input name="IDME" type="number" min="-1" max="99">RE+DE<br/>
DMX Port: <input name="IDMP" type="number" min="1" max="2"><br/>
</div>
<div id="dmxOutput" style="display: none">
<h3>Wired DMX Output Pin</h3>
DMX TX: <input name="IDMO" type="number" min="-1" max="99"><br/>
</div>
Comment on lines +205 to +208
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the missing reboot-required hint for IDMO.

This setting is only applied during WLED::setup(), so saving a new DMX output pin will not take effect until restart. The new section should say that explicitly, like the DMX input block does.

💡 Proposed change
 <div id="dmxOutput" style="display: none">
 	<h3>Wired DMX Output Pin</h3>
 	DMX TX: <input name="IDMO" type="number" min="-1" max="99"><br/>
+	<i class="warn">Reboot required to apply changes.</i>
 </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/data/settings_sync.htm` around lines 205 - 208, The DMX output setting
block (div id "dmxOutput" with input name "IDMO") is missing a reboot-required
hint; update the UI markup for the DMX TX/IDMO control to include the same
explicit note shown in the DMX input block that changes take effect only after
restart, and mention that the value is applied in WLED::setup() so users must
reboot for the new DMX output pin to take effect.

<div id="dmxInputOff">
<br><i class="warn">This firmware build does not include DMX Input support. <br></i>
<br><i class="warn">This firmware build does not include DMX Input support.</i>
</div>
<div id="dmxOnOffOutput">
<br><i class="warn">This firmware build does not include DMX output support. <br></i>
<div id="dmxOutputOff">
<br><i class="warn">This firmware build does not include DMX output support.</i>
</div>
</div>
<div class="sec">
Expand Down
2 changes: 1 addition & 1 deletion wled00/dmx_input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ bool DMXInput::installDriver()
void DMXInput::init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum)
{

#ifdef WLED_ENABLE_DMX_OUTPUT
#ifdef WLED_ENABLE_DMX
//TODO add again once dmx output has been merged
// if(inputPortNum == dmxOutputPort)
// {
Expand Down
216 changes: 182 additions & 34 deletions wled00/dmx_output.cpp
Original file line number Diff line number Diff line change
@@ -1,33 +1,191 @@
#ifdef WLED_ENABLE_DMX

#include "wled.h"
#include "dmx_output.h"
#ifdef ESP8266
#include "uart.h"
#include "esp8266_peri.h"
#else
#include "hal/uart_ll.h"
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/*
* Support for DMX output via serial (e.g. MAX485).
* Change the output pin in src/dependencies/ESPDMX.cpp, if needed (ESP8266)
* Change the output pin in src/dependencies/SparkFunDMX.cpp, if needed (ESP32)
* ESP8266 Library from:
* https://github.com/Rickgg/ESP-Dmx
* ESP32 Library from:
* https://github.com/sparkfun/SparkFunDMX
*/
bool DMXOutput::init(int8_t outputPin, uint8_t updateRate, int8_t uartNo) {

#ifdef WLED_ENABLE_DMX
// If already initialized, users have to call end() first. We won't do it for them.
if(_uartNo >= 0)
return false;

#ifdef ESP8266
if(uartNo == -1) uartNo = 1;
if((uartNo != 1) || (outputPin != 2)) {
DEBUG_PRINTF_P(PSTR("DMXOutput: Can only run with UART1, TX pin 2 on ESP8266."));
return false;
}
#else //not ESP8266
static_assert(SOC_UART_NUM > 1, "DMX output is not possible on your MCU, as it does not have HardwareSerial(1)");

if(uartNo == -1) {
uartNo = SOC_UART_NUM - 1; // use last UART as default
}
if(uartNo == 0) {
DEBUG_PRINTF_P(PSTR("DMXOutput: Error: Cannot run on chips with <=1 hardware UART, or with UART0."));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this restriction necessary?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I'm inclined to say I only updated some module here and these restrictions applied before too, even more restrictive. SparkFunDMX would throw an error if HardwareSerial(2) didn't exist.
This restriction was eased by netmindz in his commit to allow 2-UART SoCs.
The problem IMO is that hardware resources don't have any means to be "reserved/dedicated" to a specific functionality, like PinManager does for pins. I don't have much clue what modules and built-in functionality exists, so what conflicts can or will arise. All I can say is Serial0 would probably be a bad choice, as at least in the beginning we have some debug output there, even if WLED_DEBUG is disabled. If this is enabled, then it gets ofc so much worse.
And even if hw is only used in an ordered, sequential manner, you'd need to pay attention to fully reset all registers, as depending on the HAL, you'd assume reset condition when initializing, thus don't reset bits that were set by other users before.

Copy link
Copy Markdown
Collaborator

@DedeHai DedeHai Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. We really need a resource manager, there is a growing list of features that have hardware resource conflicts. This is something that came up in discussions several times, it is only a question of when and how.

return false;
}
#endif //ESP8266 or ESP32

if(outputPin < 0) return false;
const bool pinAllocated = PinManager::allocatePin(outputPin, true, PinOwner::DMX_OUTPUT);
if(!pinAllocated) {
DEBUG_PRINTF_P(PSTR("DMXOutput: Error: Failed to allocate pin %d for DMX output\n"), outputPin);
return false;
}
DEBUG_PRINTF_P(PSTR("DMXOutput: init: pin %d\n"), outputPin);

digitalWrite(outputPin, 1);
pinMode(outputPin, OUTPUT);
_outputPin = outputPin;
_updateRate = updateRate;
_dmxSerial = new HardwareSerial(uartNo);
_uartNo = uartNo;

#ifdef ESP8266
// Sadly no TX buffer. But at least still a TX FIFO.
_dmxSerial->begin(DMXSPEED, DMXFORMAT, SERIAL_TX_ONLY, outputPin);
#else
// DMX_CHANNELS + SOC_UART_FIFO_LEN is the minimum that leads to full async operation. Don't ask me why.
_dmxSerial->setTxBufferSize(DMX_CHANNELS + SOC_UART_FIFO_LEN); //641 = 1ms, 600 = 6ms, 514 = 10ms, 513-SOC_UART_FIFO_LEN=385 = 10ms, 185 = 17ms
_dmxSerial->begin(DMXSPEED, DMXFORMAT, -1, outputPin);
#endif

void handleDMXOutput()
{
return true;
}

void DMXOutput::end() {
if(_uartNo >= 0) {
#ifdef ESP8266
_dmxSerial->end();
#endif
delete _dmxSerial; // end() is implied in delete
pinMode(_outputPin, INPUT);
_uartNo = -1;

PinManager::deallocatePin(_outputPin, PinOwner::DMX_OUTPUT);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

DMXOutput::~DMXOutput() {
end();
}

void DMXOutput::write(uint16_t channel, uint8_t value) {
if(channel > DMX_CHANNEL_TOP) return; // out of bounds
_dmxData[channel] = value;
}

void DMXOutput::writeBytes(uint16_t channelStart, uint8_t values[], uint16_t len) {
if(channelStart == 0) return; // channel 0 is no valid start channel, because it is special function
for(int i = 0; i < len; i++) {
if(channelStart + i > DMX_CHANNEL_TOP) break; // finish when we reached the DMX channel 512
write(channelStart + i, values[i]);
}
}

uint8_t DMXOutput::read(uint16_t channel) {
if(channel > DMX_CHANNEL_TOP) return 0; // out of bounds
return _dmxData[channel];
}

bool DMXOutput::readBytes(uint16_t channelStart, uint8_t values[], uint16_t len) {
if(channelStart + len > DMX_CHANNELS) return false; // out of bounds

memcpy(values, &_dmxData[channelStart], len);
return true;
Comment on lines +99 to +103
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a non-overflowing bounds check before memcpy.

channelStart + len > DMX_CHANNELS can wrap for large inputs, so calls like readBytes(65535, buf, 2) bypass the guard and read past _dmxData.

🛡️ Proposed fix
 bool DMXOutput::readBytes(uint16_t channelStart, uint8_t values[], uint16_t len) {
-  if(channelStart + len > DMX_CHANNELS) return false;   // out of bounds
+  if(channelStart > DMX_CHANNELS) return false;
+  if(len > DMX_CHANNELS - channelStart) return false;   // out of bounds
 
   memcpy(values, &_dmxData[channelStart], len);
   return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 99 - 103, DMXOutput::readBytes uses
channelStart + len which can overflow; replace that check with a non-overflowing
test such as verifying channelStart is within range and len does not exceed the
remaining channels (e.g., check channelStart >= DMX_CHANNELS || len >
DMX_CHANNELS - channelStart) before calling memcpy on _dmxData so out-of-bounds
reads are prevented.

}

bool DMXOutput::update() {
// false if not properly initialized
if(_uartNo < 0) return false;

// Rate limiting & only send dmx frame if no other frame is just ongoing i.e. TXbuf is empty
if((timeToNextUpdate() <= 0) && !busy()) {
_lastDmxOutMillis = millis();

// Send DMX break
// Cannot change UART format while running. End and reinit takes much longer than the additional stopbit here.
_dmxSerial->updateBaudRate(BREAKSPEED); //change to DMX break settings
_dmxSerial->write(0);
_dmxSerial->flush();

// Send DMX data
_dmxSerial->updateBaudRate(DMXSPEED); //change to regular DMX speed
_dmxSerial->write(_dmxData, DMX_CHANNELS);

return true;
}
return false;
}

bool DMXOutput::busy() {
if(_uartNo < 0) return true; // not initialized

#ifdef ESP8266
// uart_tx_fifo_available is inline-only in uart.cpp, reproduce it here:
size_t uart_tx_fifo_available = (USS(_uartNo) >> USTXC) & 0xff;

if(uart_tx_fifo_available == 0) {
// according to uart.cpp there can be one more transmission (11 baud) after tx_fifo is empty, so we'll wait just
// in case. This makes every call take 45us which seems acceptable.
delayMicroseconds(11 * 1000000 / DMXSPEED + 1);
return false;
} else
return true;
#else
// not busy if tx idle. HardwareSerial.availableForWrite() didn't work reliable.
return !uart_ll_is_tx_idle(UART_LL_GET_HW(_uartNo));
#endif
}

unsigned long DMXOutput::getLastDmxOut() {
return _lastDmxOutMillis;
}

void DMXOutput::setUpdateRate(uint8_t updateRate) {
_updateRate = updateRate;
}
uint8_t DMXOutput::getUpdateRate() {
return _updateRate;
}

int DMXOutput::timeToNextUpdate() {
if(_uartNo < 0) return INT_MAX; // not initialized
if(_updateRate == 0) return -1; // if refresh rate set to 0, refresh rate is max.

// treat _updateRate as maximum, so round up the refresh delay
float fdmxFrameTime = 1000.0 / _updateRate;
int dmxFrameTime = (uint16_t)fdmxFrameTime;
if(fdmxFrameTime - dmxFrameTime > 0.0) dmxFrameTime += 1; // if fractional part > 0, add one

return dmxFrameTime - (millis() - _lastDmxOutMillis);
}

bool DMXOutput::handleDMXOutput() {
// don't act, when in DMX Proxy mode
if (e131ProxyUniverse != 0) return;
if (e131ProxyUniverse != 0) return false;

uint8_t brightness = strip.getBrightness();

bool calc_brightness = true;

// check if no shutter channel is set
for (unsigned i = 0; i < DMXChannels; i++)
{
for (unsigned i = 0; i < DMXChannels; i++) {
if (DMXFixtureMap[i] == 5) calc_brightness = false;
}

uint16_t len = strip.getLengthTotal();
if(DMXGap < 1) DMXGap = 1; // failsafe
uint16_t maxLen = (DMX_CHANNELS - DMXStart) / DMXGap; // maximum LEDs that fit into one physical DMX512 universe
if (len > maxLen) len = maxLen;
Comment thread
Mdbelen marked this conversation as resolved.

for (int i = DMXStartLED; i < len; i++) { // uses the amount of LEDs as fixture count

uint32_t in = strip.getPixelColor(i); // get the colors for the individual fixtures as suggested by Aircoookie in issue #462
Expand All @@ -40,42 +198,32 @@ void handleDMXOutput()
for (int j = 0; j < DMXChannels; j++) {
int DMXAddr = DMXFixtureStart + j;
switch (DMXFixtureMap[j]) {
case 0: // Set this channel to 0. Good way to tell strobe- and fade-functions to fuck right off.
dmx.write(DMXAddr, 0);
case 0: // Set this channel to 0
write(DMXAddr, 0);
break;
case 1: // Red
dmx.write(DMXAddr, calc_brightness ? (r * brightness) / 255 : r);
write(DMXAddr, calc_brightness ? (r * brightness) / 255 : r);
break;
case 2: // Green
dmx.write(DMXAddr, calc_brightness ? (g * brightness) / 255 : g);
write(DMXAddr, calc_brightness ? (g * brightness) / 255 : g);
break;
case 3: // Blue
dmx.write(DMXAddr, calc_brightness ? (b * brightness) / 255 : b);
write(DMXAddr, calc_brightness ? (b * brightness) / 255 : b);
break;
case 4: // White
dmx.write(DMXAddr, calc_brightness ? (w * brightness) / 255 : w);
write(DMXAddr, calc_brightness ? (w * brightness) / 255 : w);
break;
case 5: // Shutter channel. Controls the brightness.
dmx.write(DMXAddr, brightness);
write(DMXAddr, brightness);
break;
case 6: // Sets this channel to 255. Like 0, but more wholesome.
dmx.write(DMXAddr, 255);
write(DMXAddr, 255);
break;
}
}
}

dmx.update(); // update the DMX bus
return update(); // update the DMX bus, if available
}

void initDMXOutput() {
#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)
dmx.init(512); // initialize with bus length
#else
dmx.initWrite(512); // initialize with bus length
#endif
}
#else
void initDMXOutput(){}
void handleDMXOutput() {}
#endif
Loading
Loading