Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ AR_lib_deps = kosme/arduinoFFT @ 2.0.1
;;
;; 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.

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.

platform = espressif32@ ~6.3.2
platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them)
build_flags = -g
Expand Down
83 changes: 83 additions & 0 deletions usermods/AT8870_I2C_PSPWM_v2/usermod.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#pragma once

#include "wled.h"
#include "bus_usermod.h"


class UMB_AT8870_I2C_PSPWM : public UsermodBus
{
public:

uint16_t getId() override { return USERMOD_ID_BUS; }
void loop() override {}

void initBus(BusUsermod* bus) override
{
allocateBusData(bus, bus->getLength() * 4);
setBusRGB(bus, true);
setBusWhite(bus, true);
}

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);
}
Comment on lines +21 to +29
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).


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]);
}
Comment on lines +31 to +36
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.


void showBus(BusUsermod* bus) override
{
const uint8_t* pins = getBusPins(bus);
uint32_t c = getBusPixelColor(bus, 0);
if ( bus->hasWhite() ) {
c = autoWhiteCalc(bus, c);
}

uint8_t pwm_duty;
uint16_t pwm_delay;
uint8_t bri = getBusBrightness(bus);


Wire.beginTransmission(pins[0]);

Wire.write(16); // 16 == start of PWM addresses

pwm_delay = 0; // PWM delay for red = 0
pwm_duty = (R(c) * bri) / 255; // PWM duty for red, scaled by brightness
Wire.write(pwm_duty);
Wire.write(pwm_delay);

pwm_delay += pwm_duty; // PWM delay for green, start after red
if ( pwm_delay > 254 ) 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;
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;
pwm_duty = (W(c) * bri) / 255; // PWM duty for white, scaled by brightness
Wire.write(pwm_duty);
Wire.write(pwm_delay);

Wire.endTransmission();

}

};

12 changes: 7 additions & 5 deletions wled00/bus_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "pin_manager.h"
#include "bus_wrapper.h"
#include "bus_manager.h"
#include "bus_usermod.h"

extern bool cctICused;

Expand Down Expand Up @@ -699,7 +700,9 @@ uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned

int BusManager::add(BusConfig &bc) {
if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1;
if (Bus::isVirtual(bc.type)) {
if (bc.type == TYPE_USERMOD) {
busses[numBusses] = new BusUsermod(bc);
} else if (Bus::isVirtual(bc.type)) {
busses[numBusses] = new BusNetwork(bc);
} else if (Bus::isDigital(bc.type)) {
busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap);
Expand Down Expand Up @@ -749,13 +752,12 @@ String BusManager::getLEDTypesJSONString(void) {
{TYPE_NET_ARTNET_RGB, "N", PSTR("Art-Net RGB (network)")},
{TYPE_NET_DDP_RGBW, "N", PSTR("DDP RGBW (network)")},
{TYPE_NET_ARTNET_RGBW, "N", PSTR("Art-Net RGBW (network)")},
// hypothetical extensions
//{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0]
//{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0]
//{TYPE_VIRTUAL_I2C_RGB, "V", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0]
{TYPE_USERMOD, "V", PSTR("Usermod (virtual)")}, // virtual bus for usermods
};
String json = "[";
for (const auto &type : types) {
extern UsermodManager usermods;
if ( (type.id == TYPE_USERMOD) && (!usermods.lookup(USERMOD_ID_BUS))) continue;
String id = String(type.id);
// capabilities follows similar pattern as JSON API
int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4;
Expand Down
5 changes: 3 additions & 2 deletions wled00/bus_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ class Bus {
type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 ||
type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel
(type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel
type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel
type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW ||
type == TYPE_USERMOD; // network types with white channel
Comment on lines +122 to +123
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 hasCCT(uint8_t type) {
return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA ||
Expand All @@ -132,7 +133,7 @@ class Bus {
static constexpr bool is2Pin(uint8_t type) { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); }
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.

static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; }
static constexpr int numPWMPins(uint8_t type) { return (type - 40); }

Expand Down
75 changes: 75 additions & 0 deletions wled00/bus_usermod.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

#include <Arduino.h>
#include <IPAddress.h>

#include "pin_manager.h"
#include "bus_wrapper.h"
#include "bus_manager.h"
#include "bus_usermod.h"

#include "Wire.h"

extern BusManager busses;
extern UsermodManager usermods;

void BusUsermod::setPixelColor(uint16_t pix, uint32_t c) {
if (!_valid || !_usermod || pix >= _len) return;
return _usermod->setBusPixelColor(this, pix, c);
}

uint32_t BusUsermod::getPixelColor(uint16_t pix) const {
if (!_valid || !_usermod || pix >= _len) return 0;
return _usermod->getBusPixelColor(this, pix);
}

void BusUsermod::show() {
if (!_valid || !_usermod) return;
return _usermod->showBus(this);
}

BusUsermod::BusUsermod(BusConfig &bc)
: Bus(bc.type, bc.start, bc.autoWhite, bc.count)
{
// safe pin configuration for the actual Usermod
_pins[0] = bc.pins[0];
_pins[1] = bc.pins[1];
_pins[2] = bc.pins[2];
_pins[3] = bc.pins[3];
_pins[4] = bc.pins[4];

_valid = 1;
// 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);
}
Comment on lines +41 to +47
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.

}

void BusUsermod::cleanup(void) {
_type = I_NONE;
_valid = false;
freeData();
}

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;
Comment on lines +56 to +60
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.

}


void UsermodBus::setup() {
uint8_t busNr;
for ( busNr = 0; busNr < busses.getNumBusses(); busNr++ ) {
BusUsermod* bus = static_cast<BusUsermod*> (busses.getBus(busNr));
if ( bus->getType() == TYPE_USERMOD ) {
if ( bus->_usermod ) continue;
bus->_usermod = this;
initBus(bus);
}
}
}

59 changes: 59 additions & 0 deletions wled00/bus_usermod.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#ifndef BusUsermod_h
#define BusUsermod_h

class JsonObject;

#include "usermod.h"

class UsermodBus;

class BusUsermod : public Bus {
public:
BusUsermod(BusConfig &bc);
~BusUsermod() { cleanup(); }

void setPixelColor(uint16_t pix, uint32_t c) override;
uint32_t getPixelColor(uint16_t pix) const override;
void show(void) override;
void cleanup(void);

uint8_t getPins(uint8_t* pinArray) const override;

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.


friend class UsermodBus;
};



class UsermodBus : public Usermod {
public:

void setup() override;

protected:
virtual void showBus(BusUsermod* bus) = 0;
virtual void setBusPixelColor(BusUsermod* bus, uint16_t pix, uint32_t c) = 0;
virtual uint32_t getBusPixelColor(const BusUsermod* bus, uint16_t pix) const = 0;
virtual void initBus(BusUsermod* bus) = 0;

// helper functions, as C++ classes do not inherit friend class permissions
inline uint8_t* allocateBusData(BusUsermod* bus, size_t size = 1) { return bus->allocateData(size); }
inline uint8_t* getBusData(const BusUsermod* bus) const { return bus->_data; }
inline const uint8_t* getBusPins(const BusUsermod* bus) const { return bus->_pins; }
inline void setBusRGB(BusUsermod* bus, const bool hasRgb) { bus->_hasRgb = hasRgb; }
inline void setBusWhite(BusUsermod* bus, const bool hasWhite) { bus->_hasWhite = hasWhite; }
inline void setBusCCT(BusUsermod* bus, const bool hasCCT) { bus->_hasCCT = hasCCT; }
inline uint8_t getBusBrightness(const BusUsermod* bus) const { return bus->_bri; }
inline uint32_t autoWhiteCalc(const BusUsermod* bus, uint32_t c) const { return bus->autoWhiteCalc(c); }

friend class BusUsermod;
};





#endif
5 changes: 5 additions & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@
#define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h"
#define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h"

#define USERMOD_ID_BUS 127 //Special ID to be used by UsermodBus instances (only one may be used at once)



//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost)
Expand Down Expand Up @@ -327,6 +331,7 @@
#define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus)
#define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused)
#define TYPE_VIRTUAL_MAX 95
#define TYPE_USERMOD 127 //virtual type for usermods

/*
// old macros that have been moved to Bus class
Expand Down
6 changes: 3 additions & 3 deletions wled00/data/settings_leds.htm
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@
let nm = LC.name.substring(0,2);
let n = LC.name.substring(2);
let t = parseInt(d.Sf["LT"+n].value, 10); // LED type SELECT
// ignore IP address
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3") {
// ignore IP address and virtual usermod
if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4") {
if (t>=80) return;
}
//check for pin conflicts
Expand Down Expand Up @@ -259,7 +259,7 @@
gId("p1d"+n).innerText = p1d;
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.

for (let p=1; p<5; p++) {
var LK = d.Sf["L"+p+n];
if (!LK) continue;
Expand Down
Loading