From f165cb6db20e50ce5a53dbdb42e60a09841c6766 Mon Sep 17 00:00:00 2001 From: Tim Leonard Date: Sat, 22 Aug 2026 12:37:00 -0400 Subject: [PATCH] Improve custom bus config UI capabilities --- wled00/bus_manager.cpp | 53 ++++++++++++++++--- wled00/bus_manager.h | 4 ++ wled00/data/settings_leds.htm | 95 ++++++++++++++++++++++++++++------- 3 files changed, 127 insertions(+), 25 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 5ba446fa9a..8ca17982c4 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -125,6 +125,42 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { return c; } +// AI: below section was generated by an AI +uint32_t Bus::mappedColorSum(uint32_t c, uint8_t ww, uint8_t cw) const { + const CustomBusConfig& custom = getCustomBusConfig(); + if (!custom.active()) return R(c) + G(c) + B(c) + W(c); + // Custom buses may map one logical WLED color source to multiple physical + // wire channels. Example: paired RGB+WW bulbs can use RGB for one WS281x + // chip and duplicate logical W across the second chip's three wire channels. + // ABL should count what is actually emitted on the wire, not only RGBW once. + uint32_t sum = 0; + for (uint8_t i = 0; i < custom.numChannels; i++) { + switch (custom.channelColors[i]) { + case WLEDpixelBus::CH_R: sum += R(c); break; + case WLEDpixelBus::CH_G: sum += G(c); break; + case WLEDpixelBus::CH_B: sum += B(c); break; + case WLEDpixelBus::CH_W: sum += W(c); break; + case WLEDpixelBus::CH_WW: sum += ww; break; + case WLEDpixelBus::CH_CW: sum += cw; break; + } + } + return sum; +} + +uint16_t Bus::currentEstimateChannels() const { + const CustomBusConfig& custom = getCustomBusConfig(); + if (!custom.active()) return hasWhite() ? 4 : 3; + // Use the number of active mapped wire channels as the denominator for + // custom buses. This keeps duplicated sources (for example W,W,W) from + // being undercounted compared with native fixed RGB/RGBW layouts. + uint16_t channels = 0; + for (uint8_t i = 0; i < custom.numChannels; i++) { + if (custom.channelColors[i] != WLEDpixelBus::CH_UNUSED) channels++; + } + return std::max(channels, 1); +} +// AI: end + // Default implementation for Bus::getCustomBusConfig() — returns a static default. // BusDigital and BusPlaceholder override this when custom.active() == true. const CustomBusConfig& Bus::getCustomBusConfig() const { @@ -174,7 +210,9 @@ BusDigital::BusDigital(const BusConfig &bc) if (bc.custom.invertOutput) _busPtr->setInverted(true); // invert output, needs to be set before bus->begin() (uses native hardware inversion capability) // TODO: should inverted be supported for normal buses too? probably better not as it complicates things for normal users, one more option to screw up the settings. } - // Derive instance capabilities from actual channel map + // Derive instance capabilities from the actual channel map instead of the + // selected base type. A custom RGB base type can therefore behave as a + // logical RGBW bus when its channel map contains W/WW/CW. _hasRgb = false; _hasWhite = false; _hasCCT = false; bool hasWW = false, hasCW = false; for (uint8_t i = 0; i < bc.custom.numChannels; i++) { @@ -187,6 +225,10 @@ BusDigital::BusDigital(const BusConfig &bc) } if (hasWW || hasCW) _hasWhite = true; if (hasWW && hasCW) _hasCCT = true; + // The base Bus constructor clamps auto-white for native RGB-only types. + // Re-apply it after custom capability detection so config-driven RGBW maps + // can expose and use the W slider / auto-white path. + _autoWhiteMode = _hasWhite ? bc.autoWhite : RGBW_MODE_MANUAL_ONLY; } else { // create bus via PixelBusAllocator wrapper which will return a WLEDpixelBus::PixelBus _busPtr = PixelBusAllocator::create(bc.type, _pins, lenToCreate + _skip, bc.colorOrder, _driverType, bc.busSpeedFactor, _frequencykHz); @@ -272,8 +314,8 @@ void BusDigital::estimateCurrent() { const uint8_t busBri = _busPtr->getBusBri(); if (busBri > 0) colorSum = ((uint64_t)colorSum * _bri) / busBri; } - // colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps - uint32_t clrUnitsPerChannel = hasWhite() ? 4*255 : 3*255; + // colorSum has all the values of color channels summed; convert to milliAmps + uint32_t clrUnitsPerChannel = currentEstimateChannels() * 255; _milliAmpsTotal = ((uint64_t)colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED } @@ -359,10 +401,10 @@ void BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (BusManager::_useABL) { // Accumulate brightness-scaled channel values for current estimation. // For 16-bit types c is unscaled (bri=255 above); ABL slightly over-estimates at low brightness. - uint8_t r = R(c), g = G(c), b = B(c); if (_milliAmpsPerLed < 255) { // normal ABL - _colorSum += r + g + b + W(c); + _colorSum += mappedColorSum(c, cctWW, cctCW); } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) + uint8_t r = R(c), g = G(c), b = B(c); _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); } } @@ -1680,4 +1722,3 @@ uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; bool BusManager::_useABL = false; Bus* BusManager::_lastBusCache = nullptr; // cache for setPixelColor() fast path - diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index c0c6935f3f..e46f9e75db 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -264,6 +264,10 @@ class Bus { uint8_t _busSpeedFactor = 100; // percent, default 100 = default timings uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const; + // AI: below section was generated by an AI + uint32_t mappedColorSum(uint32_t c, uint8_t ww, uint8_t cw) const; + uint16_t currentEstimateChannels() const; + // AI: end }; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 1cf2a2a95f..5f55550941 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -40,6 +40,32 @@ if (is16b(t)) ch *= 2; return ch; } + // Effective bus capabilities come from the saved custom bus config when enabled. + // This lets the UI mirror hardware that is logically one bulb but physically + // several wire channels, e.g. RGB on channels 1-3 and W duplicated on 4-6. + // Example: a custom map containing W/WW/CW makes the white controls visible. + function customCaps(n) { + if (!isCst(n)) return 0; + let nch = parseInt(d.Sf["CBch"+n]?.value) || 3; + let caps = 0, hasWW = false, hasCW = false; + for (let ci = 0; ci < nch && ci < 6; ci++) { + let v = parseInt(d.Sf["CBc"+ci+n]?.value) || 0; + if (v >= 1 && v <= 3) caps |= 0x01; + if (v === 4) caps |= 0x02; + if (v === 5) hasWW = true; + if (v === 6) hasCW = true; + } + if (hasWW || hasCW) caps |= 0x02; + if (hasWW && hasCW) caps |= 0x04; + if (d.Sf["CBb"+n]?.checked) caps |= 0x10; + return caps; + } + function busCaps(t, n) { + return isCst(n) ? customCaps(n) : gT(t).c; + } + function busHasW(t, n) { + return !!(busCaps(t, n) & 0x02); + } // pre-fill the channel-map override with the native layout of type t (called when "Customize" is first checked or the type changes) function presetCustomDefaults(n, t) { const order = ["GRB","RGB","BRG","RBG","BGR","GBR"][parseInt(d.Sf["CO"+n]?.value) || 0]; @@ -66,6 +92,30 @@ if (d.Sf["CBrst"+n]) d.Sf["CBrst"+n].value = type.trst || 300; } } + function applyCustomBusConfig(n, v) { + let enabled = typeof v.cch !== "undefined" && v.cch > 0; + let cb = d.Sf["CBen"+n]; + if (cb) cb.checked = enabled; + if (!enabled) return; + // Restore custom-bus config into the form before UI() evaluates + // capabilities, otherwise controls like the W slider would still follow + // the base LED type instead of the saved channel map. + if (d.Sf["CBch"+n]) d.Sf["CBch"+n].value = v.cch; + if (d.Sf["CBb"+n]) d.Sf["CBb"+n].checked = !!v.c16; + if (d.Sf["CBio"+n]) d.Sf["CBio"+n].checked = !!v.cio; + if (d.Sf["CBt0h"+n]) d.Sf["CBt0h"+n].value = v.ct0h ?? 300; + if (d.Sf["CBt0l"+n]) d.Sf["CBt0l"+n].value = v.ct0l ?? 900; + if (d.Sf["CBt1h"+n]) d.Sf["CBt1h"+n].value = v.ct1h ?? 700; + if (d.Sf["CBt1l"+n]) d.Sf["CBt1l"+n].value = v.ct1l ?? 500; + if (d.Sf["CBrst"+n]) d.Sf["CBrst"+n].value = v.crst ?? 300; + let mask = v.cinv || 0; + for (let ci = 0; ci < 6; ci++) { + let color = d.Sf["CBc"+ci+n]; + if (color) color.value = Array.isArray(v.cmap) && ci < v.cmap.length ? v.cmap[ci] : 0; + let inv = d.Sf["CBi"+ci+n]; + if (inv) inv.checked = !!(mask & (1 << ci)); + } + } // iterate every configured LED bus: fn(n, t, sel, idx) function forEachBus(fn) { d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((sel, idx) => { @@ -106,8 +156,10 @@ try { if (d && d.hw && d.hw.led && Array.isArray(d.hw.led.ins)) { d.hw.led.ins.forEach((v,i)=>{ - let el = d.getElementsByName("SF"+i)[0]; + let n = chrID(i); + let el = d.getElementsByName("SF"+n)[0]; if (el) el.value = (typeof v.bsf !== 'undefined') ? (v.bsf || 100) : 100; + applyCustomBusConfig(n, v); }); } } catch(e) {} @@ -422,7 +474,8 @@ d.Sf["MA"+n].min = (!isDig(t)) ? 0 : 250; // set minimum value for PSU mA } gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory - gRGBW |= hasW(t); // RGBW checkbox + let busHasWhite = busHasW(t, n); + gRGBW |= busHasWhite; // RGBW checkbox gId("co"+n).style.display = (isVir(t) || isAna(t) || isHub75(t) || isCst(n)) ? "none":"inline"; // hide color order for PWM / customized bus gId("dig"+n+"w").style.display = (isDig(t) && hasW(t) && !isCst(n)) ? "inline":"none"; // show swap channels dropdown (not for customized bus) gId("dig"+n+"w").querySelector("[data-opt=CCT]").disabled = !hasCCT(t); // disable WW/CW swapping @@ -444,7 +497,9 @@ gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual gId("dig"+n+"s").style.display = (isVir(t) || isAna(t) || isHub75(t)) ? "none":"inline"; // hide skip 1st for virtual & analog gId("dig"+n+"f").style.display = (isDig(t) || (isPWM(t) && maxL>2048)) ? "inline":"none"; // hide refresh (PWM hijacks reffresh for dithering on ESP32) - gId("dig"+n+"a").style.display = (hasW(t)) ? "inline":"none"; // auto calculate white + // Show auto-white for native RGBW buses and for custom maps whose + // config exposes white, even if the selected base type is RGB. + gId("dig"+n+"a").style.display = busHasWhite ? "inline":"none"; // auto calculate white gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off) gId("rev"+n).innerHTML = isAna(t) ? "Inverted output":"Reversed"; // change reverse text for analog else (rotated 180°) //gId("psd"+n).innerHTML = isAna(t) ? "Index:":"Start:"; // change analog start description @@ -909,21 +964,23 @@ let l = c.hw.led; l.ins.forEach((v,i,a)=>{ addLEDs(1); - for (var j=0; j>4) & 0x0F; - d.getElementsByName("SP"+i)[0].value = v.freq; - d.getElementsByName("SF"+i)[0].value = v.bsf || 100; // bus speed factor, default to 100% - d.getElementsByName("LA"+i)[0].value = v.ledma; - d.getElementsByName("MA"+i)[0].value = v.maxpwr; + let n = chrID(i); + for (var j=0; j>4) & 0x0F; + d.getElementsByName("SP"+n)[0].value = v.freq; + d.getElementsByName("SF"+n)[0].value = v.bsf || 100; // bus speed factor, default to 100% + d.getElementsByName("LA"+n)[0].value = v.ledma; + d.getElementsByName("MA"+n)[0].value = v.maxpwr; + applyCustomBusConfig(n, v); }); d.getElementsByName("MA")[0].value = l.maxpwr; d.getElementsByName("ABL")[0].checked = l.maxpwr > 0; @@ -1408,4 +1465,4 @@

Advanced

- \ No newline at end of file +