Skip to content

Usermod bus type#4123

Draft
jw2013 wants to merge 5 commits intowled:mainfrom
jw2013:bus-config
Draft

Usermod bus type#4123
jw2013 wants to merge 5 commits intowled:mainfrom
jw2013:bus-config

Conversation

@jw2013
Copy link
Copy Markdown

@jw2013 jw2013 commented Aug 31, 2024

PR regarding bus implementations as usermods, as discussed with @blazoncek

Summary by CodeRabbit

  • New Features

    • Added usermod-based virtual bus architecture enabling custom LED device driver implementations
    • Introduced AT8870 I2C PWM RGBW pixel driver with independent color channel control, PWM duty cycling, and auto-white calculation support
  • Updates

    • Updated ESP32 V4 board partition configuration
    • Enhanced LED output configuration interface with improved virtual bus type support and pin validation

@blazoncek blazoncek changed the base branch from bus-config to 0_15 September 15, 2024 11:42
@softhack007 softhack007 added this to the 0.15.1 candidate milestone Sep 28, 2024
@blazoncek blazoncek changed the title Bus config Usermod bus type Oct 1, 2024
@blazoncek blazoncek added enhancement keep This issue will never become stale/closed automatically usermod usermod related labels Oct 1, 2024
@netmindz netmindz changed the base branch from 0_15 to main December 16, 2024 13:32
@blazoncek blazoncek self-assigned this Feb 26, 2025
@blazoncek blazoncek removed their assignment Jan 2, 2026
@softhack007
Copy link
Copy Markdown
Member

maintainers call for discussion: i think we should first understand if an "adding a bus by usermod" feature still fits to our future view of the WLED architecture.

@blazoncek
Copy link
Copy Markdown
Contributor

Since I'm no longer one of the WLED development team members I'll try to explain why I thought this was a good idea at the time.

Bus architecture is IMO good and flexible (though it could be better still) so adding a user extensible bus seemed nice addition to allow (advanced) users to extend output to devices of not necessarily LED type.
Once @willmmiles modified usermod architecture to be library-like this fit even more into the extensible bus architecture idea. Think of bus_wrapper.h as a usermod (with proprietary API) for NeoPixelBus driver and you'll get the idea.
Swapping LED driver is now (0.16) even easier to achieve as there are no requirements for the driver other than to support output methods. It does not need any buffers or "input" methods (AKA GetPixelColor()).
Just for fun: Perhaps consider writing driver utilizing FastLED instead of NeoPixelBus. This is now entirely possible (just needs new wrapper or usermod once API is completed).
However in its current state bus API isn't sufficient to support usermod types (including UI which may be the biggest task) so additional work will be needed.

@jw2013
Copy link
Copy Markdown
Author

jw2013 commented Jan 5, 2026

As the one who started this PR originally, despite not being involved in anything WLED related for the past year, I'm still convinced that it is useful for advanced users (as @blazoncek wrote).

There are many exotic use cases out there, that one could cover using WLED with minor additions. The project I was involved in 2024 required a way to control 'polarity change christmas lights', that typically run on 31V "AC".

I got that working by writing a phase shift pwm firmware for AT8870 motor shields, and controlling those from WLED via I2C. The same modified motor shields also work perfectly to control 12V-36V RGBW strips.

So it's certainly not for the average user, but I was glad to have that possibility back then.

@netmindz
Copy link
Copy Markdown
Member

netmindz commented Jan 7, 2026

Any modular approach needs to support multiple types, not just one.
It also requires that we complete the work I started to truly encapsulate each driver so that we don't have common code that says things like ifVirtual(), the type array needs do actually come from each bus implementation, as per my original design, not the current awkward compromise

@netmindz netmindz modified the milestones: 16.0.0 beta, 16.1 Jan 20, 2026
@softhack007 softhack007 modified the milestones: 16.1, 0.15.5, 17.0 Mar 14, 2026
;;
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv
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.

i think this change is obsolete - the V4 environment has evolved, and the default partition should not be set here.

@softhack007

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

Walkthrough

This PR introduces a new usermod bus infrastructure that enables usermods to function as custom LED drivers. It adds core bus-usermod bridging classes (BusUsermod and UsermodBus), implements a concrete AT8870 I2C PWM driver usermod, updates the bus manager to recognize the TYPE_USERMOD type, and refactors usermod declarations into a dedicated header file.

Changes

Cohort / File(s) Summary
Usermod Infrastructure
wled00/usermod.h, wled00/fcn_declare.h
Introduced new usermod.h header defining Usermod and UsermodManager classes with lifecycle hooks and manager logic; moved usermod API declarations from fcn_declare.h to the new header.
Bus-Usermod Bridge
wled00/bus_usermod.h, wled00/bus_usermod.cpp
Added BusUsermod (derived from Bus) and UsermodBus (derived from Usermod) classes that bridge the bus and usermod systems; includes pixel-color forwarding, bus initialization, and helper methods for derived usermods.
Bus Manager Updates
wled00/bus_manager.h, wled00/bus_manager.cpp
Updated BusManager::add() to handle TYPE_USERMOD as a virtual bus type; added TYPE_USERMOD to LED types JSON when the usermod bus is available; extended Bus::hasWhite() and Bus::isVirtual() helpers.
AT8870 I2C Driver
usermods/AT8870_I2C_PSPWM_v2/usermod.h, wled00/usermods_list.cpp
Implemented UMB_AT8870_I2C_PSPWM usermod providing I2C-based RGBW pixel control via PWM duty/delay cycles sent over the I2C bus; conditionally registered in usermods_list.cpp.
Constants and Configuration
wled00/const.h, wled00/data/settings_leds.htm, platformio.ini
Added USERMOD_ID_BUS and TYPE_USERMOD constants; updated pin-validation and secondary pin visibility logic in settings UI to handle virtual/usermod bus types; added default partition selection for ESP32 IDF build.

Sequence Diagram(s)

sequenceDiagram
    participant App as WLED App
    participant BusMgr as BusManager
    participant BusUM as BusUsermod
    participant UsermodBus as UsermodBus<br/>(AT8870)
    participant Wire as I2C Bus

    App->>BusMgr: add(BusConfig with TYPE_USERMOD)
    BusMgr->>BusUM: BusUsermod(config)
    BusUM->>UsermodBus: lookup(USERMOD_ID_BUS)
    UsermodBus->>BusUM: initBus(bus)
    UsermodBus->>BusUM: allocateBusData(4 bytes/pixel)

    App->>BusUM: setPixelColor(idx, RGBW)
    BusUM->>UsermodBus: setBusPixelColor(bus, idx, RGBW)
    UsermodBus->>BusUM: store RGBW in 4-byte buffer

    App->>BusUM: show()
    BusUM->>UsermodBus: showBus(bus)
    UsermodBus->>UsermodBus: calc PWM duty cycles
    UsermodBus->>UsermodBus: calc inter-channel delays
    UsermodBus->>Wire: beginTransmission
    UsermodBus->>Wire: write(R_duty, G_duty, B_duty, W_duty, ...)
    UsermodBus->>Wire: endTransmission
    Wire->>Wire: I2C transmission
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, hardware

Suggested reviewers

  • blazoncek
  • netmindz
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Usermod bus type' is partially related to the changeset. It refers to a real aspect of the changes (introducing usermod bus support), but is overly broad and vague about the specific purpose or primary objective. Consider a more specific title that clarifies the primary intent, such as 'Add UsermodBus support for extensible bus implementations' or 'Introduce usermod-based bus type for custom device drivers'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch bus-config

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

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 10, 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: 9

🧹 Nitpick comments (5)
wled00/usermod.h (2)

21-35: Missing include for size_t.

This header uses size_t (line 23) but doesn't include <stddef.h> or <cstddef>. While this works when the header is included after other headers that transitively define size_t, it's fragile and could break if include order changes.

💡 Proposed fix
 `#ifndef` Usermod_h
 `#define` Usermod_h

+#include <stddef.h>  // for size_t
+
 //um_manager.cpp
 typedef enum UM_Data_Types {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/usermod.h` around lines 21 - 35, The header declares UM_Exchange_Data
and uses size_t for u_size (typedef um_data_t) but doesn't include the standard
header that defines size_t; add an appropriate include (e.g., <cstddef> or
<stddef.h>) at the top of the file so UM_Exchange_Data::u_size and the um_data_t
typedef are unambiguous and won't break with different include orders.

36-36: Misleading size comment.

The comment // 12 bytes is architecture-dependent (pointer sizes vary between 32-bit and 64-bit platforms). On ESP32 (32-bit), it may be 12 bytes, but on 64-bit systems it would be larger. Consider removing or generalizing this comment.

💡 Suggested fix
-const unsigned int um_data_size = sizeof(um_data_t);  // 12 bytes
+const unsigned int um_data_size = sizeof(um_data_t);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/usermod.h` at line 36, The inline comment on the declaration const
unsigned int um_data_size = sizeof(um_data_t); incorrectly asserts a fixed size
("// 12 bytes") which is architecture-dependent; remove the hardcoded byte count
or replace it with a note that the size depends on target architecture (e.g.,
"// size depends on architecture, use sizeof(um_data_t)"), and if you need to
enforce a specific layout use static_assert or packed structs around um_data_t
to guarantee size instead of a fixed comment.
usermods/AT8870_I2C_PSPWM_v2/usermod.h (3)

60-73: Off-by-one in pwm_delay wrap-around logic.

The condition pwm_delay > 254 followed by pwm_delay -= 255 has an edge case: when pwm_delay == 255, it becomes 0 (correct), but when pwm_delay == 254, it doesn't wrap and stays at 254. If the intent is to wrap at 255, the condition should be >= 255.

💡 Suggested fix
       pwm_delay += pwm_duty;                    // PWM delay for green, start after red
-      if ( pwm_delay > 254 ) pwm_delay -= 255;
+      if ( pwm_delay >= 255 ) pwm_delay -= 255;
       pwm_duty = (G(c) * bri) / 255;       // PWM duty for green, scaled by brightness
       Wire.write(pwm_duty);
       Wire.write(pwm_delay);

       pwm_delay += pwm_duty;                    // PWM delay for blue, start after green
-      if ( pwm_delay > 254 ) pwm_delay -= 255;
+      if ( pwm_delay >= 255 ) pwm_delay -= 255;
       pwm_duty = (B(c) * bri) / 255;       // PWM duty for blue, scaled by brightness
       Wire.write(pwm_duty);
       Wire.write(pwm_delay);

       pwm_delay += pwm_duty;                    // PWM delay for white, start after blue
-      if ( pwm_delay > 254 ) pwm_delay -= 255;
+      if ( pwm_delay >= 255 ) pwm_delay -= 255;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h` around lines 60 - 73, The pwm_delay
wrap check uses "if (pwm_delay > 254) pwm_delay -= 255;" which misses the case
when pwm_delay == 254 should wrap at 255; update each wrap condition (the three
occurrences around pwm_delay updates in usermod.h) to use "if (pwm_delay >= 255)
pwm_delay -= 255;" so pwm_delay correctly wraps when it reaches 255 after adding
pwm_duty; ensure you change the conditions near the pwm_delay updates that
precede writing green/blue/white pwm_duty and pwm_delay via Wire.write.

51-78: No I2C error handling.

Wire.endTransmission() returns a status code indicating success or failure (0 = success, 1-5 = various errors). Silently ignoring transmission failures makes debugging difficult if the I2C device is not responding or misconfigured. Consider logging or tracking failed transmissions.

💡 Proposed error handling
-      Wire.endTransmission();
+      uint8_t err = Wire.endTransmission();
+      if (err != 0) {
+        DEBUG_PRINTF("I2C transmission error: %d for address 0x%02X\n", err, pins[0]);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h` around lines 51 - 78, The I2C
transfer using Wire.endTransmission() in usermod.h currently ignores the return
status; capture its return value and handle non-zero errors (1–5) by reporting
or recording the failure (e.g., Serial.println/ESP_LOG, incrementing a failure
counter, or triggering a retry/backoff), so replace the silent endTransmission()
call with logic that checks the status and logs or reacts to failures when
writing the PWM bytes via Wire.beginTransmission / Wire.write /
Wire.endTransmission.

38-41: Only pixel 0 is read for output.

showBus retrieves color from pixel 0 only (getBusPixelColor(bus, 0)), which means this driver only supports single-pixel (single-channel) output. This is likely intentional for the AT8870 use case, but a brief comment clarifying this limitation would help future maintainers.

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

In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h` around lines 38 - 41, showBus
currently reads only pixel 0 via getBusPixelColor(bus, 0), which restricts the
driver to single-pixel output; either add a brief comment in showBus stating
this intentional single-pixel (single-channel) limitation for the AT8870, or if
multi-pixel support is desired, replace the single read with a loop using
getBusNumPixels(bus) and getBusPixelColor(bus, idx) to process each pixel—update
the showBus implementation (and any related logic) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platformio.ini`:
- Line 239: The change to default_partitions in platformio.ini should not be
committed in this PR; revert the edit that sets default_partitions =
tools/WLED_ESP32_4MB_1MB_FS.csv from platformio.ini, move this partition
override into a local/custom build config or an external platformio override
file for your usermod, and reopen the PR only after obtaining explicit approval
from a maintainer/organization Member; ensure you flag the modification for
review rather than leaving the default_partitions change in-tree.

In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h`:
- Around line 31-36: getBusPixelColor reads four bytes without bounds-checking;
add the same guard used elsewhere to verify pix is in-range before accessing
getBusData. For example, at the start of getBusPixelColor(const BusUsermod* bus,
uint16_t pix) check that pix < bus->numPixels (or use the existing accessor like
getBusLength/getBusPixelCount if your codebase provides one) and return 0 (or a
safe default) when out of range, then proceed to compute i = pix * 4 and build
the RGBW32 value.
- Around line 21-29: setBusPixelColor currently computes i = pix * 4 and writes
4 bytes to the buffer returned by getBusData(bus) without range validation; add
a bounds check at the start of setBusPixelColor to ensure pix is within
bus->getLength() (or another appropriate length accessor) and that
getBusData(bus) is non-null before writing, returning early if the check fails
to prevent buffer overflow when writing R(c)/G(c)/B(c)/W(c).

In `@wled00/bus_manager.h`:
- Line 136: The change added TYPE_USERMOD to isVirtual(uint8_t type) but other
logic still treats usermod buses as non-virtual (BusConfig only copies pins[4]
and getNumVirtualBusses() counts only TYPE_VIRTUAL_MIN..MAX), causing usermod
buses to lose pins beyond 4 and occupy physical slots; update all related
helpers to treat TYPE_USERMOD consistently as virtual: extend BusConfig paths
that handle virtual buses to include TYPE_USERMOD so they copy the full virtual
config structure (not just pins[4]), update getNumVirtualBusses() to count
TYPE_USERMOD instances (or expand its range/logic accordingly), and audit any
other code paths that branch on isVirtual to ensure TYPE_USERMOD receives the
same virtual-bus handling as types in TYPE_VIRTUAL_MIN..TYPE_VIRTUAL_MAX.
- Around line 122-123: The branch in hasWhite() wrongly treats TYPE_USERMOD as
always having a white channel; remove TYPE_USERMOD from the hardcoded check and
instead query a usermod-supplied capability (e.g., add/consult a field or method
on the bus/usermod like usermodHasWhite() or a capability flag) so only buses
whose usermod declares white support use 4-byte pixels; update the hasWhite()
check to include TYPE_NET_DDP_RGBW and TYPE_NET_ARTNET_RGBW but rely on the
usermod capability for TYPE_USERMOD, and ensure buffer layout/offset logic reads
that same capability.

In `@wled00/bus_usermod.cpp`:
- Around line 41-47: Defer binding and replace the unsafe numeric cast: remove
the early call to usermods.lookup(USERMOD_ID_BUS) and the static_cast in this
init path so initBus() is not invoked before the usermod is initialized; instead
implement a typed probe method on the base class (e.g., Usermod::asBus() or a
virtual bool isBus()/UsermodBus* probeBus()) and call that from
UsermodBus::setup() where the usermod is guaranteed initialized, then have
UsermodBus::setup() perform the safe bind and call initBus(this) on the returned
UsermodBus pointer; keep USERMOD_ID_BUS usage for registration but avoid casting
by numeric ID in the early init sequence.
- Around line 56-60: BusUsermod::getPins currently dereferences pinArray even
when callers pass nullptr to query just the count; update the function so after
checking _valid and computing numPins it does not write into pinArray when
pinArray is nullptr—only copy _pins into pinArray if pinArray is non-null (e.g.,
if (pinArray) { for (...) pinArray[i] = _pins[i]; }) and always return the pin
count (or 0 when !_valid). Ensure you still use the same identifiers (_valid,
_pins, numPins, getPins) so the behavior matches other bus implementations.

In `@wled00/bus_usermod.h`:
- Line 24: Replace the non-standard type uint8 with the fixed-width standard
type uint8_t for the _pins member in class/struct declarations to ensure
portability; specifically update the declaration of _pins (uint8 _pins[5]) in
bus_usermod.h to use uint8_t and include <cstdint> or <stdint.h> if the header
does not already import it so the symbol _pins compiles with the correct type
across platforms.

In `@wled00/data/settings_leds.htm`:
- Line 262: The new fifth virtual config byte (L4) is being included in GPIO
checks later in UI(), so update the virtual fast-path there to treat indices
0..4 as non-GPIO (not just 0..3); locate the UI() routine and any
loops/conditions that currently use a hardcoded limit of 4 (e.g., checks like i
< 4 or exemption logic that only skips L0–L3) and change them to include L4 (i <
5 or equivalent), and ensure the pin-count calculation that sets `pins` (the
line using Math.min(gT(t).t.length,1) + 4*isVir(t) - 1*isNet(t)) remains
consistent with the new exemption so L4 uses 0–255 config semantics and is
excluded from max_gpio and pin-conflict highlighting.

---

Nitpick comments:
In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h`:
- Around line 60-73: The pwm_delay wrap check uses "if (pwm_delay > 254)
pwm_delay -= 255;" which misses the case when pwm_delay == 254 should wrap at
255; update each wrap condition (the three occurrences around pwm_delay updates
in usermod.h) to use "if (pwm_delay >= 255) pwm_delay -= 255;" so pwm_delay
correctly wraps when it reaches 255 after adding pwm_duty; ensure you change the
conditions near the pwm_delay updates that precede writing green/blue/white
pwm_duty and pwm_delay via Wire.write.
- Around line 51-78: The I2C transfer using Wire.endTransmission() in usermod.h
currently ignores the return status; capture its return value and handle
non-zero errors (1–5) by reporting or recording the failure (e.g.,
Serial.println/ESP_LOG, incrementing a failure counter, or triggering a
retry/backoff), so replace the silent endTransmission() call with logic that
checks the status and logs or reacts to failures when writing the PWM bytes via
Wire.beginTransmission / Wire.write / Wire.endTransmission.
- Around line 38-41: showBus currently reads only pixel 0 via
getBusPixelColor(bus, 0), which restricts the driver to single-pixel output;
either add a brief comment in showBus stating this intentional single-pixel
(single-channel) limitation for the AT8870, or if multi-pixel support is
desired, replace the single read with a loop using getBusNumPixels(bus) and
getBusPixelColor(bus, idx) to process each pixel—update the showBus
implementation (and any related logic) accordingly.

In `@wled00/usermod.h`:
- Around line 21-35: The header declares UM_Exchange_Data and uses size_t for
u_size (typedef um_data_t) but doesn't include the standard header that defines
size_t; add an appropriate include (e.g., <cstddef> or <stddef.h>) at the top of
the file so UM_Exchange_Data::u_size and the um_data_t typedef are unambiguous
and won't break with different include orders.
- Line 36: The inline comment on the declaration const unsigned int um_data_size
= sizeof(um_data_t); incorrectly asserts a fixed size ("// 12 bytes") which is
architecture-dependent; remove the hardcoded byte count or replace it with a
note that the size depends on target architecture (e.g., "// size depends on
architecture, use sizeof(um_data_t)"), and if you need to enforce a specific
layout use static_assert or packed structs around um_data_t to guarantee size
instead of a fixed comment.
🪄 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: 1a6d40fa-c074-4aaa-9468-8b717beedcee

📥 Commits

Reviewing files that changed from the base of the PR and between 18e0ec9 and 64b62ac.

📒 Files selected for processing (11)
  • platformio.ini
  • usermods/AT8870_I2C_PSPWM_v2/usermod.h
  • wled00/bus_manager.cpp
  • wled00/bus_manager.h
  • wled00/bus_usermod.cpp
  • wled00/bus_usermod.h
  • wled00/const.h
  • wled00/data/settings_leds.htm
  • wled00/fcn_declare.h
  • wled00/usermod.h
  • wled00/usermods_list.cpp

;;
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv
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

Please keep this usermod PR out of the main platformio.ini.

Even a one-line change here can perturb shared CI/build environments, and this PR does not look like the dual-framework exception that needs core build-system edits. This should stay in a custom override/build config unless a maintainer explicitly wants it in-tree.

As per coding guidelines, modifications to platformio.ini MUST be approved explicitly by a maintainer or WLED organisation Member. Flag all modifications as they may break GitHub Action builds.

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

In `@platformio.ini` at line 239, The change to default_partitions in
platformio.ini should not be committed in this PR; revert the edit that sets
default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv from platformio.ini, move
this partition override into a local/custom build config or an external
platformio override file for your usermod, and reopen the PR only after
obtaining explicit approval from a maintainer/organization Member; ensure you
flag the modification for review rather than leaving the default_partitions
change in-tree.

Comment on lines +21 to +29
void setBusPixelColor(BusUsermod* bus, uint16_t pix, uint32_t c) override
{
uint8_t* data = getBusData(bus);
uint16_t i = pix * 4;
data[i++] = R(c);
data[i++] = G(c);
data[i++] = B(c);
data[i++] = W(c);
}
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

Missing bounds check before buffer access.

setBusPixelColor computes i = pix * 4 and writes 4 bytes without validating that pix < bus->getLength(). If an out-of-range pixel index is passed, this will write beyond the allocated buffer.

🛡️ Proposed bounds check
     void             setBusPixelColor(BusUsermod* bus, uint16_t pix, uint32_t c) override
     {
+      if (pix >= bus->getLength()) return;
       uint8_t* data = getBusData(bus);
       uint16_t i = pix * 4;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h` around lines 21 - 29,
setBusPixelColor currently computes i = pix * 4 and writes 4 bytes to the buffer
returned by getBusData(bus) without range validation; add a bounds check at the
start of setBusPixelColor to ensure pix is within bus->getLength() (or another
appropriate length accessor) and that getBusData(bus) is non-null before
writing, returning early if the check fails to prevent buffer overflow when
writing R(c)/G(c)/B(c)/W(c).

Comment on lines +31 to +36
uint32_t getBusPixelColor(const BusUsermod* bus, uint16_t pix) const override
{
uint8_t* data = getBusData(bus);
uint16_t i = pix * 4;
return RGBW32(data[i], data[i+1], data[i+2], data[i+3]);
}
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

Same bounds check needed for getBusPixelColor.

Reading from an out-of-range pixel index will access uninitialized or out-of-bounds memory.

🛡️ Proposed bounds check
     uint32_t         getBusPixelColor(const BusUsermod* bus, uint16_t pix) const override
     {
+      if (pix >= bus->getLength()) return 0;
       uint8_t* data = getBusData(bus);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint32_t getBusPixelColor(const BusUsermod* bus, uint16_t pix) const override
{
uint8_t* data = getBusData(bus);
uint16_t i = pix * 4;
return RGBW32(data[i], data[i+1], data[i+2], data[i+3]);
}
uint32_t getBusPixelColor(const BusUsermod* bus, uint16_t pix) const override
{
if (pix >= bus->getLength()) return 0;
uint8_t* data = getBusData(bus);
uint16_t i = pix * 4;
return RGBW32(data[i], data[i+1], data[i+2], data[i+3]);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/AT8870_I2C_PSPWM_v2/usermod.h` around lines 31 - 36,
getBusPixelColor reads four bytes without bounds-checking; add the same guard
used elsewhere to verify pix is in-range before accessing getBusData. For
example, at the start of getBusPixelColor(const BusUsermod* bus, uint16_t pix)
check that pix < bus->numPixels (or use the existing accessor like
getBusLength/getBusPixelCount if your codebase provides one) and return 0 (or a
safe default) when out of range, then proceed to compute i = pix * 4 and build
the RGBW32 value.

Comment on lines +122 to +123
type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW ||
type == TYPE_USERMOD; // network types with white channel
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

Don’t hardcode every usermod bus as RGBW.

TYPE_USERMOD is the generic extension point, but hasWhite() feeds both UI parsing and buffer layout. With this branch every usermod bus gets white-channel handling and 4-byte pixel storage, so an RGB-only/custom implementation will receive the wrong channel offsets. Please make these capabilities usermod-supplied instead of baking them into the shared type.

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

In `@wled00/bus_manager.h` around lines 122 - 123, The branch in hasWhite()
wrongly treats TYPE_USERMOD as always having a white channel; remove
TYPE_USERMOD from the hardcoded check and instead query a usermod-supplied
capability (e.g., add/consult a field or method on the bus/usermod like
usermodHasWhite() or a capability flag) so only buses whose usermod declares
white support use 4-byte pixels; update the hasWhite() check to include
TYPE_NET_DDP_RGBW and TYPE_NET_ARTNET_RGBW but rely on the usermod capability
for TYPE_USERMOD, and ensure buffer layout/offset logic reads that same
capability.

static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); }
static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); }
static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); }
static constexpr bool isVirtual(uint8_t type) { return ((type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX) || type == TYPE_USERMOD); }
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

TYPE_USERMOD doesn’t fit the existing virtual-bus helper semantics yet.

In this header, isVirtual() is reused for more than classification: BusConfig still copies only 4 config bytes for virtual buses, and getNumVirtualBusses() still only counts the old 80-95 network range. So this change drops pins[4] for usermod buses and still makes them consume a physical-bus slot.

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

In `@wled00/bus_manager.h` at line 136, The change added TYPE_USERMOD to
isVirtual(uint8_t type) but other logic still treats usermod buses as
non-virtual (BusConfig only copies pins[4] and getNumVirtualBusses() counts only
TYPE_VIRTUAL_MIN..MAX), causing usermod buses to lose pins beyond 4 and occupy
physical slots; update all related helpers to treat TYPE_USERMOD consistently as
virtual: extend BusConfig paths that handle virtual buses to include
TYPE_USERMOD so they copy the full virtual config structure (not just pins[4]),
update getNumVirtualBusses() to count TYPE_USERMOD instances (or expand its
range/logic accordingly), and audit any other code paths that branch on
isVirtual to ensure TYPE_USERMOD receives the same virtual-bus handling as types
in TYPE_VIRTUAL_MIN..TYPE_VIRTUAL_MAX.

Comment on lines +41 to +47
// error: 'dynamic_cast' not permitted with '-fno-rtti'
// _usermod = dynamic_cast<UsermodBus*> (usermods.lookup(USERMOD_ID_BUS));
_usermod = static_cast<UsermodBus*> (usermods.lookup(USERMOD_ID_BUS));

if ( _usermod ) {
_usermod->initBus(this);
}
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

Binding the backing usermod here is too early, and the cast is not type-safe.

At this point the usermod has been registered but not yet run through usermods.setup(), so initBus() can fire before the usermod initializes itself. Separately, lookup(USERMOD_ID_BUS) is only an ID match; static_cast<UsermodBus*> becomes UB if any unrelated usermod also returns 127. Please defer binding to UsermodBus::setup() and expose a typed probe on Usermod instead of casting by numeric ID.

Based on learnings, hardcoded usermod IDs are intentionally used as part of a strategy to avoid modifying core code.

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

In `@wled00/bus_usermod.cpp` around lines 41 - 47, Defer binding and replace the
unsafe numeric cast: remove the early call to usermods.lookup(USERMOD_ID_BUS)
and the static_cast in this init path so initBus() is not invoked before the
usermod is initialized; instead implement a typed probe method on the base class
(e.g., Usermod::asBus() or a virtual bool isBus()/UsermodBus* probeBus()) and
call that from UsermodBus::setup() where the usermod is guaranteed initialized,
then have UsermodBus::setup() perform the safe bind and call initBus(this) on
the returned UsermodBus pointer; keep USERMOD_ID_BUS usage for registration but
avoid casting by numeric ID in the early init sequence.

Comment on lines +56 to +60
uint8_t BusUsermod::getPins(uint8_t* pinArray) const {
if (!_valid) return 0;
unsigned numPins = 5;
for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i];
return numPins;
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

Honor the getPins(nullptr) contract in this override.

Other bus implementations let callers query just the pin count without supplying a buffer. This version unconditionally dereferences pinArray and will crash on that path.

Suggested fix
 uint8_t BusUsermod::getPins(uint8_t* pinArray) const {
   if (!_valid) return 0;
   unsigned numPins = 5;
-  for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i];
+  if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i];
   return numPins;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/bus_usermod.cpp` around lines 56 - 60, BusUsermod::getPins currently
dereferences pinArray even when callers pass nullptr to query just the count;
update the function so after checking _valid and computing numPins it does not
write into pinArray when pinArray is nullptr—only copy _pins into pinArray if
pinArray is non-null (e.g., if (pinArray) { for (...) pinArray[i] = _pins[i]; })
and always return the pin count (or 0 when !_valid). Ensure you still use the
same identifiers (_valid, _pins, numPins, getPins) so the behavior matches other
bus implementations.


protected:
UsermodBus* _usermod;
uint8 _pins[5]; // used as configuration hints for the Usermod
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

Use uint8_t instead of uint8.

uint8 is not a standard C/C++ type. Use uint8_t for portability and consistency with the rest of the codebase.

🔧 Proposed fix
-    uint8            _pins[5];              // used as configuration hints for the Usermod
+    uint8_t          _pins[5];              // used as configuration hints for the Usermod
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint8 _pins[5]; // used as configuration hints for the Usermod
uint8_t _pins[5]; // used as configuration hints for the Usermod
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/bus_usermod.h` at line 24, Replace the non-standard type uint8 with
the fixed-width standard type uint8_t for the _pins member in class/struct
declarations to ensure portability; specifically update the declaration of _pins
(uint8 _pins[5]) in bus_usermod.h to use uint8_t and include <cstdint> or
<stdint.h> if the header does not already import it so the symbol _pins compiles
with the correct type across platforms.

gId("off"+n).innerText = off;
// secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off)
let pins = Math.min(gT(t).t.length,1) + 3*isNet(t); // fixes network pins to 4
let pins = Math.min(gT(t).t.length,1) + 4*isVir(t) - 1*isNet(t); // fixes network pins to 4, other virtual to 5
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

The new fifth virtual config field is still treated like a GPIO later in UI().

This branch exposes L4 for non-network virtual buses, but the later virtual fast-path still only exempts L0-L3. As a result, L4 keeps max_gpio limits and pin-conflict highlighting instead of 0-255 config semantics, so the fifth usermod config byte is unusable for many values.

Suggested follow-up
-				if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3") {
+				if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4") {
 					if (isVir(t)) {
 						LC.max = 255;
 						LC.min = 0;
 						LC.style.color="#fff";
 						return; // do not check conflicts
 					} else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let pins = Math.min(gT(t).t.length,1) + 4*isVir(t) - 1*isNet(t); // fixes network pins to 4, other virtual to 5
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4") {
if (isVir(t)) {
LC.max = 255;
LC.min = 0;
LC.style.color="#fff";
return; // do not check conflicts
} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/data/settings_leds.htm` at line 262, The new fifth virtual config byte
(L4) is being included in GPIO checks later in UI(), so update the virtual
fast-path there to treat indices 0..4 as non-GPIO (not just 0..3); locate the
UI() routine and any loops/conditions that currently use a hardcoded limit of 4
(e.g., checks like i < 4 or exemption logic that only skips L0–L3) and change
them to include L4 (i < 5 or equivalent), and ensure the pin-count calculation
that sets `pins` (the line using Math.min(gT(t).t.length,1) + 4*isVir(t) -
1*isNet(t)) remains consistent with the new exemption so L4 uses 0–255 config
semantics and is excluded from max_gpio and pin-conflict highlighting.

@softhack007 softhack007 added the needs_rework PR needs improvements before merging (RED FLAG) label Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

discussion enhancement hardware (Likely) Setup-specific issue, e.g. flickering LEDs keep This issue will never become stale/closed automatically needs_rework PR needs improvements before merging (RED FLAG) usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants