Skip to content

Non-blocking, unified, dependency-less DMXOutput#5522

Open
Mdbelen wants to merge 20 commits intowled:mainfrom
Mdbelen:dev/dmxAllTogether
Open

Non-blocking, unified, dependency-less DMXOutput#5522
Mdbelen wants to merge 20 commits intowled:mainfrom
Mdbelen:dev/dmxAllTogether

Conversation

@Mdbelen
Copy link
Copy Markdown

@Mdbelen Mdbelen commented Apr 21, 2026

tl;dr:
Related to #5216 also see previous comments there. This is an alternative to make dmx.update() non-blocking. With less dependencies, more unified between ESP8266&32 and I claim more stable.
Fixes #5133

The core essence of this PR is dmx_output.cpp:57, the rest is cosmetics.

Long:
Independent from https://github.com/Stijn-Jacobs I also encountered the problem that once DMX output gets enabled in v0.15.3, WLED becomes super laggy regarding LED output via ArtNet, and almost unreachable via network. Short digging showed, that the DMX output is implemented in a blocking way, leading to 23ms of CPU time per loop run just for DMX.
This is unacceptable.
Stijn kindly prepared a fix in 5216 and I made mine in parallel locally. While I evaluated Stijns solution, I saw on oscilloscope, that the DMX frame initial MAB pulse, which is supposed to be at least 12us, sometimes is only 8us. I consider this suboptimal and would feel a little unsure using this. The used esp_dmx dependency is comparatively super complicated for what DMX needs, so I decided to post my solution as an alternative here.

The DMX part is quite trivial and the same for DMXESPOutput and SparkFunDMX. Now that DMX Input has been moved to another module altogether, I saw no more reason to keep those two separate dependencies and merged both in this new DMXOutput module. ESP8266 is still mostly blocking (due to limitations in HAL/Arduino), while ESP32 is almost completely non-blocking and takes less than 1ms per run.

The module is prepared to have an update rate element added to the web UI in future. Setting update rate to 0 ensures the max possible rate. Maximum with all 512/513 DMX channels is 43Hz, this is currently the default.

[outdated, see comments: I decided to also change the #defines to better differentiate between the new DMXInput and DMXOutput modules. The new way to enable DMXOutput is to set WLED_ENABLE_DMX_OUTPUT instead of WLED_ENABLE_DMX, which I found confusing.]
Also, there's the possibility to set the initial TX pin using DMX_TXPIN_DEFAULT=1337. kno.wled.ge would need an update here.

In this branch, I also included some commits from netmindz #5287 adding TX pin setting on web UI, and the update rate limiting from Stijn (sadly couldn't cherry-pick as was mixed with too many other changes).
I did not use the number of DMX-fixtures UI element, because I consider it irrelevant for ESP32 and found it more confusing. This would lower the processor and RAM load marginally. It could be possibly helpful for ESP8266, but I think this is not the focus here.

This is tested on my ESP32 hardware with an oscilloscope. Didn't get the chance to connect my DMX fixtures, yet, but I'm pretty sure that will work. Untested on ESP8266, no hardware available.

Summary by CodeRabbit

  • New Features

    • DMX output pin is now configurable in Sync settings, allowing flexible pin assignment instead of hardcoded defaults
  • Improvements

    • Updated DMX settings UI with clearer labeling ("DMX Output Settings") and instructional guidance
    • Enhanced DMX output transmission with improved efficiency and rate limiting capabilities

Mdbelen and others added 15 commits April 22, 2026 00:03
This increases clarity that WLED_ENABLE_DMX_INPUT is not a subset of WLED_ENABLE_DMX but independent.
…allocation.

This is useful for boards with DMX.
This limits the number of used pixels to total to 512 channels used for transmitting via DMX
according to the DMX start and spacing settings.
All DMX functions are handled on its own for ESP32 (remove SparkFunDMX dependency)
features:
* non-blocking DMX update function
* maximum refresh rate setting
* some more useful functions
For ESP8266 stay with DMXESPSerial for now and wrap that in DMXOutput.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Walkthrough

This PR refactors WLED's DMX output support by introducing a new unified DMXOutput class to replace legacy library implementations (ESPDMX, SparkFunDMX), adds configurable TX pin support through the sync settings UI, and integrates DMX channel output with UART-based serial communication and performance monitoring.

Changes

Cohort / File(s) Summary
DMX Output Core Implementation
wled00/dmx_output.h, wled00/dmx_output.cpp
New DMXOutput class implementing UART-driven lifecycle: init() with ESP8266-specific UART1 enforcement, channel read/write APIs, update() with break/frame transmission, busy() status, rate limiting via setUpdateRate(), and platform-specific TX FIFO busy detection.
Legacy DMX Library Removal
wled00/src/dependencies/dmx/ESPDMX.*, wled00/src/dependencies/dmx/SparkFunDMX.*, wled00/src/dependencies/dmx/LICENSE.md
Deletes old ESPDMX and SparkFunDMX implementations along with associated license file, consolidating to single new DMXOutput backend.
Configuration & Serialization
wled00/cfg.cpp
Adds conditional dmxOutputPin serialization/deserialization under WLED_ENABLE_DMX to persist configured TX pin in JSON config.
Global State & Declarations
wled00/wled.h, wled00/fcn_declare.h, wled00/dmx_input.cpp
Replaces platform-specific dmx global objects with unified dmxOutput instance; removes legacy initDMXOutput() and handleDMXOutput() function declarations; updates preprocessor flag in DMX input code from WLED_ENABLE_DMX_OUTPUT to WLED_ENABLE_DMX.
Pin Management
wled00/pin_manager.h, wled00/pin_manager.cpp
Renames PinOwner::DMX enum to PinOwner::DMX_OUTPUT; updates pin owner string lookup to reflect new naming.
Initialization & Performance Monitoring
wled00/wled.cpp
Replaces initDMXOutput() call and GPIO2 reservation with dmxOutput.init(dmxOutputPin, 43); adds debug-only performance instrumentation for dmxOutput.handleDMXOutput() runtime tracking in main loop.
UI & Settings Updates
wled00/data/settings_dmx.htm, wled00/data/settings_sync.htm, wled00/set.cpp, wled00/xml.cpp
Updates DMX settings page header to "DMX Output Settings" with sync-page pin configuration instructions; adds hidden dmxOutput block with IDMO numeric input for wired DMX output pin; refactors hideNoDMXOutput() to toggle new UI containers; adds IDMO parameter handling in settings sync submission; updates GPIO info generation to reserve dmxOutputPin instead of hardcoded pin 2.
E1.31 Protocol Integration
wled00/e131.cpp
Replaces loop-based dmx.write() calls with single bulk dmxOutput.writeBytes() operation for improved efficiency in E1.31 proxy-universe path.

Sequence Diagram

sequenceDiagram
    participant WLED as WLED Core
    participant DMXOut as DMXOutput
    participant UART as UART/Serial

    WLED->>DMXOut: init(dmxOutputPin, updateRate)
    activate DMXOut
    DMXOut->>UART: create HardwareSerial, configure TX pin
    DMXOut->>DMXOut: initialize channel buffer
    deactivate DMXOut

    loop Main Loop
        WLED->>DMXOut: handleDMXOutput()
        activate DMXOut
        alt not busy() and rate-limited
            DMXOut->>DMXOut: write(channel, value)
            DMXOut->>DMXOut: update()
            activate DMXOut
            DMXOut->>UART: send DMX break (baud/format switch)
            DMXOut->>UART: transmit full channel buffer
            DMXOut->>UART: reset start code
            deactivate DMXOut
        end
        deactivate DMXOut
    end

    WLED->>DMXOut: end()
    activate DMXOut
    DMXOut->>UART: stop serial, deallocate resources
    deactivate DMXOut
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #5353 — Modifies the same hideNoDMXOutput() visibility logic and related DOM element IDs in the sync settings UI.
  • PR #5473 — Updates settings_sync.htm to add/adjust DMX output pin controls and show/hide configuration.
  • PR #4997 — Modifies the E1.31 packet handling path in e131.cpp for DMX channel data forwarding.

Suggested labels

optimization

Suggested reviewers

  • DedeHai
  • softhack007
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing a non-blocking, unified DMX output module that removes external dependencies.
Linked Issues check ✅ Passed The PR addresses issue #5133 by replacing the blocking DMX implementation with a non-blocking unified DMXOutput module, eliminating CPU blocking that caused frontend unresponsiveness.
Out of Scope Changes check ✅ Passed All changes are directly related to DMX output refactoring: replacing legacy DMX libraries, adding unified DMXOutput class, updating configuration/serialization, and adjusting UI. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added enhancement hardware (Likely) Setup-specific issue, e.g. flickering LEDs labels Apr 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
wled00/dmx_output.h (2)

30-98: Missing <Arduino.h> include and a few const correctness nits.

This header uses HardwareSerial*, uint8_t, uint16_t without including anything. It works only because every current consumer transitively pulls in Arduino.h via wled.h first (clang's standalone parse correctly reports the errors shown in the static-analysis hints). Add an explicit #include <Arduino.h> so the header is self-contained.

Also, the query methods should be const so they can be called on a const DMXOutput&:

🧹 Proposed tweaks
 `#ifndef` DMX_OUTPUT_H
 `#define` DMX_OUTPUT_H
+
+#include <Arduino.h>
...
-    uint8_t read(uint16_t channel);
+    uint8_t read(uint16_t channel) const;
...
-    unsigned long getLastDmxOut();
+    unsigned long getLastDmxOut() const;
-    bool busy();
+    bool busy();                 // non-const: delays on ESP8266, touches hw state
-    void setUpdateRate(uint8_t updateRate);
-    uint8_t getUpdateRate();
+    void    setUpdateRate(uint8_t updateRate);
+    uint8_t getUpdateRate() const;
-    int timeToNextUpdate();
+    int     timeToNextUpdate() const;

As per coding guidelines: “Mark getter/query methods as const.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.h` around lines 30 - 98, Add a self-contained Arduino
include and make the query/getter methods const: add `#include` <Arduino.h> at the
top of the DMXOutput header, and mark the query methods getLastDmxOut(), busy(),
getUpdateRate(), timeToNextUpdate() (and any other non-mutating accessors like
read()/readBytes() if intended to be non-mutating) as const in their
declarations so they can be called on const DMXOutput instances; ensure the
signatures in the class (DMXOutput::~DMXOutput, init, end, write, writeBytes,
read, readBytes, update, handleDMXOutput, getLastDmxOut, busy, setUpdateRate,
getUpdateRate, timeToNextUpdate) remain consistent with any corresponding
definitions.

21-27: Parenthesize DMX_CHANNELS and prefer constexpr for these header constants.

#define DMX_CHANNELS DMX_CHANNEL_TOP + 1 is unparenthesized — any future use like DMX_CHANNELS * N or sizeof(buf)/DMX_CHANNELS would silently compute the wrong value. Current call sites happen to be safe, but this is a classic foot-gun.

Additionally, per the coding guidelines, header-scope compile-time constants should be constexpr rather than #define, which also gives them proper types and namespacing.

♻️ Suggested refactor
-#define DMX_CHANNEL_TOP 512
-#define DMX_CHANNELS DMX_CHANNEL_TOP + 1
-
-#define DMXSPEED       250000
-#define DMXFORMAT      SERIAL_8N2
-#define BREAKSPEED     83000
-#define BREAKFORMAT    SERIAL_8N1   // unused, instead, DMXFORMAT is used
+constexpr uint16_t DMX_CHANNEL_TOP = 512;
+constexpr uint16_t DMX_CHANNELS    = DMX_CHANNEL_TOP + 1;
+constexpr uint32_t DMXSPEED        = 250000;
+constexpr uint32_t BREAKSPEED      = 83000;
+constexpr auto     DMXFORMAT       = SERIAL_8N2;
+// BREAKFORMAT intentionally omitted — unused, DMXFORMAT is used for breaks too.

As per coding guidelines: “Prefer constexpr over #define for compile-time constants in C++”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.h` around lines 21 - 27, Replace the unparenthesized macro
and other header `#defines` with typed constexpr variables and ensure the
DMX_CHANNELS expression is evaluated correctly; specifically, change the macros
DMX_CHANNEL_TOP and DMX_CHANNELS to constexpr (e.g., constexpr int
DMX_CHANNEL_TOP = 512; constexpr int DMX_CHANNELS = DMX_CHANNEL_TOP + 1) so
DMX_CHANNELS behaves correctly in arithmetic contexts, and similarly convert
DMXSPEED, BREAKSPEED (use an appropriate integer type), and named constants like
DMXFORMAT/BREAKFORMAT to constexpr or scoped constexpr integers/enum values as
appropriate for the platform; keep the original names (DMX_CHANNEL_TOP,
DMX_CHANNELS, DMXSPEED, DMXFORMAT, BREAKSPEED, BREAKFORMAT) so call sites remain
valid.
wled00/dmx_output.cpp (1)

11-50: init() initialization-state check and pin guard miss a couple of edge cases.

Two small issues in init():

  • Line 14 uses if(_uartNo > 0) to mean “already initialized”, but _uartNo is only default-initialized to -1 when DMXOutput is a statically constructed global. Given dmxOutput is a global here that's fine, but a second init() call after a failed first call (where _uartNo was never assigned) is silently re-entered — consider asserting _dmxSerial == nullptr or using >= 0 with an explicit “initialized” flag, so the contract matches the doc comment.
  • Line 37 if(outputPin < 1) return false; is a no-op for the common “disabled” sentinel of -1 because outputPin is uint8_t, which makes it 255 (see the wled.h comment on dmxOutputPin). Changing the parameter to int8_t (or int) makes this guard actually protect against the documented sentinel.

Finally, per the coding guidelines the preprocessor #error at line 24–26 could be a static_assert(SOC_UART_NUM > 1, "...") — but since SOC_UART_NUM is only defined via SOC caps, #error is acceptable; flagging only for awareness.

As per coding guidelines: “Use static_assert over #if ... #error`` for compile-time assertions in C++”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 11 - 50, DMXOutput::init currently uses
if(_uartNo > 0) to detect prior initialization and takes outputPin as uint8_t
which breaks the documented -1 sentinel; change the init precondition to check a
proper initialized flag or _uartNo >= 0 and/or assert _dmxSerial == nullptr
(referencing _uartNo and _dmxSerial) so a failed prior init doesn't leave init
re-entrant, and change the outputPin parameter from uint8_t to a signed type
(int8_t or int) and update the guard from if(outputPin < 1) to correctly handle
the -1 disabled sentinel before calling PinManager::allocatePin and pinMode/
digitalWrite; leave the SOC_UART_NUM `#error` as-is or replace with static_assert
only if SOC_UART_NUM is a compile-time constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/cfg.cpp`:
- Around line 610-612: dmxOutputPin is deserialized as a signed int and then
passed to DMXOutput::init(uint8_t, ...) via CJSON(dmxOutputPin,
if_live_dmx[F("dmxOutputPin")]); validate the integer value before narrowing:
check that dmxOutputPin is within the valid range (>=1 and <= WLED_NUM_PINS) and
handle out-of-range values (e.g., ignore, set to 0/disabled, or log/error)
before casting to uint8_t; update the code path that calls DMXOutput::init (and
the if_live_dmx handling) to perform this range check and only pass a uint8_t
when the value is confirmed valid to avoid integer wrapping.

In `@wled00/data/settings_dmx.htm`:
- Line 57: The inline style on the help paragraph uses an invalid CSS property
`size: 0.8em`; update the <p> element (the help text paragraph referencing "For
pin settings, go to Sync settings.") to use `font-size: 0.8em` instead of `size:
0.8em` (keep the other style rules intact: padding-top, margin-top, font-style:
italic) so the smaller text is applied correctly.

In `@wled00/data/settings_sync.htm`:
- Around line 53-56: The hideNoDMXOutput function currently sets
gId("dmxOutput").style.display = "inline", which can collapse the div layout;
change it to set gId("dmxOutput").style.display = "block" (keep
gId("dmxOutputOff").style.display="none" as is) so the DMX output div
(gId("dmxOutput")) renders as a block containing its <h4> and form row; update
the display assignment in the hideNoDMXOutput function accordingly.

In `@wled00/dmx_output.cpp`:
- Around line 175-182: Validate and clamp DMX configuration before calculating
maxLen: ensure DMXGap != 0 (return/skip or set to 1 if invalid), ensure DMXStart
<= DMX_CHANNELS (or clamp DMXStart to DMX_CHANNELS), and assert DMXChannels >=
1; then compute maxLen using safe subtraction/cast (e.g., treat DMX_CHANNELS -
DMXStart as unsigned guarded value) so division by DMXGap and subtraction cannot
underflow or divide-by-zero; apply these checks around the block that computes
maxLen (references: DMXGap, DMXStart, DMX_CHANNELS, DMXChannels,
strip.getLengthTotal(), maxLen) and clamp len to the sanitized maxLen before
iterating.
- Around line 5-9: The code currently includes "uart.cpp" in dmx_output.cpp for
ESP8266 which will fail compile and can cause ODR/linker issues; instead remove
that include and add an external declaration for the required ESP8266 core
function (declare extern "C" int uart_tx_fifo_available(); or the correct
signature) so the compiler knows the symbol and the linker will resolve it from
the ESP8266 Arduino core; update the conditional branch around the include (the
`#ifdef` ESP8266 section) to declare the function prototype referencing
uart_tx_fifo_available() rather than including uart.cpp and verify the declared
signature matches the core header.

In `@wled00/e131.cpp`:
- Around line 117-121: The proxy currently always forwards from &e131_data[1],
which misaligns Art-Net (art_data starts at index 0) and can read past the
payload; update the proxy logic around e131ProxyUniverse / uni and
dmxOutput.writeBytes to pick the correct data pointer and length based on
protocol: use art_data (offset 0) when the packet is Art‑Net and e131_data+1
(offset 1) for E1.31, and clamp the byte count to the actual payload length
(min(dmxChannels, payloadLength - offset)) before calling dmxOutput.writeBytes
and dmxOutput.update. Ensure you reference the packet type/flags available in
the surrounding code to choose between art_data and e131_data and adjust the
offset accordingly.

In `@wled00/set.cpp`:
- Around line 477-479: When handling request->arg(F("IDMO")).toInt() for
dmxOutputPin (inside the WLED_ENABLE_DMX_OUTPUT block), avoid blindly saving 0
from toInt(): read the raw arg string first, if it's empty leave dmxOutputPin
unchanged, otherwise parse to an int and only assign it when parsed >= 1 and
passes runtime GPIO validation (use your existing runtime-valid pin check or the
same logic used by dmx_output.cpp which rejects outputPin < 1); for any
malformed/invalid value set dmxOutputPin = -1 (disabled) or keep prior -1, but
never store 0 as a valid pin.

In `@wled00/wled.cpp`:
- Around line 571-572: Move the guarded DMX pin reservation so the configured
DMX GPIO is allocated before any LED buses or usermods can claim it: call
dmxOutput.init(dmxOutputPin, 43) immediately after config load (right after
loading settings) and before beginStrip() and usermod setup, and remove the
later dmxOutput.init(...) call currently located after beginStrip(); reference
dmxOutput.init, dmxOutputPin and beginStrip to find and update the code.

In `@wled00/wled.h`:
- Line 43: The DMX feature flag was renamed from WLED_ENABLE_DMX to
WLED_ENABLE_DMX_OUTPUT and legacy uses will be silently ignored; add a migration
guard in wled.h before the DMX include/block that detects if WLED_ENABLE_DMX is
defined and emits a preprocessor warning or error (e.g., `#warning` or `#error`)
instructing users to rename it to WLED_ENABLE_DMX_OUTPUT, and optionally define
WLED_ENABLE_DMX_OUTPUT automatically for backward compatibility when the legacy
flag is present so existing platformio_override.ini/my_config.h/CI configs don’t
drop DMX silently.
- Around line 451-456: The DMX output code silently converts the sentinel
DMX_TXPIN_DEFAULT (-1) to 255 when calling DMXOutput::init, causing spurious
"pin 255" allocation errors; fix by (1) changing DMXOutput::init signature to
accept a signed type (int8_t or int) instead of uint8_t so the sentinel
round-trips and the existing outputPin < 1 guard behaves as intended (update
declaration in dmx_output.h and its implementation), and (2) in the caller
(where dmxOutput.init(dmxOutputPin, 43) is invoked) skip calling dmxOutput.init
entirely when dmxOutputPin equals DMX_TXPIN_DEFAULT (or < 0) to avoid attempting
init with an unset pin; finally, reconcile the default updateRate value between
the DMXOutput::init default (44) and the call site/PR (43) so the documented
default matches the actual used value.

---

Nitpick comments:
In `@wled00/dmx_output.cpp`:
- Around line 11-50: DMXOutput::init currently uses if(_uartNo > 0) to detect
prior initialization and takes outputPin as uint8_t which breaks the documented
-1 sentinel; change the init precondition to check a proper initialized flag or
_uartNo >= 0 and/or assert _dmxSerial == nullptr (referencing _uartNo and
_dmxSerial) so a failed prior init doesn't leave init re-entrant, and change the
outputPin parameter from uint8_t to a signed type (int8_t or int) and update the
guard from if(outputPin < 1) to correctly handle the -1 disabled sentinel before
calling PinManager::allocatePin and pinMode/ digitalWrite; leave the
SOC_UART_NUM `#error` as-is or replace with static_assert only if SOC_UART_NUM is
a compile-time constant.

In `@wled00/dmx_output.h`:
- Around line 30-98: Add a self-contained Arduino include and make the
query/getter methods const: add `#include` <Arduino.h> at the top of the DMXOutput
header, and mark the query methods getLastDmxOut(), busy(), getUpdateRate(),
timeToNextUpdate() (and any other non-mutating accessors like read()/readBytes()
if intended to be non-mutating) as const in their declarations so they can be
called on const DMXOutput instances; ensure the signatures in the class
(DMXOutput::~DMXOutput, init, end, write, writeBytes, read, readBytes, update,
handleDMXOutput, getLastDmxOut, busy, setUpdateRate, getUpdateRate,
timeToNextUpdate) remain consistent with any corresponding definitions.
- Around line 21-27: Replace the unparenthesized macro and other header `#defines`
with typed constexpr variables and ensure the DMX_CHANNELS expression is
evaluated correctly; specifically, change the macros DMX_CHANNEL_TOP and
DMX_CHANNELS to constexpr (e.g., constexpr int DMX_CHANNEL_TOP = 512; constexpr
int DMX_CHANNELS = DMX_CHANNEL_TOP + 1) so DMX_CHANNELS behaves correctly in
arithmetic contexts, and similarly convert DMXSPEED, BREAKSPEED (use an
appropriate integer type), and named constants like DMXFORMAT/BREAKFORMAT to
constexpr or scoped constexpr integers/enum values as appropriate for the
platform; keep the original names (DMX_CHANNEL_TOP, DMX_CHANNELS, DMXSPEED,
DMXFORMAT, BREAKSPEED, BREAKFORMAT) so call sites remain valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 931d3154-cb77-4ca0-b2d7-48b7b035e84f

📥 Commits

Reviewing files that changed from the base of the PR and between 01328a6 and f2edd8d.

📒 Files selected for processing (22)
  • platformio_override.sample.ini
  • tools/cdata.js
  • wled00/cfg.cpp
  • wled00/const.h
  • wled00/data/settings_dmx.htm
  • wled00/data/settings_sync.htm
  • wled00/dmx_output.cpp
  • wled00/dmx_output.h
  • wled00/e131.cpp
  • wled00/fcn_declare.h
  • wled00/pin_manager.cpp
  • wled00/pin_manager.h
  • wled00/set.cpp
  • wled00/src/dependencies/dmx/ESPDMX.cpp
  • wled00/src/dependencies/dmx/ESPDMX.h
  • wled00/src/dependencies/dmx/LICENSE.md
  • wled00/src/dependencies/dmx/SparkFunDMX.cpp
  • wled00/src/dependencies/dmx/SparkFunDMX.h
  • wled00/wled.cpp
  • wled00/wled.h
  • wled00/wled_server.cpp
  • wled00/xml.cpp
💤 Files with no reviewable changes (6)
  • wled00/src/dependencies/dmx/LICENSE.md
  • wled00/fcn_declare.h
  • wled00/src/dependencies/dmx/ESPDMX.h
  • wled00/src/dependencies/dmx/ESPDMX.cpp
  • wled00/src/dependencies/dmx/SparkFunDMX.h
  • wled00/src/dependencies/dmx/SparkFunDMX.cpp

Comment thread wled00/cfg.cpp Outdated
Comment thread wled00/data/settings_dmx.htm Outdated
Comment thread wled00/data/settings_sync.htm
Comment thread wled00/dmx_output.cpp
Comment thread wled00/dmx_output.cpp
Comment thread wled00/e131.cpp Outdated
Comment on lines +117 to +121
#ifdef WLED_ENABLE_DMX_OUTPUT
// does not act on out-of-order packets yet
if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) {
for (uint16_t i = 1; i <= dmxChannels; i++)
dmx.write(i, e131_data[i]);
dmx.update();
dmxOutput.writeBytes(1, &e131_data[1], dmxChannels);
dmxOutput.update();
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 | 🔴 Critical

Use the protocol-specific DMX data offset before proxying.

Art-Net payloads start at art_data[0], while E1.31 property values reserve index 0 for the start code. The unconditional &e131_data[1] shifts Art-Net output by one channel and reads past the payload for full 512-channel packets.

🐛 Proposed fix
   `#ifdef` WLED_ENABLE_DMX_OUTPUT
   // does not act on out-of-order packets yet
   if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) {
-    dmxOutput.writeBytes(1, &e131_data[1], dmxChannels);
+    const uint16_t dmxDataOffset = (mde == REALTIME_MODE_ARTNET) ? 0 : 1;
+    dmxOutput.writeBytes(1, &e131_data[dmxDataOffset], dmxChannels);
     dmxOutput.update();
   }
   `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/e131.cpp` around lines 117 - 121, The proxy currently always forwards
from &e131_data[1], which misaligns Art-Net (art_data starts at index 0) and can
read past the payload; update the proxy logic around e131ProxyUniverse / uni and
dmxOutput.writeBytes to pick the correct data pointer and length based on
protocol: use art_data (offset 0) when the packet is Art‑Net and e131_data+1
(offset 1) for E1.31, and clamp the byte count to the actual payload length
(min(dmxChannels, payloadLength - offset)) before calling dmxOutput.writeBytes
and dmxOutput.update. Ensure you reference the packet type/flags available in
the surrounding code to choose between art_data and e131_data and adjust the
offset accordingly.

Comment thread wled00/set.cpp Outdated
Comment thread wled00/wled.cpp Outdated
Comment on lines +571 to +572
#ifdef WLED_ENABLE_DMX_OUTPUT
dmxOutput.init(dmxOutputPin, 43);
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

Reserve the configured DMX pin before buses and usermods claim it.

dmxOutput.init() is the first actual PinManager allocation for dmxOutputPin, but it runs after beginStrip() and usermod setup. A default LED bus or usermod can claim the configured DMX GPIO first, causing DMX output init to fail.

🔧 Proposed fix

Move the guarded init to immediately after config load and before beginStrip():

   DEBUG_PRINTLN(F("Reading config"));
   bool needsCfgSave = deserializeConfigFromFS();
   DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
+
+#ifdef WLED_ENABLE_DMX_OUTPUT
+  if (dmxOutputPin > 0 && dmxOutputPin < (int)WLED_NUM_PINS) dmxOutput.init(static_cast<uint8_t>(dmxOutputPin), 43);
+#endif
 
 `#if` defined(STATUSLED) && STATUSLED>=0

Then remove the later init:

-#ifdef WLED_ENABLE_DMX_OUTPUT
-  dmxOutput.init(dmxOutputPin, 43);
-#endif
 `#ifdef` WLED_ENABLE_DMX_INPUT
   dmxInput.init(dmxInputReceivePin, dmxInputTransmitPin, dmxInputEnablePin, dmxInputPort);
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/wled.cpp` around lines 571 - 572, Move the guarded DMX pin reservation
so the configured DMX GPIO is allocated before any LED buses or usermods can
claim it: call dmxOutput.init(dmxOutputPin, 43) immediately after config load
(right after loading settings) and before beginStrip() and usermod setup, and
remove the later dmxOutput.init(...) call currently located after beginStrip();
reference dmxOutput.init, dmxOutputPin and beginStrip to find and update the
code.

Comment thread wled00/wled.h Outdated
Comment thread wled00/wled.h Outdated
Comment thread wled00/dmx_output.cpp
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.

@netmindz
Copy link
Copy Markdown
Member

I'll try and have a proper look through, but a quick bit of feedback

Please don't rename the define for the output feature, it's just an unnecessary breaking change. Now we are on V4 for all builds I'll probably be removing the DMX_INPUT as it's supposed on all esp32s and has minimal size impact

@netmindz netmindz added the connectivity Issue regarding protocols, WiFi connection or availability of interfaces label Apr 22, 2026
@netmindz netmindz self-assigned this Apr 22, 2026
@netmindz
Copy link
Copy Markdown
Member

Thanks for the PR @Mdbelen , can you clarify which MCUs you have tested with and have you used a DMX tester or logic analyzer to validate the output of your new driver?

Mdbelen added 2 commits April 23, 2026 21:21
…TPUT"

This reverts commit bc80c4a.
It does keep the PinManager name change from DMX to DMX_OUTPUT!
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
wled00/e131.cpp (1)

119-121: ⚠️ Potential issue | 🔴 Critical

Use the protocol-specific payload offset when proxying DMX.

Art-Net payload starts at art_data[0], while E1.31 reserves property_values[0] for the start code. The unconditional &e131_data[1] shifts every Art-Net channel by one and reads past the payload on full 512-channel packets.

🐛 Suggested fix
   if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) {
-    dmxOutput.writeBytes(1, &e131_data[1], dmxChannels);
+    const uint16_t dmxDataOffset = (mde == REALTIME_MODE_ARTNET) ? 0 : 1;
+    dmxOutput.writeBytes(1, &e131_data[dmxDataOffset], dmxChannels);
     dmxOutput.update();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/e131.cpp` around lines 119 - 121, The proxying currently always uses
the E1.31 payload offset (&e131_data[1]) which misaligns Art‑Net packets
(art_data starts at art_data[0]) and can read past the buffer on 512‑channel
frames; update the proxy logic in the block checking e131ProxyUniverse/uni to
choose the correct pointer and offset based on the source protocol (use art_data
for Art‑Net starting at index 0, and e131_data starting at index 1 for E1.31)
when calling dmxOutput.writeBytes and ensure dmxChannels bounds are respected;
locate the branch around e131ProxyUniverse, uni, dmxOutput.writeBytes and adjust
to pass the protocol‑appropriate buffer pointer and length.
wled00/set.cpp (1)

477-479: ⚠️ Potential issue | 🟠 Major

Apply and validate the DMX TX pin change instead of only storing toInt().

toInt() turns empty/malformed input into 0, so this can silently lose the -1 disabled sentinel. More importantly, the live driver is only initialized once in WLED::setup() (wled00/wled.cpp), so saving a new pin here leaves DMX output bound to the old GPIO until reboot. Please validate the submitted value and either reinitialize dmxOutput here or force a reboot/apply step when the pin changes.

🔧 Suggested fix
 `#ifdef` WLED_ENABLE_DMX
-    dmxOutputPin = request->arg(F("IDMO")).toInt();
+    if (request->hasArg(F("IDMO"))) {
+      const int newPin = request->arg(F("IDMO")).toInt();
+      if (newPin >= -1 && newPin < WLED_NUM_PINS && newPin != dmxOutputPin) {
+        dmxOutputPin = newPin;
+        doReboot = true; // or rebind dmxOutput here
+      }
+    }
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/set.cpp` around lines 477 - 479, The code assigns dmxOutputPin =
request->arg(F("IDMO")).toInt() which silently converts empty/malformed input to
0 and loses the -1 sentinel, and it only updates the stored value without
reinitializing the DMX driver (initialized in WLED::setup()), so changes don't
take effect until reboot; fix by parsing and validating the raw string from
request->arg(F("IDMO")) (preserve -1 as a valid "disabled" value and reject
non-numeric input), update the stored dmxOutputPin only if the parsed value is
valid, and when the pin actually changes either reinitialize the DMX output
driver immediately (call the driver init/reset function used in WLED::setup() —
reference the same DMX init routine) or mark the config as needing an
apply/reboot and return a response instructing the user to reboot/apply.
wled00/wled.cpp (1)

571-572: ⚠️ Potential issue | 🟠 Major

Reserve the configured DMX GPIO before buses and usermods claim it.

dmxOutput.init() is the first PinManager allocation for the TX pin, but it runs after beginStrip() and usermod setup. If either path grabs the configured GPIO first, DMX output init fails on boot even though the setting is valid.

🔧 Suggested fix
   DEBUG_PRINTLN(F("Reading config"));
   bool needsCfgSave = deserializeConfigFromFS();
   DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
+
+#ifdef WLED_ENABLE_DMX
+  dmxOutput.init(dmxOutputPin, 43);
+#endif
 
 `#if` defined(STATUSLED) && STATUSLED>=0
   if (!PinManager::isPinAllocated(STATUSLED)) {
-#ifdef WLED_ENABLE_DMX
-  dmxOutput.init(dmxOutputPin, 43);
-#endif
 `#ifdef` WLED_ENABLE_DMX_INPUT
   dmxInput.init(dmxInputReceivePin, dmxInputTransmitPin, dmxInputEnablePin, dmxInputPort);
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/wled.cpp` around lines 571 - 572, The DMX TX pin is being allocated
too late (dmxOutput.init is called after beginStrip() and usermod setup), so
other buses/usermods can claim it first; move the reservation earlier by
reserving the configured DMX GPIO with the PinManager before any bus or usermod
claims it (either call dmxOutput.init(dmxOutputPin, 43) before
beginStrip()/usermod setup or explicitly call the PinManager reserve API for the
TX pin—e.g., pinManager.reservePin(dmxOutputPin, /*owner*/ DMX_TX) —so the pin
is locked for DMX use prior to beginStrip() and usermod initialization).
🧹 Nitpick comments (4)
wled00/dmx_output.cpp (2)

157-167: Consider integer math for timeToNextUpdate().

The float division + conversion + ceil-rounding can be expressed as pure integer math ((1000 + _updateRate - 1) / _updateRate) which avoids pulling the softfloat path on ESP8266 and is simpler to reason about. This function is called on every loop iteration.

♻️ Proposed refactor
-  // 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);
+  // treat _updateRate as maximum, so round up the refresh delay
+  const int dmxFrameTime = (1000 + _updateRate - 1) / _updateRate;
+  return dmxFrameTime - (int)(millis() - _lastDmxOutMillis);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 157 - 167, The timeToNextUpdate()
function uses float math to compute frame delay which pulls in softfloat on
ESP8266 and is inefficient; replace the float/ceil logic with integer arithmetic
by computing dmxFrameTime = (1000 + _updateRate - 1) / _updateRate (handle
_updateRate==0 earlier as already done), then return dmxFrameTime - (millis() -
_lastDmxOutMillis) while keeping the existing early checks for _uartNo < 0 and
_updateRate == 0; update only DMXOutput::timeToNextUpdate() and keep references
to _updateRate, _lastDmxOutMillis, _uartNo and millis() intact.

5-10: uart.h include no longer needed.

After inlining the FIFO check with USS()/USTXC (line 131), only esp8266_peri.h is actually used from these two headers; uart.h can be dropped from the ESP8266 branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 5 - 10, Remove the now-unneeded uart.h
include from the ESP8266 branch in dmx_output.cpp: keep only `#include`
"esp8266_peri.h" under the `#ifdef` ESP8266 conditional since the FIFO check was
inlined using USS()/USTXC; delete the `#include` "uart.h" line and ensure the
conditional block still correctly selects esp8266_peri.h for ESP8266 and
hal/uart_ll.h for other platforms.
wled00/dmx_output.h (2)

53-92: Mark getter/query methods const.

read, readBytes, getLastDmxOut, getUpdateRate, busy, and timeToNextUpdate don't mutate instance state and should be const, matching the project guideline and making them callable from const contexts. (readBytes currently takes a non-const out-buffer — that's fine, only the method itself needs const qualification.)

♻️ Proposed fix
-    uint8_t read(uint16_t channel);
+    uint8_t read(uint16_t channel) const;
...
-    bool readBytes(uint16_t channelStart, uint8_t values[], uint16_t len);
+    bool readBytes(uint16_t channelStart, uint8_t values[], uint16_t len) const;
...
-    unsigned long getLastDmxOut();
+    unsigned long getLastDmxOut() const;
...
-    bool busy();
+    bool busy() const;
...
-    uint8_t getUpdateRate();
+    uint8_t getUpdateRate() const;
...
-    int timeToNextUpdate();
+    int timeToNextUpdate() const;

(Remember to mirror the const on the corresponding definitions in dmx_output.cpp.)

As per coding guidelines: "Mark getter/query methods as const and use static for methods not accessing instance state".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.h` around lines 53 - 92, Several accessor/query methods in
the DMX output class are not marked const; change the declarations of
read(uint16_t channel), readBytes(uint16_t channelStart, uint8_t values[],
uint16_t len), getLastDmxOut(), getUpdateRate(), busy(), and timeToNextUpdate()
to be const methods (e.g., uint8_t read(uint16_t channel) const), and update the
matching definitions in dmx_output.cpp to include the same const qualifier so
they are callable from const contexts and adhere to the project guideline.

24-30: Prefer constexpr over #define for these compile-time constants.

DMX_CHANNEL_TOP, DMX_CHANNELS, DMXSPEED, BREAKSPEED are plain integer constants and don't need preprocessor scoping. DMXFORMAT/BREAKFORMAT resolve to SERIAL_8N2/SERIAL_8N1 which are uint32_t enum-ish values — also representable as constexpr. This gives them type safety and keeps them out of the global macro namespace (where DMX_CHANNELS risks collision with the DMXChannels global variable used in handleDMXOutput()).

♻️ Proposed refactor
-#define DMX_CHANNEL_TOP 512
-#define DMX_CHANNELS (DMX_CHANNEL_TOP + 1)
-
-#define DMXSPEED       250000
-#define DMXFORMAT      SERIAL_8N2
-#define BREAKSPEED     83000
-#define BREAKFORMAT    SERIAL_8N1   // unused, instead, DMXFORMAT is used
+constexpr uint16_t DMX_CHANNEL_TOP = 512;
+constexpr uint16_t DMX_CHANNELS    = DMX_CHANNEL_TOP + 1;
+constexpr uint32_t DMXSPEED        = 250000;
+constexpr uint32_t BREAKSPEED      = 83000;
+constexpr auto     DMXFORMAT       = SERIAL_8N2;
+constexpr auto     BREAKFORMAT     = SERIAL_8N1;   // unused, instead, DMXFORMAT is used

As per coding guidelines: "Prefer constexpr over #define for compile-time constants in C++".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.h` around lines 24 - 30, Replace the preprocessor macros
with typed constexpr variables: change DMX_CHANNEL_TOP, DMX_CHANNELS, DMXSPEED,
BREAKSPEED to constexpr integers (e.g., int or std::size_t where appropriate)
and change DMXFORMAT and BREAKFORMAT to constexpr uint32_t (or the exact enum
type that SERIAL_8N2/SERIAL_8N1 resolve to); update any includes if needed
(e.g., <cstdint>) and ensure references such as DMXChannels in handleDMXOutput()
still compile (avoiding macro collisions) by removing the `#define` lines and
declaring the new constexpr symbols with the same names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/dmx_output.cpp`:
- Line 131: The declaration of uart_tx_fifo_available (using USS(_uartNo) >>
USTXC) is missing its terminating semicolon which causes compilation failure;
update the statement in dmx_output.cpp where uart_tx_fifo_available is defined
(referencing the symbols uart_tx_fifo_available, USS, USTXC, _uartNo) by adding
the missing semicolon to the end of the line so the local variable declaration
is properly terminated.
- Around line 63-72: The end() method in DMXOutput leaks the TX pin because
init() allocates it via PinManager::allocatePin(_outputPin, true,
PinOwner::DMX_OUTPUT) but end() never releases it; modify DMXOutput::end() to
call PinManager::deallocatePin(_outputPin, PinOwner::DMX_OUTPUT) (or the
appropriate deallocation API) before clearing _outputPin/_uartNo and deleting
_dmxSerial, and also ensure any failure paths in DMXOutput::init() deallocate
the pin if allocation succeeded but initialization failed so the pin is not
permanently owned by DMX_OUTPUT.

In `@wled00/dmx_output.h`:
- Around line 94-101: The member _updateRate of class DMXOutput is left
uninitialized which can cause undefined behavior if accessed before init(); set
a safe in-class initializer for _updateRate (for example 0) in the DMXOutput
declaration so any global/default-constructed DMXOutput has a well-defined
value; update the member declaration for _updateRate and ensure callers such as
timeToNextUpdate() and init() rely on that initialized value.

---

Duplicate comments:
In `@wled00/e131.cpp`:
- Around line 119-121: The proxying currently always uses the E1.31 payload
offset (&e131_data[1]) which misaligns Art‑Net packets (art_data starts at
art_data[0]) and can read past the buffer on 512‑channel frames; update the
proxy logic in the block checking e131ProxyUniverse/uni to choose the correct
pointer and offset based on the source protocol (use art_data for Art‑Net
starting at index 0, and e131_data starting at index 1 for E1.31) when calling
dmxOutput.writeBytes and ensure dmxChannels bounds are respected; locate the
branch around e131ProxyUniverse, uni, dmxOutput.writeBytes and adjust to pass
the protocol‑appropriate buffer pointer and length.

In `@wled00/set.cpp`:
- Around line 477-479: The code assigns dmxOutputPin =
request->arg(F("IDMO")).toInt() which silently converts empty/malformed input to
0 and loses the -1 sentinel, and it only updates the stored value without
reinitializing the DMX driver (initialized in WLED::setup()), so changes don't
take effect until reboot; fix by parsing and validating the raw string from
request->arg(F("IDMO")) (preserve -1 as a valid "disabled" value and reject
non-numeric input), update the stored dmxOutputPin only if the parsed value is
valid, and when the pin actually changes either reinitialize the DMX output
driver immediately (call the driver init/reset function used in WLED::setup() —
reference the same DMX init routine) or mark the config as needing an
apply/reboot and return a response instructing the user to reboot/apply.

In `@wled00/wled.cpp`:
- Around line 571-572: The DMX TX pin is being allocated too late
(dmxOutput.init is called after beginStrip() and usermod setup), so other
buses/usermods can claim it first; move the reservation earlier by reserving the
configured DMX GPIO with the PinManager before any bus or usermod claims it
(either call dmxOutput.init(dmxOutputPin, 43) before beginStrip()/usermod setup
or explicitly call the PinManager reserve API for the TX pin—e.g.,
pinManager.reservePin(dmxOutputPin, /*owner*/ DMX_TX) —so the pin is locked for
DMX use prior to beginStrip() and usermod initialization).

---

Nitpick comments:
In `@wled00/dmx_output.cpp`:
- Around line 157-167: The timeToNextUpdate() function uses float math to
compute frame delay which pulls in softfloat on ESP8266 and is inefficient;
replace the float/ceil logic with integer arithmetic by computing dmxFrameTime =
(1000 + _updateRate - 1) / _updateRate (handle _updateRate==0 earlier as already
done), then return dmxFrameTime - (millis() - _lastDmxOutMillis) while keeping
the existing early checks for _uartNo < 0 and _updateRate == 0; update only
DMXOutput::timeToNextUpdate() and keep references to _updateRate,
_lastDmxOutMillis, _uartNo and millis() intact.
- Around line 5-10: Remove the now-unneeded uart.h include from the ESP8266
branch in dmx_output.cpp: keep only `#include` "esp8266_peri.h" under the `#ifdef`
ESP8266 conditional since the FIFO check was inlined using USS()/USTXC; delete
the `#include` "uart.h" line and ensure the conditional block still correctly
selects esp8266_peri.h for ESP8266 and hal/uart_ll.h for other platforms.

In `@wled00/dmx_output.h`:
- Around line 53-92: Several accessor/query methods in the DMX output class are
not marked const; change the declarations of read(uint16_t channel),
readBytes(uint16_t channelStart, uint8_t values[], uint16_t len),
getLastDmxOut(), getUpdateRate(), busy(), and timeToNextUpdate() to be const
methods (e.g., uint8_t read(uint16_t channel) const), and update the matching
definitions in dmx_output.cpp to include the same const qualifier so they are
callable from const contexts and adhere to the project guideline.
- Around line 24-30: Replace the preprocessor macros with typed constexpr
variables: change DMX_CHANNEL_TOP, DMX_CHANNELS, DMXSPEED, BREAKSPEED to
constexpr integers (e.g., int or std::size_t where appropriate) and change
DMXFORMAT and BREAKFORMAT to constexpr uint32_t (or the exact enum type that
SERIAL_8N2/SERIAL_8N1 resolve to); update any includes if needed (e.g.,
<cstdint>) and ensure references such as DMXChannels in handleDMXOutput() still
compile (avoiding macro collisions) by removing the `#define` lines and declaring
the new constexpr symbols with the same names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 86e36cfb-18e2-4b1e-b3d5-f9e4693dc53f

📥 Commits

Reviewing files that changed from the base of the PR and between f2edd8d and 8b8cc07.

📒 Files selected for processing (11)
  • wled00/cfg.cpp
  • wled00/data/settings_dmx.htm
  • wled00/data/settings_sync.htm
  • wled00/dmx_input.cpp
  • wled00/dmx_output.cpp
  • wled00/dmx_output.h
  • wled00/e131.cpp
  • wled00/set.cpp
  • wled00/wled.cpp
  • wled00/wled.h
  • wled00/xml.cpp
✅ Files skipped from review due to trivial changes (2)
  • wled00/data/settings_dmx.htm
  • wled00/cfg.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • wled00/data/settings_sync.htm

Comment thread wled00/dmx_output.cpp
Comment thread wled00/dmx_output.cpp Outdated
Comment thread wled00/dmx_output.h
Comment on lines +94 to +101
private:
HardwareSerial* _dmxSerial;
uint8_t _outputPin; // DMX TX pin
uint8_t _dmxData[DMX_CHANNELS] = {0};
int8_t _uartNo = -1;
uint8_t _updateRate;
unsigned long _lastDmxOutMillis = 0;
};
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

Initialize _updateRate in the member declaration.

_updateRate has no in-class initializer, so a default-constructed global DMXOutput dmxOutput leaves it indeterminate until init() is called. timeToNextUpdate() guards via _uartNo < 0 today, but any future call path that reads _updateRate before init() would hit UB. Trivial to make watertight:

♻️ Proposed fix
     int8_t _uartNo = -1;
-    uint8_t _updateRate;
+    uint8_t _updateRate = 43;
     unsigned long _lastDmxOutMillis = 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.h` around lines 94 - 101, The member _updateRate of class
DMXOutput is left uninitialized which can cause undefined behavior if accessed
before init(); set a safe in-class initializer for _updateRate (for example 0)
in the DMXOutput declaration so any global/default-constructed DMXOutput has a
well-defined value; update the member declaration for _updateRate and ensure
callers such as timeToNextUpdate() and init() rely on that initialized value.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
wled00/dmx_output.cpp (2)

159-169: Optional: integer-only ceiling math in timeToNextUpdate().

fdmxFrameTime / cast / fractional check can be replaced with a single integer ceiling expression, avoiding the FP divide/subtract on every call (this is invoked from the hot service path via update()).

♻️ Proposed refactor
-  // 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);
+  // treat _updateRate as maximum, so round up the refresh delay
+  const int dmxFrameTime = (1000 + _updateRate - 1) / _updateRate;   // integer ceil(1000 / _updateRate)
+  return dmxFrameTime - (int)(millis() - _lastDmxOutMillis);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 159 - 169, Replace the floating-point
ceiling logic in DMXOutput::timeToNextUpdate with integer-only math: compute the
frame period as an integer ceiling using (_updateRate) and integer arithmetic
(e.g., (1000 + _updateRate - 1) / _updateRate) instead of using fdmxFrameTime,
casting and fractional checks; keep the same early returns for _uartNo and
_updateRate and return the same expression dmxFrameTime - (millis() -
_lastDmxOutMillis) using the new integer dmxFrameTime variable so the hot path
avoids FP divide/subtract.

148-157: Mark pure getters as const.

getLastDmxOut() and getUpdateRate() don't modify instance state and should be const; same applies to timeToNextUpdate() (Line 159) — it only reads _uartNo, _updateRate, _lastDmxOutMillis. The header declaration will need to match.

-unsigned long DMXOutput::getLastDmxOut() {
+unsigned long DMXOutput::getLastDmxOut() const {
   return _lastDmxOutMillis;
 }
...
-uint8_t DMXOutput::getUpdateRate() {
+uint8_t DMXOutput::getUpdateRate() const {
   return _updateRate;
 }

As per coding guidelines: "Mark getter/query methods as const and use static for methods not accessing instance state".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 148 - 157, The getters getLastDmxOut(),
getUpdateRate() and the method timeToNextUpdate() are pure queries and should be
marked const; update their definitions in the .cpp (DMXOutput::getLastDmxOut(),
DMXOutput::getUpdateRate(), DMXOutput::timeToNextUpdate()) to end with const and
make the matching declarations in the header const as well, and if you find any
method that does not access instance state at all consider marking it static;
ensure signatures in both header and implementation match to fix
const-correctness.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@wled00/dmx_output.cpp`:
- Around line 159-169: Replace the floating-point ceiling logic in
DMXOutput::timeToNextUpdate with integer-only math: compute the frame period as
an integer ceiling using (_updateRate) and integer arithmetic (e.g., (1000 +
_updateRate - 1) / _updateRate) instead of using fdmxFrameTime, casting and
fractional checks; keep the same early returns for _uartNo and _updateRate and
return the same expression dmxFrameTime - (millis() - _lastDmxOutMillis) using
the new integer dmxFrameTime variable so the hot path avoids FP divide/subtract.
- Around line 148-157: The getters getLastDmxOut(), getUpdateRate() and the
method timeToNextUpdate() are pure queries and should be marked const; update
their definitions in the .cpp (DMXOutput::getLastDmxOut(),
DMXOutput::getUpdateRate(), DMXOutput::timeToNextUpdate()) to end with const and
make the matching declarations in the header const as well, and if you find any
method that does not access instance state at all consider marking it static;
ensure signatures in both header and implementation match to fix
const-correctness.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dcc131bf-6314-4747-87d7-5da36ca2d87b

📥 Commits

Reviewing files that changed from the base of the PR and between 8b8cc07 and 8c658ce.

📒 Files selected for processing (1)
  • wled00/dmx_output.cpp

@Mdbelen
Copy link
Copy Markdown
Author

Mdbelen commented Apr 23, 2026

I've changed back to WLED_ENABLE_DMX without _OUTPUT as requested. Note that I kept the DMX_OUTPUT naming with PinManager. Hope that's okay.
I've flashed this to an ESP32 on a custom board and made some oscilloscope measurements. I have no DMX fixtures here, but at another location. Could probably test that on the weekend.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented Apr 23, 2026

As this moves away from any library (which I am not at all opposed to), it requires thorough testing on all supported platforms.

@Mdbelen
Copy link
Copy Markdown
Author

Mdbelen commented Apr 23, 2026

True in general. As I said in the initial message, the principle is the same between SparkFunDMX, DMXESPOutput (used for ESP8266) and still very similar to this module.

void SparkFunDMX::update() {
  if (_READWRITE == _WRITE)
  {
    //Send DMX break
    digitalWrite(txPin, HIGH);
    DMXSerial.begin(BREAKSPEED, BREAKFORMAT, rxPin, txPin);//Begin the Serial port
    DMXSerial.write(0);
    DMXSerial.flush();
    delay(1);
    DMXSerial.end();

    //Send DMX data
    DMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin);//Begin the Serial port
    DMXSerial.write(dmxData, chanSize);
    DMXSerial.flush();
    DMXSerial.end();//clear our DMX array, end the Hardware Serial port
  }
void DMXESPSerial::update() {
  if (dmxStarted == false) init();

  //Send break
  digitalWrite(sendPin, HIGH);
  Serial1.begin(BREAKSPEED, BREAKFORMAT);
  Serial1.write(0);
  Serial1.flush();
  delay(1);
  Serial1.end();

  //send data
  Serial1.begin(DMXSPEED, DMXFORMAT);
  digitalWrite(sendPin, LOW);
  Serial1.write(dmxDataStore, channelSize);
  Serial1.flush();
  delay(1);
  Serial1.end();
}
  // 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);

The difference in mine is that i set _dmxSerial->setTxBufferSize(DMX_CHANNELS + SOC_UART_FIFO_LEN); and don't flush the buffer after writing the DMX data. Then I use low level API access to check whether the transfer has finished before allowing the next DMX frame to be sent and reject calls to update() in between, if _updateRate is set to 0 (max speed). Or I use the _updateRate setting to throttle the frames.
I would hope that the HardwareSerial and Arduino libs are the same for all ESP32xx variants(?) so they'd all be checked if mine is running. ESP8266 is a different story.
I've just build esp32S3_wroom2, lolin_s2_mini and esp32c3dev variants to check whether I'd get any incompatible API errors or warnings. This is not the case.

Maybe if I find time, I'll add more UI improvements to allow the TX pin to be changed without rebooting. And add an input field to set the DMXOutput serial port (like with DMXInput) and update rate.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented Apr 23, 2026

The difference in mine is that i set _dmxSerial->setTxBufferSize(DMX_CHANNELS + SOC_UART_FIFO_LEN); and don't flush the buffer after writing the DMX data.

In my experience each and every ESP can act differently as the hardware implementations are not the same and they do have quirks, so testing is absolutely required.

@Mdbelen
Copy link
Copy Markdown
Author

Mdbelen commented Apr 26, 2026

Successfully tested WLED as Art-Net to DMX with ESP32WROVER and Cameo and Eurolight fixtures.

@nmeurer
Copy link
Copy Markdown

nmeurer commented Apr 29, 2026

Successfully tested this on esp32c3dev (Pin 5) using a SN75176B IC and a Eurolite fixture

@softhack007
Copy link
Copy Markdown
Member

@coderabbitai please compare the new implementation in this PR with the previous code in main, especially ESPDMX and SparkfunDMX (send mode). Is the previous behaviour fully included in the new implementation, including special handling of channel0?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get new implementation
echo "=== NEW: dmx_output.h ==="
cat wled00/dmx_output.h

echo ""
echo "=== NEW: dmx_output.cpp ==="
cat wled00/dmx_output.cpp

echo ""
echo "=== OLD (main): ESPDMX.cpp ==="
git show origin/main:wled00/src/dependencies/dmx/ESPDMX.cpp 2>/dev/null || echo "NOT FOUND on main"

echo ""
echo "=== OLD (main): ESPDMX.h ==="
git show origin/main:wled00/src/dependencies/dmx/ESPDMX.h 2>/dev/null || echo "NOT FOUND on main"

echo ""
echo "=== OLD (main): SparkFunDMX.cpp ==="
git show origin/main:wled00/src/dependencies/dmx/SparkFunDMX.cpp 2>/dev/null || echo "NOT FOUND on main"

echo ""
echo "=== OLD (main): SparkFunDMX.h ==="
git show origin/main:wled00/src/dependencies/dmx/SparkFunDMX.h 2>/dev/null || echo "NOT FOUND on main"

echo ""
echo "=== OLD (main): wled.h DMX section ==="
git show origin/main:wled00/wled.h 2>/dev/null | grep -A 20 "WLED_ENABLE_DMX"

echo ""
echo "=== OLD (main): handleDMXOutput in dmx_output.cpp ==="
git show origin/main:wled00/dmx_output.cpp 2>/dev/null || echo "NOT FOUND on main"

Length of output: 27401


@softhack007 Here is a detailed comparison of the new DMXOutput implementation against the old ESPDMX (ESP8266) and SparkFunDMX (ESP32) send paths:


✅ Behaviors preserved

  • LED-to-DMX channel mapping (handleDMXOutput): All 7 fixture map cases (0=off, 1=R, 2=G, 3=B, 4=W, 5=shutter, 6=255), brightness calculation, shutter-disable logic, and proxy universe guard are functionally identical.
  • Channel 0 as DMX start code: _dmxData[0] is zero-initialized and nothing in handleDMXOutput or writeBytes ever writes to it, so it remains 0x00 — the correct standard DMX null start code — by default.
  • Break + data sequence structure: Send a break byte at lower baud rate, then the full DMX frame at 250000 baud — same logical flow.

⚠️ Behavioral differences / potential issues

1. Break byte serial format — protocol compliance concern

This is the most significant issue. The old implementations both used begin(BREAKSPEED, BREAKFORMAT) where BREAKFORMAT = SERIAL_8N1 for the break byte. The new implementation uses only updateBaudRate(BREAKSPEED), which keeps the existing port format DMXFORMAT = SERIAL_8N2 for the break byte. BREAKFORMAT is defined in dmx_output.h but explicitly documented as "unused":

`#define` BREAKFORMAT    SERIAL_8N1   // unused, instead, DMXFORMAT is used

This means the break byte is sent at 83000 baud with 2 stop bits instead of 1. The resulting break pulse timing will differ from what the old code produced — this could affect DMX standard compliance and receiver compatibility, especially with strict DMX decoders.

2. No digitalWrite(txPin, HIGH) before break

Old SparkFunDMX::update() explicitly drove the TX pin HIGH before reconfiguring the UART for the break. The new code omits this. This could affect the Mark Before Break (MBB) period on some hardware configurations.

3. Channel 0 (start code) not enforced in write()

Old SparkFunDMX::write() always executed dmxData[0] = 0 — every single channel write unconditionally reset the start code byte. The new DMXOutput::write() allows write(0, nonzero) to change the start code. While writeBytes() correctly blocks channelStart == 0, users calling write(0, x) directly could inadvertently corrupt the start code. The comment on writeBytes() mentions this is intentional to allow start-code access via write(), but it's a behavioral change worth noting.

4. ESP8266: Channel 512 was never sent in old code — now fixed

Old ESPDMX::update() called Serial1.write(dmxDataStore, channelSize) where channelSize = 512. Since dmxDataStore[0] is the start code, this actually sent: start code + channels 1–511. Channel 512 was silently dropped on ESP8266. The new implementation always sends all 513 bytes, fixing this.

5. Break speed: 83333 → 83000 baud

Minor change, well within DMX standard tolerance (break must be ≥92µs; at 83000 baud a single byte with 8N1 framing is ~120µs).

6. delay(1) removed — blocking behavior eliminated

Old code had delay(1) after both the break and (ESP8266 only) after the data write. The new implementation replaces this with flush() (only after break) + the busy() check in update(). This is the core of the non-blocking improvement but changes timing margins.

7. Dynamic chanSize growth removed

Old SparkFunDMX::write() would grow chanSize if Channel > chanSize. The new implementation has a fixed 513-byte buffer with hard bounds. This is an improvement for predictability but is a behavioral change.

8. Universe overflow protection added in handleDMXOutput

Old code had no guard against writing past DMX channel 512. New code adds:

uint16_t maxLen = (DMX_CHANNELS - DMXStart) / DMXGap;
if (len > maxLen) len = maxLen;

This is an improvement, and is likely part of the fix for issue #5133.


Summary table

Aspect Old ESPDMX (ESP8266) Old SparkFunDMX (ESP32) New DMXOutput
Break format SERIAL_8N1 ✅ SERIAL_8N1 ✅ SERIAL_8N2 ⚠️
Break baud 83333 83333 83000
TX pin HIGH before break Yes Yes No
Blocking per frame ~23ms ~23ms ~150µs (non-blocking)
Channel 512 sent No (bug) Yes Yes ✅
Start code enforcement Zero-init only dmxData[0]=0 on every write() Zero-init only
Universe overflow guard No No Yes ✅
Dynamic channel count Yes Yes No (fixed 513)

The break format issue (point 1) is the most critical one worth verifying on an oscilloscope — does the break pulse duration and framing match what DMX receivers expect?


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/bus_manager.cpp:549-556
Timestamp: 2026-02-11T16:20:02.872Z
Learning: ESP-IDF V5 requires a complete rewrite of the LEDC (PWM/analog LED) subsystem in WLED. The framework now manages LEDC channels internally (unlike V4 where WLED managed them directly), and dithering functionality may not work as it did before. Direct LEDC struct access (e.g., `LEDC.channel_group[gr].channel[ch].duty.duty`) used in the current implementation will need to be replaced with proper HAL API calls. This affects `wled00/bus_manager.cpp` BusPwm class and related pin manager LEDC allocation code.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR `#4876`, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T20:46:14.014Z
Learning: In WLED (wled00/wled.cpp), on ESP32 with arduino-esp32 v2.0.x (IDF V4 branch), calling `pinMode(hardwareRX, INPUT_PULLDOWN)` has an unexpected side effect: it routes the RX pin through the GPIO matrix instead of keeping it on UART0's IOMUX path, effectively detaching the pin from the UART unit and causing serial receive to stop working (issue `#5501`). The correct way to enable a pull-down on the hardware RX pin without disrupting the UART mapping is to call `gpio_pulldown_en((gpio_num_t)hardwareRX)` directly from the ESP-IDF GPIO driver (`driver/gpio.h`). This is the fix applied in PR `#5503`. Do NOT use `pinMode()` to set pull resistors on UART RX/TX pins on ESP32.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: wled00/const.h:90-102
Timestamp: 2026-04-20T19:04:17.626Z
Learning: In WLED (wled00/const.h), ESP-IDF v5.3's `soc/soc_caps.h` defines `SOC_RMT_TX_CANDIDATES_PER_GROUP` which matches exactly the WLED_MAX_RMT_CHANNELS hardcoded values per target: esp32=8, esp32s2=4, esp32s3=4, esp32c3=2, esp32c6=2, esp32p4=4. This macro could replace the per-target `#elif` chain for WLED_MAX_RMT_CHANNELS using `SOC_RMT_TX_CANDIDATES_PER_GROUP * SOC_RMT_GROUPS`. However, SOC_I2S_NUM (hardware peripheral count: 2/1/2/1/1/3) cannot replace WLED_MAX_I2S_CHANNELS, which represents NPB parallel DMA output capability (8/8/8/0/0/?) and is a library-support concept, not a hardware count. The soc_caps.h header is not officially stable public API but has been consistent in practice since IDF 4.4.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Additionally, upstream connectedhomeip TypeTraits.h already has the fix built-in. ESP-IDF v5.5 uses GCC 14.2.0. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: Mdbelen
Repo: wled/WLED PR: 5522
File: wled00/dmx_output.cpp:5-9
Timestamp: 2026-04-21T23:32:50.241Z
Learning: In WLED's `dmx_output.cpp` (ESP8266 path), `uart_tx_fifo_available()` is declared `inline __attribute__((always_inline))` in the ESP8266 Arduino core's `uart.cpp` and is NOT exported through any header. The workaround used is `#include "uart.cpp"` directly, which works because the function is always_inline (no ODR violation for that symbol). A cleaner alternative is to reproduce the one-liner body inline using the `USS()` and `USTXC` macros from `esp8266_peri.h`. Do not flag `#include "uart.cpp"` as an unconditional build failure for this file — it is a known limitation of the ESP8266 core's API surface.

@softhack007
Copy link
Copy Markdown
Member

@coderabbitai the previous implementation in main had a problem due to missing rate limiting - handleDmxOutput() was called on each main loop iteration, regardless of the fact that DMX fixtures usually cannot handle more than ~40Hz refresh rate. This lead to serial buffer congestion and very bad overall response time, see #5133 (comment). The suggested solution was to add a simple rate limiter in handleDmxOutput().

Is the refresh rate limit problem addressed properly in this PR?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== dmx_output.h ==="
cat wled00/dmx_output.h

echo ""
echo "=== dmx_output.cpp ==="
cat wled00/dmx_output.cpp

Length of output: 10899


@softhack007 Yes, the rate-limiting problem is properly addressed. Here's the detailed breakdown:


Dual-gate in update()

if ((timeToNextUpdate() <= 0) && !busy()) {
    // send frame
}

Two independent conditions both must be true before a frame is sent:

  1. Time gate (timeToNextUpdate() <= 0): enforces the configured Hz cap. At the default 43 Hz, this computes ceil(1000/43) = 24 ms as the minimum inter-frame interval.
  2. TX-idle gate (!busy()): prevents starting a new frame while the previous one is still clocking out of the UART, which was the direct cause of serial buffer congestion in the old code.

Because && short-circuits, if the time gate is not met, busy() is never evaluated — meaning on ESP8266, the delayMicroseconds(~45 µs) inside busy() is never reached for premature calls. On ESP32, it's a single register read at worst.

Non-blocking on ESP32

The TX ring buffer sized at DMX_CHANNELS + SOC_UART_FIFO_LEN (641 bytes) is the key enabler:

_dmxSerial->setTxBufferSize(DMX_CHANNELS + SOC_UART_FIFO_LEN);

The 513-byte _dmxData write returns in ~150 µs (only the BREAK byte blocks via flush()). The rest of the frame clocks out in the background via DMA/FIFO, while the main loop is free. The uart_ll_is_tx_idle() check in busy() accurately reflects when the hardware is done.

Rate settable to 0 = max speed

When _updateRate == 0, timeToNextUpdate() returns -1 (always ≤ 0), so transmission is limited only by the busy() check — effectively as fast as the hardware allows without congestion.

handleDMXOutput() in the main loop — safe

Since handleDMXOutput() just writes channels and calls update() at the end, it can be called every main loop iteration at full WLED loop frequency. The overhead when not time to send is essentially just:

  • One timeToNextUpdate() call (integer arithmetic, millis() read)
  • Short-circuit exit

One minor note on timestamp semantics

_lastDmxOutMillis is stamped at the start of transmission (before the break byte), not at completion. For 43 Hz, the full frame takes ~22.7 ms to clock out, so the next slot opens at ~24 ms elapsed — giving only ~1.3 ms of margin. The busy() check covers this gap, so there's no actual issue, but the effective achieved rate will be slightly below 43 Hz, which is the correct conservative behavior for DMX fixtures.


Bottom line: The refresh rate limiting is properly implemented and directly addresses the serial buffer congestion and loop-blocking described in issue #5133.


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/bus_manager.cpp:549-556
Timestamp: 2026-02-11T16:20:02.872Z
Learning: ESP-IDF V5 requires a complete rewrite of the LEDC (PWM/analog LED) subsystem in WLED. The framework now manages LEDC channels internally (unlike V4 where WLED managed them directly), and dithering functionality may not work as it did before. Direct LEDC struct access (e.g., `LEDC.channel_group[gr].channel[ch].duty.duty`) used in the current implementation will need to be replaced with proper HAL API calls. This affects `wled00/bus_manager.cpp` BusPwm class and related pin manager LEDC allocation code.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: DedeHai
Repo: wled/WLED PR: 4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR `#4876`, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Learnt from: Mdbelen
Repo: wled/WLED PR: 5522
File: wled00/dmx_output.cpp:5-9
Timestamp: 2026-04-21T23:32:50.241Z
Learning: In WLED's `dmx_output.cpp` (ESP8266 path), `uart_tx_fifo_available()` is declared `inline __attribute__((always_inline))` in the ESP8266 Arduino core's `uart.cpp` and is NOT exported through any header. The workaround used is `#include "uart.cpp"` directly, which works because the function is always_inline (no ODR violation for that symbol). A cleaner alternative is to reproduce the one-liner body inline using the `USS()` and `USTXC` macros from `esp8266_peri.h`. Do not flag `#include "uart.cpp"` as an unconditional build failure for this file — it is a known limitation of the ESP8266 core's API surface.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: DedeHai
Repo: wled/WLED PR: 4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-25T12:25:11.610Z
Learning: In WLED's `Segment::startTransition()` (wled00/FX_fcn.cpp:284), when `isInTransition()` is true and `_t->_oldSegment` already exists, the function silently returns without updating `_t->_start` or `_t->_bri`. This causes a bug where rapid successive on/off toggles during a non-FADE blending transition (e.g., fairy dust) leave the transition clock stale: by the time of the second re-trigger, elapsed time may already exceed `_dur`, so `updateTransitionProgress()` sets `_progress = 0xFFFF` immediately on the next service tick and `stopTransition()` fires — the blending effect never plays. The fix is to always reset `_t->_start = millis()`, `_t->_dur = dur`, and `_t->_bri = currentBri()` (current visible brightness) in the `isInTransition()` branch, regardless of whether `_oldSegment` exists.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5109
File: wled00/FX.cpp:3174-3343
Timestamp: 2025-11-27T06:33:11.436Z
Learning: WLED Ants effect (wled00/FX.cpp): The author prefers the current velocity initialization using hw_random16(1000, 5000)/5000.0f, resulting in an effective range of ~3.6–10.0 (with VELOCITY_MIN=2.0, VELOCITY_MAX=10.0), and wants the code kept as-is with comments updated to document this behavior. Avoid suggesting changes to span the full 2.0–10.0 range in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-08T00:57:36.134Z
Learning: In WLED (wled00/cfg.cpp), `deserializeConfig()` is called with an empty JsonObject when cfg.json doesn't exist on fresh install. Any value read without the ArduinoJSON `|` fallback operator or CJSON macro will override correct constructor defaults with 0/null. Known affected values: `strip.setTargetFps(hw_led["fps"])` at line 179 (sets FPS to 0/unlimited instead of WLED_FPS=42). Fix: `strip.setTargetFps(hw_led["fps"] | WLED_FPS)`. The gamma issue (gammaCorrectCol/Bri) had the same root cause and was fixed in commit d1d9dec402 (Jan 2026) using inline `| default` fallbacks.

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Additionally, upstream connectedhomeip TypeTraits.h already has the fix built-in. ESP-IDF v5.5 uses GCC 14.2.0. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T20:46:14.014Z
Learning: In WLED (wled00/wled.cpp), on ESP32 with arduino-esp32 v2.0.x (IDF V4 branch), calling `pinMode(hardwareRX, INPUT_PULLDOWN)` has an unexpected side effect: it routes the RX pin through the GPIO matrix instead of keeping it on UART0's IOMUX path, effectively detaching the pin from the UART unit and causing serial receive to stop working (issue `#5501`). The correct way to enable a pull-down on the hardware RX pin without disrupting the UART mapping is to call `gpio_pulldown_en((gpio_num_t)hardwareRX)` directly from the ESP-IDF GPIO driver (`driver/gpio.h`). This is the fix applied in PR `#5503`. Do NOT use `pinMode()` to set pull resistors on UART RX/TX pins on ESP32.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

@softhack007
Copy link
Copy Markdown
Member

@Mdbelen please check if any of the differences listed by the rabbit in #5522 (comment) would need a fix in your new code.

@softhack007
Copy link
Copy Markdown
Member

@coderabbitai please check if this PR implies other behavioral changes (user-visible and not yet covered by the PR description) or ripple effects that should be considered by reviewers.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== wled00/cfg.cpp DMX section ==="
grep -n -A 10 -B 5 "dmxOutputPin\|dmxOutput\|WLED_ENABLE_DMX" wled00/cfg.cpp

echo ""
echo "=== wled00/const.h DEFAULT_LED_PIN ==="
grep -n -A 5 -B 5 "DEFAULT_LED_PIN\|WLED_ENABLE_DMX" wled00/const.h | head -60

echo ""
echo "=== wled00/wled.cpp DMX section ==="
grep -n -A 5 -B 3 "dmxOutput\|initDMX\|handleDMX\|DMX" wled00/wled.cpp

echo ""
echo "=== wled00/set.cpp DMX section ==="
grep -n -A 5 -B 3 "dmxOutput\|DMX\|IDMO\|IDMI" wled00/set.cpp

echo ""
echo "=== wled00/xml.cpp DMX section ==="
grep -n -A 5 -B 3 "dmxOutput\|DMX\|IDMO\|hideNoDMX" wled00/xml.cpp

echo ""
echo "=== wled00/e131.cpp DMX section ==="
grep -n -A 10 -B 5 "dmxOutput\|dmx\." wled00/e131.cpp | head -80

echo ""
echo "=== wled00/dmx_input.cpp WLED_ENABLE_DMX section ==="
grep -n -A 5 -B 3 "WLED_ENABLE_DMX" wled00/dmx_input.cpp | head -40

echo ""
echo "=== wled00/fcn_declare.h DMX section ==="
grep -n -A 2 -B 2 "DMX\|dmx" wled00/fcn_declare.h

echo ""
echo "=== wled00/wled.h DMX section ==="
grep -n -A 10 -B 2 "WLED_ENABLE_DMX\|dmxOutput\|dmxInput" wled00/wled.h | head -80

echo ""
echo "=== wled00/dmx_output.cpp - init and handleDMXOutput full ==="
cat wled00/dmx_output.cpp

echo ""
echo "=== wled00/dmx_output.h full ==="
cat wled00/dmx_output.h

echo ""
echo "=== Check if initDMXInput / handleDMXInput are still declared anywhere ==="
grep -rn "initDMXInput\|handleDMXInput\|initDMXOutput\|handleDMXOutput" wled00/

echo ""
echo "=== wled00/dmx_input.cpp - check init and handle function ==="
grep -n "void initDMXInput\|void handleDMXInput\|void DMXInput" wled00/dmx_input.cpp | head -20

echo ""
echo "=== Check usermods for DMX usage ==="
grep -rn "dmx\.\|WLED_ENABLE_DMX\|initDMXOutput\|handleDMXOutput\|DMXESPSerial\|SparkFunDMX" usermods/ 2>/dev/null | head -40

echo ""
echo "=== platformio.ini DMX-related build flags ==="
grep -n "DMX\|ESPDMX\|SparkFun" platformio.ini | head -40

echo ""
echo "=== Check if cfg.cpp reads/writes dmxOutputPin with fallback ==="
grep -n "dmxOutputPin" wled00/cfg.cpp

echo ""
echo "=== Check if dmxOutputPin is saved/loaded from cfg.json ==="
grep -rn "dmxOutputPin" wled00/

echo ""
echo "=== Check main branch dmx_output.cpp for old initDMXOutput ==="
git show origin/main:wled00/dmx_output.cpp 2>/dev/null | head -60

echo ""
echo "=== Check wled00/data/settings_sync.htm DMX section ==="
grep -n -A 5 -B 3 "dmxOutput\|hideNoDMX\|IDMO\|DMX" wled00/data/settings_sync.htm | head -80

Length of output: 34209


@softhack007 Here is a comprehensive list of behavioral changes and ripple effects in this PR that are not covered in the PR description, beyond what was already discussed:


🐛 1. dmxOutputPin defaults to 0 on fresh install (GPIO0 bug)

wled.h initializes dmxOutputPin to DMX_TXPIN_DEFAULT = -1 (disabled). However, in cfg.cpp Line 611:

CJSON(dmxOutputPin, if_live_dmx[F("dmxOutputPin")]);

CJSON is used without an | -1 fallback. ArduinoJSON returns 0 for a missing integer key, so on a fresh install (no cfg.json) or any upgrade from main where this key was never persisted, dmxOutputPin is set to 0 — and then dmxOutput.init(0, 43) at Line 572 of wled.cpp tries to claim GPIO0 for DMX output. On ESP8266 this will be rejected (pin ≠ 2 check), but on ESP32 GPIO0 is the boot strapping pin and should not be claimed. The fix is:

dmxOutputPin = if_live_dmx[F("dmxOutputPin")] | DMX_TXPIN_DEFAULT;

⚠️ 2. DEFAULT_LED_PIN migration for ESP8266 + DMX users

In main, DEFAULT_LED_PIN on ESP8266 with WLED_ENABLE_DMX was 1 (to avoid conflict with Serial1/GPIO2 used by ESPDMX). With this PR it is unconditionally 2. Since DMX output now requires an explicit pin configuration via the UI, GPIO2 is no longer reserved by default. This is architecturally correct, but existing ESP8266+DMX users upgrading will see their default LED pin silently change from 1→2 if they never explicitly set it — a config migration concern.


⚠️ 3. TX pin change requires reboot but no reboot warning is shown

In set.cpp Line 478, dmxOutputPin is updated in RAM. However dmxOutput.init() is not re-called with the new pin — the change only takes effect after a reboot. There is no rebootRequired = true call and no "Reboot required" warning in the settings_sync.htm UI next to the DMX TX pin field (unlike, for example, the "Reboot required" label beside the DMX Input pins). Users will change the pin in the UI, save, and see no change until they manually reboot.


⚠️ 4. Update rate hardcoded at 43 Hz — no UI to change it

wled.cpp calls dmxOutput.init(dmxOutputPin, 43) with a hardcoded 43 Hz. The class has setUpdateRate()/getUpdateRate() and even supports updateRate=0 for maximum speed, but there is no UI field or cfg.json persistence for this value. The PR description lists "update-rate limiting (update rate 0 = maximum)" as a feature, but it is functionally inaccessible to users in this implementation.


⚠️ 5. WLED_ENABLE_DMX_OUTPUT build flag removed — silent breakage for custom builds

The PR renames the define back to WLED_ENABLE_DMX as requested by netmindz. Any custom platformio.ini environment (external boards, CI pipelines, user configs) that used -D WLED_ENABLE_DMX_OUTPUT to enable DMX output will silently stop producing DMX output with no compile error. The old flag name simply won't match any #ifdef.


ℹ️ 6. E1.31/ArtNet → DMX proxy path is now rate-limited to 43 Hz

In e131.cpp, the proxy path now calls dmxOutput.update() which applies the 43 Hz rate gate. Previously dmx.update() (old SparkFunDMX) had no rate limiting and sent every packet immediately. This is an improvement (matches DMX fixture capabilities), but it is undocumented and could be noticed by integrators running high-frequency ArtNet sources.


ℹ️ 7. ESP8266: init() silently returns false for any pin other than GPIO2

The init code enforces UART1, pin 2 on ESP8266 and returns false otherwise with only a DEBUG_PRINTF message. If a user configures any other pin via the new UI on ESP8266, DMX output silently stops working with no user-visible error. A UI-level warning for ESP8266 constraints would be helpful.


ℹ️ 8. HardwareSerial instance is heap-allocated (new)

Old code used static Serial1 (ESP8266) or a file-scope HardwareSerial (SparkFunDMX). The new code does _dmxSerial = new HardwareSerial(uartNo), which adds one small heap allocation. This is not problematic in practice but is a style/memory-management change worth noting.


Summary table of new findings

# Category Severity Covered in PR desc?
1 dmxOutputPin → GPIO0 on fresh install Bug ❌ No
2 ESP8266 DEFAULT_LED_PIN 1→2 migration Migration ❌ No
3 No reboot warning for pin change in UI UX gap ❌ No
4 Update rate hardcoded, no UI to change Incomplete feature Partial
5 WLED_ENABLE_DMX_OUTPUT flag removed Breaking for custom builds ❌ No
6 E1.31 proxy rate-capped at 43 Hz Improvement ❌ No
7 ESP8266 pin ≠ 2 silently fails UX gap ❌ No
8 HardwareSerial heap-allocated Style change ❌ No

🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/bus_manager.cpp:549-556
Timestamp: 2026-02-11T16:20:02.872Z
Learning: ESP-IDF V5 requires a complete rewrite of the LEDC (PWM/analog LED) subsystem in WLED. The framework now manages LEDC channels internally (unlike V4 where WLED managed them directly), and dithering functionality may not work as it did before. Direct LEDC struct access (e.g., `LEDC.channel_group[gr].channel[ch].duty.duty`) used in the current implementation will need to be replaced with proper HAL API calls. This affects `wled00/bus_manager.cpp` BusPwm class and related pin manager LEDC allocation code.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR `#4876`, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Learnt from: DedeHai
Repo: wled/WLED PR: 4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: wled00/const.h:90-102
Timestamp: 2026-04-20T19:04:17.626Z
Learning: In WLED (wled00/const.h), ESP-IDF v5.3's `soc/soc_caps.h` defines `SOC_RMT_TX_CANDIDATES_PER_GROUP` which matches exactly the WLED_MAX_RMT_CHANNELS hardcoded values per target: esp32=8, esp32s2=4, esp32s3=4, esp32c3=2, esp32c6=2, esp32p4=4. This macro could replace the per-target `#elif` chain for WLED_MAX_RMT_CHANNELS using `SOC_RMT_TX_CANDIDATES_PER_GROUP * SOC_RMT_GROUPS`. However, SOC_I2S_NUM (hardware peripheral count: 2/1/2/1/1/3) cannot replace WLED_MAX_I2S_CHANNELS, which represents NPB parallel DMA output capability (8/8/8/0/0/?) and is a library-support concept, not a hardware count. The soc_caps.h header is not officially stable public API but has been consistent in practice since IDF 4.4.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:14.510Z
Learning: In WLED (wled00/util.cpp), `allocate_buffer()` processes `BFRALLOC_NOBYTEACCESS` in an `if` with an `else` that covers all other flags. On classic ESP32, this means `BFRALLOC_ENFORCE_PSRAM` and `BFRALLOC_PREFER_PSRAM` are silently ignored when `BFRALLOC_NOBYTEACCESS` is also set — IRAM (MALLOC_CAP_32BIT) is always tried first. Combining `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` is unsafe: the allocator's internal `memset` at util.cpp:901 will perform byte-level access on a potentially IRAM-resident buffer. This affects `Segment::pixels` in `setGeometry()` (FX.h) and `_pixels` in `finalizeInit()` (FX_fcn.cpp). Additionally, `memcpy` on `Segment::pixels` in the copy constructor and copy assignment (FX_fcn.cpp lines ~73 and ~116) is also a byte-level violation when the buffer is in IRAM. The safe pattern is element-wise 32-bit access (for loops over `uint32_t*`) rather than `memset`/`memcpy` for all `BFRALLOC_NOBYTEACCESS` buffers.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1304-1313
Timestamp: 2026-03-25T07:03:35.475Z
Learning: In WLED `mode_dissolveplus` (usermods/user_fx/user_fx.cpp), using `hw_random16(SEGLEN)` to select the survivor pixel index is correct and safe for this 1D-only effect. The 0xFFFF unmapped-entry concern from the physical bus mapping does not apply to 1D segments because virtual indices 0..SEGLEN-1 always map to valid physical LEDs without gaps. Do not flag this as a bug in future reviews of 1D effects.

Learnt from: CR
Repo: wled/WLED PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-04-12T07:15:56.657Z
Learning: Document changes in pull requests with clear description of what changed and why, including expected impact on user-visible behavior, related issue links, and screenshots for new features

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Additionally, upstream connectedhomeip TypeTraits.h already has the fix built-in. ESP-IDF v5.5 uses GCC 14.2.0. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T20:46:14.014Z
Learning: In WLED (wled00/wled.cpp), on ESP32 with arduino-esp32 v2.0.x (IDF V4 branch), calling `pinMode(hardwareRX, INPUT_PULLDOWN)` has an unexpected side effect: it routes the RX pin through the GPIO matrix instead of keeping it on UART0's IOMUX path, effectively detaching the pin from the UART unit and causing serial receive to stop working (issue `#5501`). The correct way to enable a pull-down on the hardware RX pin without disrupting the UART mapping is to call `gpio_pulldown_en((gpio_num_t)hardwareRX)` directly from the ESP-IDF GPIO driver (`driver/gpio.h`). This is the fix applied in PR `#5503`. Do NOT use `pinMode()` to set pull resistors on UART RX/TX pins on ESP32.

Learnt from: Mdbelen
Repo: wled/WLED PR: 5522
File: wled00/dmx_output.cpp:5-9
Timestamp: 2026-04-21T23:32:50.241Z
Learning: In WLED's `dmx_output.cpp` (ESP8266 path), `uart_tx_fifo_available()` is declared `inline __attribute__((always_inline))` in the ESP8266 Arduino core's `uart.cpp` and is NOT exported through any header. The workaround used is `#include "uart.cpp"` directly, which works because the function is always_inline (no ODR violation for that symbol). A cleaner alternative is to reproduce the one-liner body inline using the `USS()` and `USTXC` macros from `esp8266_peri.h`. Do not flag `#include "uart.cpp"` as an unconditional build failure for this file — it is a known limitation of the ESP8266 core's API surface.

Comment thread wled00/e131.cpp
for (uint16_t i = 1; i <= dmxChannels; i++)
dmx.write(i, e131_data[i]);
dmx.update();
dmxOutput.writeBytes(1, &e131_data[1], dmxChannels);
Copy link
Copy Markdown
Member

@softhack007 softhack007 Apr 30, 2026

Choose a reason for hiding this comment

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

Should this be e131_data[0] in Art-Net mode? Afaik the first channel offset is 0 for Art-Net, but 1 for other E131 protocols?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@coderabbitai please verify if my concern is correct here.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm how e131_data is assigned per protocol and confirm the Art-Net vs E1.31 offset difference
rg -n "e131_data\s*=" wled00/e131.cpp
rg -n "art_data\|property_values\|dataOffset\|REALTIME_MODE_ARTNET" wled00/e131.cpp | head -40

Length of output: 107


@softhack007, your concern is correct. Here is the analysis:

The e131_data pointer is assigned differently per protocol:

  • Art-Net (Line 93): e131_data = p->art_data; → DMX channel 1 lives at index 0
  • E1.31 (Line 103): e131_data = p->property_values; → index 0 is the zero start-code byte, DMX channel 1 lives at index 1

The proxy universe block:

dmxOutput.writeBytes(1, &e131_data[1], dmxChannels);

…passes &e131_data[1] unconditionally. That is correct for E1.31 but off by one for Art-Net: it silently drops DMX channel 1 and potentially reads one byte past the declared dmxChannels length.

However, this is a pre-existing bug, not a regression introduced by this PR. The old code had identical behaviour:

for (uint16_t i = 1; i <= dmxChannels; i++)
    dmx.write(i, e131_data[i]);  // same: starts at [1], skips channel 1 for Art-Net

The handleDMXData() function does compensate for the Art-Net offset via if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) dataOffset--;, but that adjustment is not applied in the proxy universe path.

A correct fix for this PR would be to mirror that adjustment in the proxy block:

`#ifdef` WLED_ENABLE_DMX
  // does not act on out-of-order packets yet
  if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) {
    // Art-Net data starts at index 0; E1.31 data starts at index 1 (index 0 is zero start-code)
    uint8_t* proxyData = (mde == REALTIME_MODE_ARTNET) ? &e131_data[0] : &e131_data[1];
    dmxOutput.writeBytes(1, proxyData, dmxChannels);
    dmxOutput.update();
  }
`#endif`

In summary: your concern is valid, the bug predates this PR, but this PR is the right place to fix it since the code is being reworked anyway.


🧠 Learnings used
Learnt from: DedeHai
Repo: wled/WLED PR: 4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR `#4876`, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Learnt from: netmindz
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-02-20T17:38:19.745Z
Learning: In WLED DDP packet handling (wled00/e131.cpp), the destination field should be validated to reject non-display packets. Packets with destination=DDP_ID_STATUS (251) or destination=DDP_ID_CONFIG (250) should be rejected and not processed as LED display data. Only packets with destination=DDP_ID_DISPLAY (1) should be accepted for LED output. WLED doesn't distinguish between multiple display IDs (e.g., display 1 vs display 2), but it must distinguish between DISPLAY packets versus STATUS/CONFIG packets.

Learnt from: softhack007
Repo: wled/WLED PR: 5547
File: wled00/e131.cpp:33-40
Timestamp: 2026-04-28T09:41:31.415Z
Learning: In WLED DDP packet handling (wled00/e131.cpp), the destination filtering uses an explicit deny-list (reject DDP_ID_CONTROL, DDP_ID_STATUS, DDP_ID_CONFIG) rather than an allow-list (only DDP_ID_DISPLAY). This is intentional for maximum compatibility — devices may use non-standard or future destination IDs that should still drive LED output. Do not suggest switching to an allow-list (`p->destination != DDP_ID_DISPLAY`) in future reviews of this code.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T09:36:13.994Z
Learning: WLED serial streaming frame maxima:
- Adalight: protocol max 65,536 LEDs (count = hi<<8 | lo, plus 1), but WLED’s uint16_t parser caps it at 65,535.
- TPM2 serial (RGB): protocol max payload 65,535 bytes → 21,845 LEDs (bytes/3); effective output always limited by configured LED count and MAX_LEDS.

Learnt from: willmmiles
Repo: wled/WLED PR: 4859
File: usermods/udp_name_sync/udp_name_sync.cpp:62-70
Timestamp: 2025-08-21T00:00:17.196Z
Learning: In WLED codebase, UDP packet processing code must never rely on null termination being present on the wire and must always validate packet size first. The codebase already has some vulnerabilities where malformed data can crash the software, so avoiding introduction of new ones is a priority.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/bus_manager.cpp:549-556
Timestamp: 2026-02-11T16:20:02.872Z
Learning: ESP-IDF V5 requires a complete rewrite of the LEDC (PWM/analog LED) subsystem in WLED. The framework now manages LEDC channels internally (unlike V4 where WLED managed them directly), and dithering functionality may not work as it did before. Direct LEDC struct access (e.g., `LEDC.channel_group[gr].channel[ch].duty.duty`) used in the current implementation will need to be replaced with proper HAL API calls. This affects `wled00/bus_manager.cpp` BusPwm class and related pin manager LEDC allocation code.

Learnt from: softhack007
Repo: wled/WLED PR: 5442
File: wled00/bus_manager.cpp:462-468
Timestamp: 2026-03-23T23:33:45.480Z
Learning: In WLED's TYPE_ANALOG_2CH_HBRIDGE (wled00/bus_manager.cpp), the dead zone at the CCT midpoint (level=127 and level=128 both producing _data[0]=0, _data[1]=0) is intentional by design. The map() blending is directional: WW channel (_data[0]) is brightest at level=0 and fades to 0 at level=127; CW channel (_data[1]) starts at 0 at level=128 and is brightest at level=255. The zero-output at the midpoint is the H-bridge polarity transition boundary, ensuring no overlap between the two polarity directions. Do not flag this dead zone as a bug in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: DedeHai
Repo: wled/WLED PR: 4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.

Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Additionally, upstream connectedhomeip TypeTraits.h already has the fix built-in. ESP-IDF v5.5 uses GCC 14.2.0. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T20:46:14.014Z
Learning: In WLED (wled00/wled.cpp), on ESP32 with arduino-esp32 v2.0.x (IDF V4 branch), calling `pinMode(hardwareRX, INPUT_PULLDOWN)` has an unexpected side effect: it routes the RX pin through the GPIO matrix instead of keeping it on UART0's IOMUX path, effectively detaching the pin from the UART unit and causing serial receive to stop working (issue `#5501`). The correct way to enable a pull-down on the hardware RX pin without disrupting the UART mapping is to call `gpio_pulldown_en((gpio_num_t)hardwareRX)` directly from the ESP-IDF GPIO driver (`driver/gpio.h`). This is the fix applied in PR `#5503`. Do NOT use `pinMode()` to set pull resistors on UART RX/TX pins on ESP32.

Learnt from: Mdbelen
Repo: wled/WLED PR: 5522
File: wled00/dmx_output.cpp:5-9
Timestamp: 2026-04-21T23:32:50.241Z
Learning: In WLED's `dmx_output.cpp` (ESP8266 path), `uart_tx_fifo_available()` is declared `inline __attribute__((always_inline))` in the ESP8266 Arduino core's `uart.cpp` and is NOT exported through any header. The workaround used is `#include "uart.cpp"` directly, which works because the function is always_inline (no ODR violation for that symbol). A cleaner alternative is to reproduce the one-liner body inline using the `USS()` and `USTXC` macros from `esp8266_peri.h`. Do not flag `#include "uart.cpp"` as an unconditional build failure for this file — it is a known limitation of the ESP8266 core's API surface.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

Learnt from: softhack007
Repo: wled/WLED PR: 5355
File: wled00/util.cpp:635-638
Timestamp: 2026-02-07T16:06:08.677Z
Learning: PSRAM-related compilation guards should enable PSRAM code only for ESP32 variants that actually include PSRAM: ESP32-C61, ESP32-C5, and ESP32-P4. Exclude ESP32-C3, ESP32-C6, and ESP8266 from these guards. Apply this rule across the codebase (not just wled00/util.cpp) by reviewing and updating PSRAM guards/macros in all relevant files (C/C++ headers and sources).

Learnt from: softhack007
Repo: wled/WLED PR: 4893
File: wled00/set.cpp:95-95
Timestamp: 2026-03-14T20:56:46.543Z
Learning: Guideline: Ensure WiFi hostname is set after WiFi.mode() but before WiFi.begin() to avoid default esp-XXXXXX hostname being used in DHCP. This ordering only takes effect after the STA interface exists (so avoid placing hostname setting before initConnection). In WLED, place the hostname configuration inside initConnection() (after WiFi.disconnect(true) and before WiFi.begin()) rather than in earlier boot code like deserializeConfig(). This rule should be applied in code reviews for WLED’s network initialization paths in wled00/*.cpp, and note that on ESP8266 the ordering is less strict but still desirable for consistency.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp:30-35
Timestamp: 2026-03-27T12:33:48.499Z
Learning: In C/C++ preprocessor conditionals (`#if`, `#elif`) GCC/Clang treat `&&` as short-circuit evaluated during preprocessing. This means guards like `#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)` are safe even if the macro/function-like macro on the RHS (e.g., `ESP_IDF_VERSION_VAL`) is not defined on some targets, because the RHS will not be expanded when the LHS is false (e.g., `defined(...)` evaluates to 0). During code review, avoid flagging such cases as “undefined function-like macro invocation” if they are protected by short-circuiting `defined(...) && ...`/`||` logic; some tools like cppcheck may not model this and can produce false positives. Also, don’t suggest refactoring that moves ESP32-specific includes/headers (e.g., `esp_idf_version.h`) outside of these guarded preprocessor blocks, since that will break targets (e.g., ESP8266) where the headers don’t exist.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:45.980Z
Learning: When calling raw lwIP APIs (e.g., around `ntpUdp.begin()` or any `lwIP`/`tcpip`-layer call) in this codebase on ESP32 Arduino-ESP32 platforms where core locking/checking is enabled, wrap the lwIP call with `LOCK_TCPIP_CORE()` / `UNLOCK_TCPIP_CORE()` from `lwip/tcpip.h`. This prevents thread-safety/core-violation crashes without requiring `sdkconfig` changes.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:45.980Z
Learning: When using lwIP “raw” APIs in WLED on ESP32 (Arduino-ESP32 / IDF 5.5+), don’t rely on LOCK_TCPIP_CORE()/UNLOCK_TCPIP_CORE() unless CONFIG_LWIP_TCPIP_CORE_LOCKING=y is guaranteed. CONFIG_LWIP_CHECK_THREAD_SAFETY=y can trigger the assertion “Required to lock TCPIP core functionality!” when raw lwIP calls occur off the TCPIP thread. If the locking mode isn’t enabled (or can’t be changed via sdkconfig), schedule lwIP work (e.g., ntpUdp.begin() and similar raw lwIP calls) via tcpip_callback() so it runs on the TCPIP thread; this should work regardless of the locking-mode setting. Review any similar raw lwIP usage for correct thread/context handling.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled.cpp:698-700
Timestamp: 2026-03-28T01:37:15.541Z
Learning: In this WLED codebase, when using `DEBUG_PRINTLN(F("..."))`, an explicit trailing `\n` inside the `F("...")` string (e.g., `DEBUG_PRINTLN(F("Warning!\n"))`) may be intentional to create a blank line in debug output as a visual separator. During code review, do not automatically flag these as “double newline” or recommend removing the `\n`—first verify with the author/context that the extra newline is deliberate.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:02.808Z
Learning: When working with WLED strings stored in PROGMEM on ESP8266/ESP32, assume you can’t use `strchr_P` for character scanning. If you need to scan a PROGMEM string for a character (e.g., searching for '@' within effect data), review changes to ensure they perform an explicit RAM copy first (e.g., via `strncpy_P` into a buffer) and then scan in RAM. Avoid proposing incremental/streaming PROGMEM character-by-character scans without a RAM buffer on these targets, since it’s impractical with the available APIs.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:09.816Z
Learning: In WLED’s WLED code, if a pixel/buffer allocation uses `BFRALLOC_NOBYTEACCESS` (and especially on classic ESP32 where byte-level access to IRAM-resident buffers is unsafe), avoid using byte-wise operations like `memset`/`memcpy` on that buffer. Specifically, do not combine `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` and do not perform `memcpy`/`memset` over `Segment::pixels` (e.g., in `setGeometry()`/`finalizeInit()` or copy ctor/assignment). Instead, use element-wise 32-bit access (loop over `uint32_t*` and assign/copy per element) to ensure 32-bit access only.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T09:14:29.218Z
Learning: In WLED, when reviewing ESP32 code that uses pixel buffers allocated with `BFRALLOC_NOBYTEACCESS` (e.g., `WS2812FX::_pixels` and `Segment::pixels`), treat `memset`/`memcpy` on those buffers as an intentionally low-risk, accepted pattern. These buffers are designed to be IRAM-safe for 32-bit-only access, and the Xtensa newlib assembly implementations perform 32-bit word stores for word-aligned, word-multiple-sized buffers (so the IRAM constraint is satisfied in practice). Do not flag `memset`/`memcpy` on `BFRALLOC_NOBYTEACCESS` pixel buffers as a critical bug; if any concern arises, downgrade severity to low and focus on potential future regressions rather than an immediate correctness violation.

Learnt from: softhack007
Repo: wled/WLED PR: 5508
File: wled00/FX.cpp:4137-4142
Timestamp: 2026-04-16T09:49:58.587Z
Learning: In WLED effect/overlay implementations (C++ under wled00), ensure effects operate through the SEGMENT.* read/write APIs for their own pixels/overlay state. Reserve global frame reads/writes like strip.getPixelColor* for copy_* style post-processing only. Do not manually composite/blend across lower layers inside the effect; segment-to-segment blending is handled later by WS2812FX::blendSegment(), so effects should produce their per-segment contributions without trying to composite with underlying layers themselves.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

However, this is a pre-existing bug, not a regression introduced by this PR.

As the PR is a major modernization/overhaul of previous code, I would like to see this old problem addressed, too.

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.

@softhack007, agreed — since this is a comprehensive overhaul, it's the right time to fix it. Here is the proposed change:

  • Fix the Art-Net vs E1.31 start-index mismatch in the proxy universe path in wled00/e131.cpp

🧠 Learnings used
Learnt from: DedeHai
Repo: wled/WLED PR: 4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR `#4876`, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Learnt from: netmindz
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-02-20T17:38:19.745Z
Learning: In WLED DDP packet handling (wled00/e131.cpp), the destination field should be validated to reject non-display packets. Packets with destination=DDP_ID_STATUS (251) or destination=DDP_ID_CONFIG (250) should be rejected and not processed as LED display data. Only packets with destination=DDP_ID_DISPLAY (1) should be accepted for LED output. WLED doesn't distinguish between multiple display IDs (e.g., display 1 vs display 2), but it must distinguish between DISPLAY packets versus STATUS/CONFIG packets.

Learnt from: softhack007
Repo: wled/WLED PR: 5547
File: wled00/e131.cpp:33-40
Timestamp: 2026-04-28T09:41:31.415Z
Learning: In WLED DDP packet handling (wled00/e131.cpp), the destination filtering uses an explicit deny-list (reject DDP_ID_CONTROL, DDP_ID_STATUS, DDP_ID_CONFIG) rather than an allow-list (only DDP_ID_DISPLAY). This is intentional for maximum compatibility — devices may use non-standard or future destination IDs that should still drive LED output. Do not suggest switching to an allow-list (`p->destination != DDP_ID_DISPLAY`) in future reviews of this code.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T09:36:13.994Z
Learning: WLED serial streaming frame maxima:
- Adalight: protocol max 65,536 LEDs (count = hi<<8 | lo, plus 1), but WLED’s uint16_t parser caps it at 65,535.
- TPM2 serial (RGB): protocol max payload 65,535 bytes → 21,845 LEDs (bytes/3); effective output always limited by configured LED count and MAX_LEDS.

Learnt from: willmmiles
Repo: wled/WLED PR: 4859
File: usermods/udp_name_sync/udp_name_sync.cpp:62-70
Timestamp: 2025-08-21T00:00:17.196Z
Learning: In WLED codebase, UDP packet processing code must never rely on null termination being present on the wire and must always validate packet size first. The codebase already has some vulnerabilities where malformed data can crash the software, so avoiding introduction of new ones is a priority.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/bus_manager.cpp:549-556
Timestamp: 2026-02-11T16:20:02.872Z
Learning: ESP-IDF V5 requires a complete rewrite of the LEDC (PWM/analog LED) subsystem in WLED. The framework now manages LEDC channels internally (unlike V4 where WLED managed them directly), and dithering functionality may not work as it did before. Direct LEDC struct access (e.g., `LEDC.channel_group[gr].channel[ch].duty.duty`) used in the current implementation will need to be replaced with proper HAL API calls. This affects `wled00/bus_manager.cpp` BusPwm class and related pin manager LEDC allocation code.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:09.816Z
Learning: In WLED’s WLED code, if a pixel/buffer allocation uses `BFRALLOC_NOBYTEACCESS` (and especially on classic ESP32 where byte-level access to IRAM-resident buffers is unsafe), avoid using byte-wise operations like `memset`/`memcpy` on that buffer. Specifically, do not combine `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` and do not perform `memcpy`/`memset` over `Segment::pixels` (e.g., in `setGeometry()`/`finalizeInit()` or copy ctor/assignment). Instead, use element-wise 32-bit access (loop over `uint32_t*` and assign/copy per element) to ensure 32-bit access only.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1304-1313
Timestamp: 2026-03-25T07:03:35.475Z
Learning: In WLED `mode_dissolveplus` (usermods/user_fx/user_fx.cpp), using `hw_random16(SEGLEN)` to select the survivor pixel index is correct and safe for this 1D-only effect. The 0xFFFF unmapped-entry concern from the physical bus mapping does not apply to 1D segments because virtual indices 0..SEGLEN-1 always map to valid physical LEDs without gaps. Do not flag this as a bug in future reviews of 1D effects.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T09:14:29.218Z
Learning: In WLED, when reviewing ESP32 code that uses pixel buffers allocated with `BFRALLOC_NOBYTEACCESS` (e.g., `WS2812FX::_pixels` and `Segment::pixels`), treat `memset`/`memcpy` on those buffers as an intentionally low-risk, accepted pattern. These buffers are designed to be IRAM-safe for 32-bit-only access, and the Xtensa newlib assembly implementations perform 32-bit word stores for word-aligned, word-multiple-sized buffers (so the IRAM constraint is satisfied in practice). Do not flag `memset`/`memcpy` on `BFRALLOC_NOBYTEACCESS` pixel buffers as a critical bug; if any concern arises, downgrade severity to low and focus on potential future regressions rather than an immediate correctness violation.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/set.cpp:551-555
Timestamp: 2026-03-27T21:00:25.902Z
Learning: In WLED PR `#5048` (wled00/set.cpp lines 551-555), the CONFIG_IDF_TARGET_ESP32C5 block that unconditionally forces ntpEnabled = false is an intentional **temporary hotfix** by softhack007 for a known ESP32-C5 crash: `assert failed: udp_new_ip_type udp.c:1278 (Required to lock TCPIP core functionality!)`. Do not flag this as a permanent design issue; the TODO comment in the code already notes it should be resolved properly once the underlying IDF/TCPIP bug on C5 is fixed. A future permanent solution should use a target capability flag rather than overwriting the user's setting.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-25T12:25:11.610Z
Learning: In WLED's `Segment::startTransition()` (wled00/FX_fcn.cpp:284), when `isInTransition()` is true and `_t->_oldSegment` already exists, the function silently returns without updating `_t->_start` or `_t->_bri`. This causes a bug where rapid successive on/off toggles during a non-FADE blending transition (e.g., fairy dust) leave the transition clock stale: by the time of the second re-trigger, elapsed time may already exceed `_dur`, so `updateTransitionProgress()` sets `_progress = 0xFFFF` immediately on the next service tick and `stopTransition()` fires — the blending effect never plays. The fix is to always reset `_t->_start = millis()`, `_t->_dur = dur`, and `_t->_bri = currentBri()` (current visible brightness) in the `isInTransition()` branch, regardless of whether `_oldSegment` exists.

Learnt from: blazoncek
Repo: wled/WLED PR: 5140
File: wled00/data/settings_time.htm:66-76
Timestamp: 2025-12-01T07:01:16.949Z
Learning: In WLED PR `#5134`, the fix for macros being initialized with the enable bit set only handles new configurations, not existing ones. If there is a bug in timer/macro handling code (e.g., in settings_time.htm), it must be fixed to work correctly for existing configurations as well.

Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: DedeHai
Repo: wled/WLED PR: 4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Additionally, upstream connectedhomeip TypeTraits.h already has the fix built-in. ESP-IDF v5.5 uses GCC 14.2.0. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T20:46:14.014Z
Learning: In WLED (wled00/wled.cpp), on ESP32 with arduino-esp32 v2.0.x (IDF V4 branch), calling `pinMode(hardwareRX, INPUT_PULLDOWN)` has an unexpected side effect: it routes the RX pin through the GPIO matrix instead of keeping it on UART0's IOMUX path, effectively detaching the pin from the UART unit and causing serial receive to stop working (issue `#5501`). The correct way to enable a pull-down on the hardware RX pin without disrupting the UART mapping is to call `gpio_pulldown_en((gpio_num_t)hardwareRX)` directly from the ESP-IDF GPIO driver (`driver/gpio.h`). This is the fix applied in PR `#5503`. Do NOT use `pinMode()` to set pull resistors on UART RX/TX pins on ESP32.

Learnt from: Mdbelen
Repo: wled/WLED PR: 5522
File: wled00/dmx_output.cpp:5-9
Timestamp: 2026-04-21T23:32:50.241Z
Learning: In WLED's `dmx_output.cpp` (ESP8266 path), `uart_tx_fifo_available()` is declared `inline __attribute__((always_inline))` in the ESP8266 Arduino core's `uart.cpp` and is NOT exported through any header. The workaround used is `#include "uart.cpp"` directly, which works because the function is always_inline (no ODR violation for that symbol). A cleaner alternative is to reproduce the one-liner body inline using the `USS()` and `USTXC` macros from `esp8266_peri.h`. Do not flag `#include "uart.cpp"` as an unconditional build failure for this file — it is a known limitation of the ESP8266 core's API surface.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

Learnt from: softhack007
Repo: wled/WLED PR: 5355
File: wled00/util.cpp:635-638
Timestamp: 2026-02-07T16:06:08.677Z
Learning: PSRAM-related compilation guards should enable PSRAM code only for ESP32 variants that actually include PSRAM: ESP32-C61, ESP32-C5, and ESP32-P4. Exclude ESP32-C3, ESP32-C6, and ESP8266 from these guards. Apply this rule across the codebase (not just wled00/util.cpp) by reviewing and updating PSRAM guards/macros in all relevant files (C/C++ headers and sources).

Learnt from: softhack007
Repo: wled/WLED PR: 4893
File: wled00/set.cpp:95-95
Timestamp: 2026-03-14T20:56:46.543Z
Learning: Guideline: Ensure WiFi hostname is set after WiFi.mode() but before WiFi.begin() to avoid default esp-XXXXXX hostname being used in DHCP. This ordering only takes effect after the STA interface exists (so avoid placing hostname setting before initConnection). In WLED, place the hostname configuration inside initConnection() (after WiFi.disconnect(true) and before WiFi.begin()) rather than in earlier boot code like deserializeConfig(). This rule should be applied in code reviews for WLED’s network initialization paths in wled00/*.cpp, and note that on ESP8266 the ordering is less strict but still desirable for consistency.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp:30-35
Timestamp: 2026-03-27T12:33:48.499Z
Learning: In C/C++ preprocessor conditionals (`#if`, `#elif`) GCC/Clang treat `&&` as short-circuit evaluated during preprocessing. This means guards like `#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)` are safe even if the macro/function-like macro on the RHS (e.g., `ESP_IDF_VERSION_VAL`) is not defined on some targets, because the RHS will not be expanded when the LHS is false (e.g., `defined(...)` evaluates to 0). During code review, avoid flagging such cases as “undefined function-like macro invocation” if they are protected by short-circuiting `defined(...) && ...`/`||` logic; some tools like cppcheck may not model this and can produce false positives. Also, don’t suggest refactoring that moves ESP32-specific includes/headers (e.g., `esp_idf_version.h`) outside of these guarded preprocessor blocks, since that will break targets (e.g., ESP8266) where the headers don’t exist.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:45.980Z
Learning: When calling raw lwIP APIs (e.g., around `ntpUdp.begin()` or any `lwIP`/`tcpip`-layer call) in this codebase on ESP32 Arduino-ESP32 platforms where core locking/checking is enabled, wrap the lwIP call with `LOCK_TCPIP_CORE()` / `UNLOCK_TCPIP_CORE()` from `lwip/tcpip.h`. This prevents thread-safety/core-violation crashes without requiring `sdkconfig` changes.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:45.980Z
Learning: When using lwIP “raw” APIs in WLED on ESP32 (Arduino-ESP32 / IDF 5.5+), don’t rely on LOCK_TCPIP_CORE()/UNLOCK_TCPIP_CORE() unless CONFIG_LWIP_TCPIP_CORE_LOCKING=y is guaranteed. CONFIG_LWIP_CHECK_THREAD_SAFETY=y can trigger the assertion “Required to lock TCPIP core functionality!” when raw lwIP calls occur off the TCPIP thread. If the locking mode isn’t enabled (or can’t be changed via sdkconfig), schedule lwIP work (e.g., ntpUdp.begin() and similar raw lwIP calls) via tcpip_callback() so it runs on the TCPIP thread; this should work regardless of the locking-mode setting. Review any similar raw lwIP usage for correct thread/context handling.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled.cpp:698-700
Timestamp: 2026-03-28T01:37:15.541Z
Learning: In this WLED codebase, when using `DEBUG_PRINTLN(F("..."))`, an explicit trailing `\n` inside the `F("...")` string (e.g., `DEBUG_PRINTLN(F("Warning!\n"))`) may be intentional to create a blank line in debug output as a visual separator. During code review, do not automatically flag these as “double newline” or recommend removing the `\n`—first verify with the author/context that the extra newline is deliberate.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:02.808Z
Learning: When working with WLED strings stored in PROGMEM on ESP8266/ESP32, assume you can’t use `strchr_P` for character scanning. If you need to scan a PROGMEM string for a character (e.g., searching for '@' within effect data), review changes to ensure they perform an explicit RAM copy first (e.g., via `strncpy_P` into a buffer) and then scan in RAM. Avoid proposing incremental/streaming PROGMEM character-by-character scans without a RAM buffer on these targets, since it’s impractical with the available APIs.

Learnt from: softhack007
Repo: wled/WLED PR: 5508
File: wled00/FX.cpp:4137-4142
Timestamp: 2026-04-16T09:49:58.587Z
Learning: In WLED effect/overlay implementations (C++ under wled00), ensure effects operate through the SEGMENT.* read/write APIs for their own pixels/overlay state. Reserve global frame reads/writes like strip.getPixelColor* for copy_* style post-processing only. Do not manually composite/blend across lower layers inside the effect; segment-to-segment blending is handled later by WS2812FX::blendSegment(), so effects should produce their per-segment contributions without trying to composite with underlying layers themselves.

Failed to handle agent chat message. Please try again.

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.

Is there any bug report about this? I kept everything as-is because I figured this would lead to a misalignment by one channel for Art-Net (or E1.31) and somebody would have reported for sure. So I thought e131.cpp handles this somewhere internally by biasing the channels for me already.
Will look into it.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@Mdbelen
Copy link
Copy Markdown
Author

Mdbelen commented Apr 30, 2026

This btw also fixes the bug covered in #5316 .

@softhack007 Thanks for chasing the rabbit though this PR! Here's some comments to the findings:

1. Break byte serial format — protocol compliance concern

According to wikipedia, DMX defines only minimum times for break and MAB. As noted in comment on dmx_output.cpp:114
"Cannot change UART format while running. End and reinit takes much longer than the additional stopbit here." keeping the UART format at 8N2 looked to be faster at oscilloscope captures than calling end() and init() which is needed to change the format.

2. No digitalWrite(txPin, HIGH) before break
Old SparkFunDMX::update() explicitly drove the TX pin HIGH before reconfiguring the UART for the break. The new code omits this. This could affect the Mark Before Break (MBB) period on some hardware configurations.

As the new implementation keeps the UART "alive" all the time and never releases the pin to GPIO, this is unnecessary in all cases.
Besides, I unnecessarily do write the pin to high in init().^^

5. Break speed: 83333 → 83000 baud

I tried to do a real pessimistic implementation, so I did a 0,5% increase in break time here to account for possible crystal drifts 🙃 (this is calculated from the wikipedia's 120us, as before).

1. dmxOutputPin defaults to 0 on fresh install (GPIO0 bug)

I believe this bug is untrue, as dmxOutputPin is initialized to -1 in wled.h and the CJSON macro will do the suggested handling already. Can you confirm this @softhack007 @coderabbitai ?

2. DEFAULT_LED_PIN migration for ESP8266 + DMX users

I didn't remember changing anything about LED pins haha. Turns out this comes from @netmindz cherry-picked commit. AI comment makes sense to me, so I'll revert those lines, k? ESP8266 can only TX DMX (Serial1) on pin 2.

3. TX pin change requires reboot but no reboot warning is shown
4. Update rate hardcoded at 43 Hz — no UI to change it

True, planned to add to a future release. Will probably add to this PR very soon

IMO that covers all important AI findings.

@coderabbitai

This comment was marked as duplicate.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Tip

For best results, initiate chat on the files or code changes.

@Mdbelen — confirmed, the GPIO0 bug report was a false alarm on my part. Apologies for the noise.

The CJSON macro expands to:

`#define` CJSON(a,b) a = b | a
// i.e.:
dmxOutputPin = if_live_dmx[F("dmxOutputPin")] | dmxOutputPin;

The ArduinoJSON | operator returns the right-hand side (the current value of a) when the JSON key is absent or null. Since dmxOutputPin is initialized to -1 in wled.h, it safely stays -1 on a fresh install with no cfg.json. No GPIO0 claim occurs at startup.

Here's the final status summary of all findings:

# Finding Resolution
Break format (8N2) DMX only specifies minimum times; keeping UART alive is faster than end()/init() ✅ Intentional, well-reasoned
MBB digitalWrite UART keeps pin ownership; GPIO drive not needed ✅ Correct
83000 baud Intentional 0.5% pessimism for crystal drift tolerance ✅ Fine
GPIO0 on fresh install False alarmCJSON macro already falls back to the initialized -1 ❌ My mistake
ESP8266 DEFAULT_LED_PIN 1→2 Reverting the cherry-picked change makes sense; GPIO2 is fixed for Serial1/DMX on ESP8266 📝 Planned revert
Reboot warning for TX pin Acknowledged 📝 Planned for this PR
Update rate UI Acknowledged 📝 Planned for this PR
#5316 fix Good to document in PR description ℹ️ Noted

✏️ Learnings added
Learnt from: Mdbelen
URL: https://github.com/wled/WLED/pull/5522

Timestamp: 2026-04-30T21:36:15.410Z
Learning: In WLED PR `#5522` (dmx_output.cpp), the DMX BREAK byte is intentionally sent at SERIAL_8N2 format (same as the DMX data frame) rather than switching to SERIAL_8N1. This is because changing UART format requires end()/init() which is much slower than the cost of the extra stop bit. The DMX standard only specifies *minimum* times for break and MAB, so the extra stop bit at 83000 baud keeps the break duration compliant (>120µs) while keeping the UART "alive" throughout. Do not flag this as a protocol compliance violation.

Learnt from: Mdbelen
URL: https://github.com/wled/WLED/pull/5522

Timestamp: 2026-04-30T21:36:15.410Z
Learning: In WLED PR `#5522` (dmx_output.cpp), the new DMXOutput implementation does NOT call `digitalWrite(txPin, HIGH)` before the BREAK byte, unlike the old SparkFunDMX. This is intentional: the UART is kept initialized at all times (never end()/begin()), so the TX pin remains under UART control throughout and the Mark Before Break (MBB) is provided by the UART's idle-high state between frames. The old `digitalWrite` was only needed because SparkFunDMX called end() after each frame, releasing the pin to GPIO mode.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/bus_manager.cpp:549-556
Timestamp: 2026-02-11T16:20:02.872Z
Learning: ESP-IDF V5 requires a complete rewrite of the LEDC (PWM/analog LED) subsystem in WLED. The framework now manages LEDC channels internally (unlike V4 where WLED managed them directly), and dithering functionality may not work as it did before. Direct LEDC struct access (e.g., `LEDC.channel_group[gr].channel[ch].duty.duty`) used in the current implementation will need to be replaced with proper HAL API calls. This affects `wled00/bus_manager.cpp` BusPwm class and related pin manager LEDC allocation code.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR `#4876`, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Learnt from: DedeHai
Repo: wled/WLED PR: 4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: wled00/const.h:90-102
Timestamp: 2026-04-20T19:04:17.626Z
Learning: In WLED (wled00/const.h), ESP-IDF v5.3's `soc/soc_caps.h` defines `SOC_RMT_TX_CANDIDATES_PER_GROUP` which matches exactly the WLED_MAX_RMT_CHANNELS hardcoded values per target: esp32=8, esp32s2=4, esp32s3=4, esp32c3=2, esp32c6=2, esp32p4=4. This macro could replace the per-target `#elif` chain for WLED_MAX_RMT_CHANNELS using `SOC_RMT_TX_CANDIDATES_PER_GROUP * SOC_RMT_GROUPS`. However, SOC_I2S_NUM (hardware peripheral count: 2/1/2/1/1/3) cannot replace WLED_MAX_I2S_CHANNELS, which represents NPB parallel DMA output capability (8/8/8/0/0/?) and is a library-support concept, not a hardware count. The soc_caps.h header is not officially stable public API but has been consistent in practice since IDF 4.4.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-08T00:57:36.134Z
Learning: In WLED (wled00/cfg.cpp), `deserializeConfig()` is called with an empty JsonObject when cfg.json doesn't exist on fresh install. Any value read without the ArduinoJSON `|` fallback operator or CJSON macro will override correct constructor defaults with 0/null. Known affected values: `strip.setTargetFps(hw_led["fps"])` at line 179 (sets FPS to 0/unlimited instead of WLED_FPS=42). Fix: `strip.setTargetFps(hw_led["fps"] | WLED_FPS)`. The gamma issue (gammaCorrectCol/Bri) had the same root cause and was fixed in commit d1d9dec402 (Jan 2026) using inline `| default` fallbacks.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:14.510Z
Learning: In WLED (wled00/util.cpp), `allocate_buffer()` processes `BFRALLOC_NOBYTEACCESS` in an `if` with an `else` that covers all other flags. On classic ESP32, this means `BFRALLOC_ENFORCE_PSRAM` and `BFRALLOC_PREFER_PSRAM` are silently ignored when `BFRALLOC_NOBYTEACCESS` is also set — IRAM (MALLOC_CAP_32BIT) is always tried first. Combining `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` is unsafe: the allocator's internal `memset` at util.cpp:901 will perform byte-level access on a potentially IRAM-resident buffer. This affects `Segment::pixels` in `setGeometry()` (FX.h) and `_pixels` in `finalizeInit()` (FX_fcn.cpp). Additionally, `memcpy` on `Segment::pixels` in the copy constructor and copy assignment (FX_fcn.cpp lines ~73 and ~116) is also a byte-level violation when the buffer is in IRAM. The safe pattern is element-wise 32-bit access (for loops over `uint32_t*`) rather than `memset`/`memcpy` for all `BFRALLOC_NOBYTEACCESS` buffers.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1304-1313
Timestamp: 2026-03-25T07:03:35.475Z
Learning: In WLED `mode_dissolveplus` (usermods/user_fx/user_fx.cpp), using `hw_random16(SEGLEN)` to select the survivor pixel index is correct and safe for this 1D-only effect. The 0xFFFF unmapped-entry concern from the physical bus mapping does not apply to 1D segments because virtual indices 0..SEGLEN-1 always map to valid physical LEDs without gaps. Do not flag this as a bug in future reviews of 1D effects.

Learnt from: DedeHai
Repo: wled/WLED PR: 4615
File: wled00/FXparticleSystem.cpp:678-680
Timestamp: 2026-03-29T06:10:40.732Z
Learning: In WLED's FXparticleSystem.cpp, `gamma8inv()` is a lookup-table call that applies the inverse of the gamma curve — it is NOT a no-op (identity function) when `gammaCorrectCol` is false. All calls to `gamma8inv()` on particle brightness values (pxlbrightness[] in renderParticle 2D standard render, the large-particle ellipse path in renderLargeParticle 2D, and pxlbrightness[] in renderParticle 1D) must be guarded by `if (gammaCorrectCol)` to avoid pre-distorting brightness when gamma correction is disabled. Without the guard, particles render noticeably darker/nonlinear in gamma-disabled configurations because the inverse correction is applied but no forward gamma correction follows in show().

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Additionally, upstream connectedhomeip TypeTraits.h already has the fix built-in. ESP-IDF v5.5 uses GCC 14.2.0. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: softhack007
Repo: wled/WLED PR: 5456
File: platformio.ini:794-830
Timestamp: 2026-03-31T13:42:00.444Z
Learning: In WLED PR `#5456` (Matter over WiFi usermod, CMakeLists.txt + platformio.ini): The GCC 14 chip::to_underlying compatibility issue with CHIP SDK's TypeTraits.h is fixed by building in gnu++20 mode (not gnu++2b/gnu++23). CMakeLists.txt uses `idf_build_replace_option_from_property` to swap `-std=gnu++2b` for `-std=gnu++20` when the matter usermod is present. The `matter_gcc14_compat.h` shim file (which pre-defines `chip::to_underlying` and sets `CHIP_TO_UNDERLYING_DEFINED`) is dead code under this configuration — it is never included anywhere. TypeTraits.h's broken C++23 `using std::to_underlying` alias path is only taken in gnu++23 mode; in gnu++20 mode CHIP defines its own `chip::to_underlying` function template normally, so no shim is needed. Do NOT flag the missing `-include` for `matter_gcc14_compat.h` as a build issue.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/set.cpp:551-555
Timestamp: 2026-03-27T21:00:25.902Z
Learning: In WLED PR `#5048` (wled00/set.cpp lines 551-555), the CONFIG_IDF_TARGET_ESP32C5 block that unconditionally forces ntpEnabled = false is an intentional **temporary hotfix** by softhack007 for a known ESP32-C5 crash: `assert failed: udp_new_ip_type udp.c:1278 (Required to lock TCPIP core functionality!)`. Do not flag this as a permanent design issue; the TODO comment in the code already notes it should be resolved properly once the underlying IDF/TCPIP bug on C5 is fixed. A future permanent solution should use a target capability flag rather than overwriting the user's setting.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled_metadata.cpp:6-8
Timestamp: 2026-03-27T21:02:06.756Z
Learning: In WLED PR `#5048` (pio-scripts/set_metadata.py + wled00/wled_metadata.cpp): The hardcoded `#define WLED_VERSION 16.0.0-alphaV5` in `wled_metadata.cpp` is an intentional **temporary hotfix** by softhack007. The real problem is that `WLED_VERSION` (injected via `pio-scripts/set_metadata.py` as a CPPDEFINE) is not reaching `wled_metadata.cpp` at compile time. The set_metadata.py change in this PR switched from `env.Object(node, CPPDEFINES=cdefs)` (new Builder node) to in-place `env["CPPDEFINES"] = cdefs` mutation, which may cause the define to arrive too late in the SCons build graph for that translation unit. The TODO comment in the code already marks this for removal. Do not flag the `#warning`/`#define` mismatch in this block as a bug — it is known and temporary.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T09:14:32.589Z
Learning: In WLED, pixel buffers allocated with `BFRALLOC_NOBYTEACCESS` (both `WS2812FX::_pixels` and `Segment::pixels`) are intentionally placed in 32-bit-only accessible IRAM by design. Buffers up to ~1000 LEDs (~4 kB) easily fit within IRAM's ~50 kB pool. `memset` and `memcpy` on these buffers work correctly in practice because the Xtensa newlib assembly implementations use 32-bit stores for word-aligned, word-multiple-size buffers — satisfying the IRAM hardware constraint as an implementation property of the assembly code, which is unlikely to change across IDF versions. The risk of a future violation exists but is accepted as very low. Do not flag `memset`/`memcpy` usage on `BFRALLOC_NOBYTEACCESS` pixel buffers as a critical bug in WLED reviews; note it as a low-risk accepted pattern.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:51.985Z
Learning: In WLED PR `#5048` (wled00/cfg.cpp), the NTP crash on ESP32-C5 (pioarduino + IDF 5.5.x) is caused by both CONFIG_LWIP_TCPIP_CORE_LOCKING=y AND CONFIG_LWIP_CHECK_THREAD_SAFETY=y being enabled by default in the pioarduino/arduino-esp32 IDF 5.x sdkconfig. The tasmota core (IDF 5.3.x, used for C6) likely has CONFIG_LWIP_CHECK_THREAD_SAFETY=n, which is why the same violation does not crash on C6. Since pioarduino already has TCPIP_CORE_LOCKING enabled, wrapping ntpUdp.begin() (and any other raw lwIP API call) with LOCK_TCPIP_CORE()/UNLOCK_TCPIP_CORE() from lwip/tcpip.h is directly applicable without sdkconfig changes in platformio.ini.

Learnt from: Mdbelen
Repo: wled/WLED PR: 5522
File: wled00/dmx_output.cpp:5-9
Timestamp: 2026-04-21T23:32:50.241Z
Learning: In WLED's `dmx_output.cpp` (ESP8266 path), `uart_tx_fifo_available()` is declared `inline __attribute__((always_inline))` in the ESP8266 Arduino core's `uart.cpp` and is NOT exported through any header. The workaround used is `#include "uart.cpp"` directly, which works because the function is always_inline (no ODR violation for that symbol). A cleaner alternative is to reproduce the one-liner body inline using the `USS()` and `USTXC` macros from `esp8266_peri.h`. Do not flag `#include "uart.cpp"` as an unconditional build failure for this file — it is a known limitation of the ESP8266 core's API surface.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled.h:336-339
Timestamp: 2026-03-28T01:36:54.706Z
Learning: In WLED (wled00/wled.h), the RX/TX hardware pin guard `#if defined(ARDUINO_ARCH_ESP32) && (!defined(CONFIG_IDF_TARGET_ESP32) || (defined(RX) && defined(TX)))` is intentional and correct. For all non-classic ESP32 targets (C3, C5, C6, S2, S3, P4, C61), the arduino-esp32 framework always defines RX and TX according to the target's native UART pins, so the unconditional entry into the hardwareRX/hardwareTX branch for `!defined(CONFIG_IDF_TARGET_ESP32)` is safe. The `|| (defined(RX) && defined(TX))` arm handles classic ESP32 boards that provide custom framework-defined UART pins. Do not flag this condition as potentially undefined-macro unsafe.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp:30-35
Timestamp: 2026-03-27T12:33:52.525Z
Learning: In WLED (and generally in C/C++ preprocessor code), the `&&` operator in `#if` directives IS short-circuit evaluated by GCC and Clang. This means a guard like `#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)` is safe on ESP8266 even if `ESP_IDF_VERSION_VAL` is not defined there, because `defined(ARDUINO_ARCH_ESP32)` evaluates to 0 and the right-hand side is never expanded. Cppcheck does NOT model this short-circuit and may emit false-positive "undefined function-like macro invocation" errors. Never suggest moving ESP32-specific headers (e.g., `esp_idf_version.h`) outside such guards as that would break ESP8266 builds where those headers do not exist.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled.cpp:1037-1051
Timestamp: 2026-03-29T16:14:17.321Z
Learning: In WLED PR `#5048` (wled00/wled.cpp, CONFIG_IDF_TARGET_ESP32P4 block): `ESP_HostedOTA.h` and `updateEspHostedSlave()` are NOT in the git repository. They are provided by the pioarduino ESP32-P4 platform package (platform-espressif32.zip, referenced in platformio.ini [esp32p4] section). The header and function are part of the ESP-Hosted Wi-Fi co-processor framework integration bundled within the pioarduino framework for ESP32-P4, not a separate Arduino library. Do NOT flag `#include "ESP_HostedOTA.h"` or calls to `updateEspHostedSlave()` as missing/undefined for ESP32-P4 builds.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-13T20:46:14.014Z
Learning: In WLED (wled00/wled.cpp), on ESP32 with arduino-esp32 v2.0.x (IDF V4 branch), calling `pinMode(hardwareRX, INPUT_PULLDOWN)` has an unexpected side effect: it routes the RX pin through the GPIO matrix instead of keeping it on UART0's IOMUX path, effectively detaching the pin from the UART unit and causing serial receive to stop working (issue `#5501`). The correct way to enable a pull-down on the hardware RX pin without disrupting the UART mapping is to call `gpio_pulldown_en((gpio_num_t)hardwareRX)` directly from the ESP-IDF GPIO driver (`driver/gpio.h`). This is the fix applied in PR `#5503`. Do NOT use `pinMode()` to set pull resistors on UART RX/TX pins on ESP32.

Learnt from: DedeHai
Repo: wled/WLED PR: 4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-12T19:41:50.655Z
Learning: In WLED bus_wrapper.h, LPD6803 (B_HS_LPO_3) and LPD8806 (B_HS_LPD_3) are missing the WLED_USE_ETHERNET guard that redirects from VSPI to HSPI. APA102 and WS2801 already have this guard. The fix is to add `#ifdef` WLED_USE_ETHERNET blocks using Lpd6803MethodBase<TwoWireHspiImple<SpiSpeedHz>> and Lpd8806MethodBase<TwoWireHspiImple<SpiSpeedHz>> respectively, mirroring the WS2801 pattern. TwoWireHspiImple.h is already included via the WS2801 ethernet path so no new `#include` is needed. This is the likely root cause of issue `#5152` where LPD6803 breaks Ethernet on ESP32.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

@coderabbitai coderabbitai Bot added the optimization re-working an existing feature to be faster, or use less memory label Apr 30, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wled00/dmx_output.cpp (1)

181-224: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp DMXChannels before indexing DMXFixtureMap.

DMXFixtureMap has 15 entries, but both loops run up to DMXChannels. A malformed config value above 15 reads past the map and turns channel generation into undefined behavior.

🛠️ Proposed fix
   bool calc_brightness = true;
+  const uint8_t channelCount = (DMXChannels > sizeof(DMXFixtureMap)) ? sizeof(DMXFixtureMap) : DMXChannels;
 
    // check if no shutter channel is set
-   for (unsigned i = 0; i < DMXChannels; i++) {
+   for (uint8_t i = 0; i < channelCount; i++) {
      if (DMXFixtureMap[i] == 5) calc_brightness = false;
    }
@@
-    for (int j = 0; j < DMXChannels; j++) {
+    for (uint8_t j = 0; j < channelCount; j++) {
       int DMXAddr = DMXFixtureStart + j;
       switch (DMXFixtureMap[j]) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/dmx_output.cpp` around lines 181 - 224, Clamp DMXChannels to the
actual size of DMXFixtureMap before any indexing to avoid out-of-bounds access:
compute a uint8_t safeChannels = min(DMXChannels,
(int)(sizeof(DMXFixtureMap)/sizeof(DMXFixtureMap[0]))); then replace uses of
DMXChannels in both for loops and any indexing (the loop that checks
DMXFixtureMap and the inner switch that uses DMXFixtureMap[j]) with safeChannels
(or use safeChannels as the loop bound) so all accesses to DMXFixtureMap[j] are
within bounds.
🧹 Nitpick comments (1)
wled00/data/settings_sync.htm (1)

51-56: ⚡ Quick win

Wire IDMO into the existing pin-selector flow.

The new DMX output pin stays a raw number input, so it misses the availability/conflict filtering that IDMR, IDMT, and IDME already get through makePinSelect() and dmxPinUpd(). Adding IDMO to the same list keeps the UI consistent and prevents selecting obviously bad/owned pins.

♻️ Proposed change
-const dmxNs = ["IDMR","IDMT","IDME"];
+const dmxNs = ["IDMR","IDMT","IDME","IDMO"];

As per coding guidelines: "When updating Web UI files in wled00/data/, make use of common functions in wled00/data/common.js whenever possible."

🤖 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 51 - 56, The DMX output pin IDMO
is not included in the pin-selector flow so it remains a raw number input and
misses availability/conflict filtering; update the dmxNs array to include "IDMO"
alongside "IDMR","IDMT","IDME" so that makePinSelect() is called for it and its
onchange is wired to dmxPinUpd(), and ensure dmxOwnPins is computed using
gN(...) for the new IDMO entry so the existing dmxPinUpd() logic applies without
further changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/data/settings_sync.htm`:
- Around line 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.

In `@wled00/dmx_output.cpp`:
- Around line 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.

---

Outside diff comments:
In `@wled00/dmx_output.cpp`:
- Around line 181-224: Clamp DMXChannels to the actual size of DMXFixtureMap
before any indexing to avoid out-of-bounds access: compute a uint8_t
safeChannels = min(DMXChannels,
(int)(sizeof(DMXFixtureMap)/sizeof(DMXFixtureMap[0]))); then replace uses of
DMXChannels in both for loops and any indexing (the loop that checks
DMXFixtureMap and the inner switch that uses DMXFixtureMap[j]) with safeChannels
(or use safeChannels as the loop bound) so all accesses to DMXFixtureMap[j] are
within bounds.

---

Nitpick comments:
In `@wled00/data/settings_sync.htm`:
- Around line 51-56: The DMX output pin IDMO is not included in the pin-selector
flow so it remains a raw number input and misses availability/conflict
filtering; update the dmxNs array to include "IDMO" alongside
"IDMR","IDMT","IDME" so that makePinSelect() is called for it and its onchange
is wired to dmxPinUpd(), and ensure dmxOwnPins is computed using gN(...) for the
new IDMO entry so the existing dmxPinUpd() logic applies without further
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f56fb0d6-95e8-4308-94b3-77f9557e32f2

📥 Commits

Reviewing files that changed from the base of the PR and between 8c658ce and d5e1354.

📒 Files selected for processing (8)
  • wled00/data/settings_sync.htm
  • wled00/dmx_input.cpp
  • wled00/dmx_output.cpp
  • wled00/dmx_output.h
  • wled00/set.cpp
  • wled00/wled.cpp
  • wled00/wled.h
  • wled00/xml.cpp
✅ Files skipped from review due to trivial changes (1)
  • wled00/dmx_input.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • wled00/set.cpp
  • wled00/xml.cpp

Comment on lines +205 to +208
<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>
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.

Comment thread wled00/dmx_output.cpp
Comment on lines +99 to +103
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;
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

connectivity Issue regarding protocols, WiFi connection or availability of interfaces enhancement hardware (Likely) Setup-specific issue, e.g. flickering LEDs optimization re-working an existing feature to be faster, or use less memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue with the WLED frontend [DMX serial-out cannot handle 144 LEDs]

5 participants