From d7e45b4805368829b3581548c33a51213e4fcf0d Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Mon, 4 May 2026 14:41:47 -0400 Subject: [PATCH 01/13] Add per-bus W-channel CCT for accurate auto-white calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Auto-Calculate White "Accurate" mode subtracts the W channel value equally from R, G, B — which implicitly assumes the physical W LED emits RGB(255, 255, 255), i.e. the sRGB white point near D65 / ~6500 K. For 2700 K WW or 5000 K CW strips this shifts the resulting color visibly. Add a per-bus configurable white-LED color temperature (Kelvin) that feeds into autoWhiteCalc, so the W LED's actual R/G/B contribution is computed via colorKtoRGB and used to (a) cap the W channel without overflowing any RGB channel and (b) subtract the correct per-channel amount in ACCURATE mode. The feature is opt-in per bus via a UI checkbox; when off (the default, wk = 0) autoWhiteCalc behaves as before, so existing configs render identically. UI lives next to the per-bus "Auto-calculate W channel from RGB" selector and is only shown when AW mode is Brighter or Accurate. The Kelvin number input is disabled (and not submitted) until the user checks the enable box, at which point it defaults to 6500 K — matching the implicit legacy reference white point. https://claude.ai/code/session_019b31kdwp79ouA3gD5Tox9A Co-authored-by: Claude --- wled00/bus_manager.cpp | 36 ++++++++++++++++++++++++++++-- wled00/bus_manager.h | 15 ++++++++++++- wled00/cfg.cpp | 4 +++- wled00/data/settings_leds.htm | 42 ++++++++++++++++++++++++++++++++++- wled00/set.cpp | 6 ++++- wled00/xml.cpp | 4 ++++ 6 files changed, 101 insertions(+), 6 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 175d49ba9c..08ed860aad 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -98,6 +98,22 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { cw = (w * cw) / 255; } +// AI: below section was generated by an AI +// recompute cached W-LED RGB equivalent when the configured Kelvin changes; +// 0 means "treat the W LED as neutral white" which preserves legacy behavior +// where autoWhiteCalc subtracted the same value from R, G, B. +void Bus::setWhiteKelvin(uint16_t k) { + _whiteKelvin = k; + if (k == 0) { + _wR = _wG = _wB = 255; // legacy: treat W as neutral + } else { + byte rgb[4]; + colorKtoRGB(k, rgb); + _wR = rgb[0]; _wG = rgb[1]; _wB = rgb[2]; + } +} +// AI: end + // calculates white channel and CCT values based on given settings uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { unsigned aWM = _autoWhiteMode; @@ -113,8 +129,23 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { } else if (aWM == RGBW_MODE_MAX) { w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel } else { - w = r < g ? (r < b ? r : b) : (g < b ? g : b); // darkest RGB channel - if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode + // AI: below section was generated by an AI + // Per-channel cap: pick the largest w such that (w * _wX)/255 <= channel + // for every X in {R,G,B}, preventing underflow when subtracting the W + // LED's RGB contribution. When _whiteKelvin==0 the cached _wR/_wG/_wB + // are all 255, so this collapses to the legacy w = min(r,g,b). + unsigned wMaxR = _wR ? (r * 255U) / _wR : 255U; + unsigned wMaxG = _wG ? (g * 255U) / _wG : 255U; + unsigned wMaxB = _wB ? (b * 255U) / _wB : 255U; + unsigned wCap = wMaxR < wMaxG ? (wMaxR < wMaxB ? wMaxR : wMaxB) : (wMaxG < wMaxB ? wMaxG : wMaxB); + if (wCap > 255U) wCap = 255U; + w = wCap; + if (aWM == RGBW_MODE_AUTO_ACCURATE) { + r -= (w * _wR) / 255; // subtract W LED's R contribution + g -= (w * _wG) / 255; // subtract W LED's G contribution + b -= (w * _wB) / 255; // subtract W LED's B contribution + } + // AI: end } c = RGBW32(r, g, b, w); } @@ -1221,6 +1252,7 @@ int BusManager::add(const BusConfig &bc, bool placeholder) { } else { busses.push_back(make_unique(bc)); } + if (!busses.empty()) busses.back()->setWhiteKelvin(bc.whiteKelvin); return busses.size(); } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index abfb08c81b..c84746b8db 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -58,6 +58,7 @@ make_unique(Args&&... args) //colors.cpp uint16_t approximateKelvinFromRGB(uint32_t rgb); +void colorKtoRGB(uint16_t kelvin, byte* rgb); #define GET_BIT(var,bit) (((var)>>(bit))&0x01) #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) @@ -121,6 +122,10 @@ class Bus { , _reversed(reversed) , _valid(false) , _needsRefresh(refresh) + , _whiteKelvin(0) + , _wR(255) + , _wG(255) + , _wB(255) { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; @@ -162,6 +167,8 @@ class Bus { inline void setStart(uint16_t start) { _start = start; } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } + inline uint16_t getWhiteKelvin() const { return _whiteKelvin; } + void setWhiteKelvin(uint16_t k); inline size_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } inline uint16_t getStart() const { return _start; } inline uint8_t getType() const { return _type; } @@ -221,6 +228,10 @@ class Bus { uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; + uint16_t _whiteKelvin; // physical W-channel CCT in Kelvin (0 = neutral/legacy behavior) + uint8_t _wR; // cached W LED RGB equivalent (255,255,255 when _whiteKelvin==0) + uint8_t _wG; + uint8_t _wB; //struct { //using bitfield struct adds abour 250 bytes to binary size bool _reversed;// : 1; bool _valid;// : 1; @@ -461,6 +472,7 @@ struct BusConfig { uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; + uint16_t whiteKelvin; // physical W-channel CCT in Kelvin (0 = neutral/legacy behavior) uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; uint16_t frequency; uint8_t milliAmpsPerLed; @@ -469,13 +481,14 @@ struct BusConfig { uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation String text; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "") + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "", uint16_t whiteK=0) : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) , reversed(rev) , skipAmount(skip) , autoWhite(aw) + , whiteKelvin(whiteK) , frequency(clock_kHz) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 2e458e7da9..a9a9a42a47 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -238,6 +238,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; + uint16_t whiteK = elm[F("wk")] | 0; // physical W-channel CCT in K (0 = neutral/legacy) uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT; uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) @@ -249,7 +250,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available String host = elm[F("text")] | String(); - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host, whiteK); doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -999,6 +1000,7 @@ void serializeConfig(JsonObject root) { ins["type"] = bus->getType() & 0x7F; ins["ref"] = bus->isOffRefreshRequired(); ins[F("rgbwm")] = bus->getAutoWhiteMode(); + ins[F("wk")] = bus->getWhiteKelvin(); ins[F("freq")] = bus->getFrequency(); ins[F("maxpwr")] = bus->getMaxCurrent(); ins[F("ledma")] = bus->getLEDCurrent(); diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index f1471e9bf4..03a5adffd0 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -205,6 +205,20 @@ }); if (ppl) d.Sf.MA.value = sumMA; // populate UI ABL value if PPL used } + // AI: below section was generated by an AI + // Per-bus W-LED color temperature toggle. The number input is disabled + // (and hidden) when the checkbox is off, so it isn't submitted with + // the form — backend then sees no WK arg and stores wk=0 (legacy + // fast path). Seed the field to 6500 K when re-enabling from a blank + // or sub-min value so the UI default matches the sRGB white point. + function wkChk(n) + { + const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; + if (!wke || !wk) return; + if (wke.checked && parseInt(wk.value)<1900) wk.value = 6500; + UI(); + } + // AI: end // enable and update LED Amps function enLA(s,n) { @@ -369,6 +383,25 @@ 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 + // AI: below section was generated by an AI + // W-channel CCT controls are only meaningful when autoWhiteCalc + // actually subtracts the W contribution — i.e. AW mode is + // Brighter (1) or Accurate (2). Hide the whole block otherwise. + // The number input is disabled (and not submitted) when the + // WKE checkbox is off, so the backend stores wk=0 and the + // legacy autoWhite path is used. + { + const awEl = d.Sf["AW"+n]; + const awv = awEl ? parseInt(awEl.value) : 0; + const wkBox = gId("dig"+n+"wk"); + if (wkBox) wkBox.style.display = (hasW(t) && (awv === 1 || awv === 2)) ? "inline" : "none"; + const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; + if (wke && wk) { + wk.disabled = !wke.checked; + wk.style.display = wke.checked ? "inline" : "none"; + } + } + // AI: end 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 @@ -593,7 +626,7 @@

Reversed:

Skip first LEDs:

Off Refresh:
-

Auto-calculate W channel from RGB:
 
+

Auto-calculate W channel from RGB:
 
`; f.insertAdjacentHTML("beforeend", cn); // fill led types (credit @netmindz) @@ -784,6 +817,13 @@ d.getElementsByName("RF"+i)[0].checked = v.ref; d.getElementsByName("CV"+i)[0].checked = v.rev; d.getElementsByName("AW"+i)[0].value = v.rgbwm; + { // AI: derive WKE checkbox + WK seed from stored wk (0 = feature off) + const wkChkEl = d.getElementsByName("WKE"+i)[0]; + const wkEl = d.getElementsByName("WK"+i)[0]; + const wkv = parseInt(v.wk) | 0; + if (wkChkEl) wkChkEl.checked = wkv > 0; + if (wkEl) wkEl.value = wkv > 0 ? wkv : 6500; + } d.getElementsByName("WO"+i)[0].value = (v.order>>4) & 0x0F; d.getElementsByName("SP"+i)[0].value = v.freq; d.getElementsByName("LA"+i)[0].value = v.ledma; diff --git a/wled00/set.cpp b/wled00/set.cpp index fb516ac7d6..81273ab5a4 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -205,6 +205,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip first N LEDs char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //refresh required char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode + char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W-channel CCT (Kelvin) char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //channel swap char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA @@ -230,6 +231,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) break; // no parameter } awmode = request->arg(aw).toInt(); + uint16_t whiteK = request->hasArg(wk) ? (uint16_t)request->arg(wk).toInt() : 0; + // Reject out-of-range or sub-1900K Kelvin values; 0 means "neutral/legacy" + if (whiteK != 0 && (whiteK < 1900 || whiteK > 10000)) whiteK = 0; uint16_t freq = request->arg(sp).toInt(); if (Bus::isPWM(type)) { switch (freq) { @@ -265,7 +269,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text); + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text, whiteK); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 812ef8c207..76f79f5444 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -371,6 +371,8 @@ void getSettingsJS(byte subPage, Print& settingsScript) char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip 1st LED char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //off refresh char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode + char wke[5] = "WKE"; wke[3] = offset+s; wke[4] = 0; //W-channel CCT enabled (UI checkbox) + char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W-channel CCT (Kelvin) char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //swap channels char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current @@ -392,6 +394,8 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,sl,bus->skippedLeds()); printSetFormCheckbox(settingsScript,rf,bus->isOffRefreshRequired()); printSetFormValue(settingsScript,aw,bus->getAutoWhiteMode()); + printSetFormCheckbox(settingsScript,wke,bus->getWhiteKelvin() > 0); + printSetFormValue(settingsScript,wk,bus->getWhiteKelvin() > 0 ? bus->getWhiteKelvin() : 6500); printSetFormValue(settingsScript,wo,bus->getColorOrder() >> 4); unsigned speed = bus->getFrequency(); if (bus->isPWM()) { From 01fe430fca69109ea249ecaf68320b4156df8a35 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Mon, 25 May 2026 15:42:21 -0400 Subject: [PATCH 02/13] Show W-LED CCT controls in DUAL auto-white mode too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DUAL mode (RGBW_MODE_DUAL) falls through to the per-channel-cap branch of autoWhiteCalc whenever the caller hasn't set the manual white value (w == 0) — the same path used by BRIGHTER and ACCURATE. The UI gate was only revealing the WKE checkbox and Kelvin input for modes 1 and 2, so users on DUAL had no way to configure the W-LED color temperature even though their output was affected by _wR/_wG/_wB. Include awv === 3 in the visibility condition and update the comment to reflect the actual code path in bus_manager.cpp. Co-authored-by: Claude --- wled00/data/settings_leds.htm | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 03a5adffd0..36e66d2e51 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -385,16 +385,17 @@ gId("dig"+n+"a").style.display = (hasW(t)) ? "inline":"none"; // auto calculate white // AI: below section was generated by an AI // W-channel CCT controls are only meaningful when autoWhiteCalc - // actually subtracts the W contribution — i.e. AW mode is - // Brighter (1) or Accurate (2). Hide the whole block otherwise. - // The number input is disabled (and not submitted) when the - // WKE checkbox is off, so the backend stores wk=0 and the - // legacy autoWhite path is used. + // uses the per-channel-cap path that consumes _wR/_wG/_wB — + // i.e. AW mode is Brighter (1), Accurate (2), or Dual (3, where + // manual w==0 falls through to the Brighter path). Hide + // otherwise. The number input is disabled (and not submitted) + // when the WKE checkbox is off, so the backend stores wk=0 + // and the legacy autoWhite path is used. { const awEl = d.Sf["AW"+n]; const awv = awEl ? parseInt(awEl.value) : 0; const wkBox = gId("dig"+n+"wk"); - if (wkBox) wkBox.style.display = (hasW(t) && (awv === 1 || awv === 2)) ? "inline" : "none"; + if (wkBox) wkBox.style.display = (hasW(t) && (awv === 1 || awv === 2 || awv === 3)) ? "inline" : "none"; const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; if (wke && wk) { wk.disabled = !wke.checked; From 113b3dc78a33d8f698ee2e79dfe109425b6ee19e Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Mon, 25 May 2026 15:43:52 -0400 Subject: [PATCH 03/13] Add fast path to autoWhiteCalc when per-bus CCT feature is off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-channel-cap branch added in the prior commit ran three integer divisions per pixel even when _whiteKelvin == 0 (the default), because the cached _wR/_wG/_wB values were always read as locals — the compiler could not constant-fold them. For RGBW strips this is a measurable hot- path regression vs the original min(r,g,b) implementation, paid by every user regardless of whether they enabled the feature. Split the else-branch in two: - _whiteKelvin == 0 (feature off, default): identical math to the pre-feature WLED code (w = min RGB, equal subtraction in ACCURATE). - _whiteKelvin > 0 (feature on): the per-channel-cap path that uses the cached W-LED RGB equivalent. Documents the underflow argument (floor division composes back through the subtraction) and the _wB == 0 case near 1900 K explicitly in the comments. Behavior is unchanged for both paths. Co-authored-by: Claude --- wled00/bus_manager.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 08ed860aad..41f3199218 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -128,12 +128,21 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) } else if (aWM == RGBW_MODE_MAX) { w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel + } else if (_whiteKelvin == 0) { + // Fast path: per-bus W-LED CCT feature is off. Identical to the + // pre-feature behavior — pick darkest RGB channel as W and (for + // ACCURATE) subtract it equally. Avoids three divisions per pixel + // in the default case, since most strips never enable the feature. + w = r < g ? (r < b ? r : b) : (g < b ? g : b); + if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } } else { // AI: below section was generated by an AI - // Per-channel cap: pick the largest w such that (w * _wX)/255 <= channel - // for every X in {R,G,B}, preventing underflow when subtracting the W - // LED's RGB contribution. When _whiteKelvin==0 the cached _wR/_wG/_wB - // are all 255, so this collapses to the legacy w = min(r,g,b). + // Per-channel cap path (feature on): pick the largest w such that + // (w * _wX)/255 <= channel for every X in {R,G,B}, preventing + // underflow when subtracting the W LED's RGB contribution. Floor + // division composes back through the subtract — i.e. + // floor((r*255)/_wR) * _wR <= r*255 — so the subtraction is safe. + // _wB can be 0 near 1900 K, hence the zero guards. unsigned wMaxR = _wR ? (r * 255U) / _wR : 255U; unsigned wMaxG = _wG ? (g * 255U) / _wG : 255U; unsigned wMaxB = _wB ? (b * 255U) / _wB : 255U; From cd00e562d5eaff00a7262da80c1388627745e8ea Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Thu, 28 May 2026 18:54:51 -0400 Subject: [PATCH 04/13] Refine W-channel CCT UI label and split onto separate lines Rename the enable checkbox label to "Tune RGB to W channel color temperature" so it mirrors the adjacent "Auto-calculate W channel from RGB" selector and reads as a refinement of it. Move the Kelvin number input out of the checkbox line into its own wrapper div (digwkv) with a dedicated "W channel color temperature:" label, matching the dominant WLED pattern of a checkbox revealing a sub-options block on the following line. UI() now toggles the wrapper's visibility instead of the bare input. Co-authored-by: Claude --- wled00/data/settings_leds.htm | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 36e66d2e51..819a1966df 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -206,11 +206,13 @@ if (ppl) d.Sf.MA.value = sumMA; // populate UI ABL value if PPL used } // AI: below section was generated by an AI - // Per-bus W-LED color temperature toggle. The number input is disabled - // (and hidden) when the checkbox is off, so it isn't submitted with - // the form — backend then sees no WK arg and stores wk=0 (legacy - // fast path). Seed the field to 6500 K when re-enabling from a blank - // or sub-min value so the UI default matches the sRGB white point. + // Per-bus W-LED color temperature toggle. The Kelvin input lives in a + // wrapper div (digwkv) that UI() shows/hides based on the checkbox; + // the input itself is also disabled when off, so it isn't submitted + // with the form — backend then sees no WK arg and stores wk=0 + // (legacy fast path). Seed the field to 6500 K when re-enabling from + // a blank or sub-min value so the UI default matches the sRGB white + // point. function wkChk(n) { const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; @@ -387,19 +389,20 @@ // W-channel CCT controls are only meaningful when autoWhiteCalc // uses the per-channel-cap path that consumes _wR/_wG/_wB — // i.e. AW mode is Brighter (1), Accurate (2), or Dual (3, where - // manual w==0 falls through to the Brighter path). Hide - // otherwise. The number input is disabled (and not submitted) - // when the WKE checkbox is off, so the backend stores wk=0 - // and the legacy autoWhite path is used. + // manual w==0 falls through to the Brighter path). Hide the + // whole toggle otherwise. The Kelvin input lives in a child + // block that's shown only when the checkbox is on; the input + // is disabled (and so not submitted) when off, so the backend + // stores wk=0 and the legacy autoWhite path is used. { const awEl = d.Sf["AW"+n]; const awv = awEl ? parseInt(awEl.value) : 0; const wkBox = gId("dig"+n+"wk"); if (wkBox) wkBox.style.display = (hasW(t) && (awv === 1 || awv === 2 || awv === 3)) ? "inline" : "none"; - const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; + const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n], wkv = gId("dig"+n+"wkv"); if (wke && wk) { wk.disabled = !wke.checked; - wk.style.display = wke.checked ? "inline" : "none"; + if (wkv) wkv.style.display = wke.checked ? "inline" : "none"; } } // AI: end @@ -627,7 +630,7 @@

Reversed:

Skip first LEDs:

Off Refresh:
-

Auto-calculate W channel from RGB:
 
+

Auto-calculate W channel from RGB:
`; f.insertAdjacentHTML("beforeend", cn); // fill led types (credit @netmindz) From cd6bf0e5155b686d40b9a961521085165be67802 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Thu, 28 May 2026 18:55:13 -0400 Subject: [PATCH 05/13] Lower W-channel CCT minimum from 1900 K to 1000 K Very warm white LEDs can read as neutral even when fed a fairly warm RGB mask, so users need to dial the configured temperature below the previous 1900 K floor to compensate. colorKtoRGB() is well-defined down to 1000 K (blue already clamps to 0 below 1900 K, green stays positive -> 255,68,0), and the per-channel-cap path's existing zero guards already handle the resulting zero channels. Lower the bound in the form validator (set.cpp), the number input's min attribute and the re-enable seed threshold (settings_leds.htm), and update the zero-guard comment in bus_manager.cpp to note that blue is zero across the whole sub-1900 K range now, not just near 1900 K. Co-authored-by: Claude --- wled00/bus_manager.cpp | 3 ++- wled00/data/settings_leds.htm | 4 ++-- wled00/set.cpp | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 41f3199218..4607ef8fef 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -142,7 +142,8 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { // underflow when subtracting the W LED's RGB contribution. Floor // division composes back through the subtract — i.e. // floor((r*255)/_wR) * _wR <= r*255 — so the subtraction is safe. - // _wB can be 0 near 1900 K, hence the zero guards. + // _wB is 0 at/below 1900 K (and _wG could reach 0 at extreme lows), + // hence the per-channel zero guards. unsigned wMaxR = _wR ? (r * 255U) / _wR : 255U; unsigned wMaxG = _wG ? (g * 255U) / _wG : 255U; unsigned wMaxB = _wB ? (b * 255U) / _wB : 255U; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 819a1966df..3fb2d93ad7 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -217,7 +217,7 @@ { const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; if (!wke || !wk) return; - if (wke.checked && parseInt(wk.value)<1900) wk.value = 6500; + if (wke.checked && parseInt(wk.value)<1000) wk.value = 6500; UI(); } // AI: end @@ -630,7 +630,7 @@

Reversed:

Skip first LEDs:

Off Refresh:
-

Auto-calculate W channel from RGB:
+

Auto-calculate W channel from RGB:
`; f.insertAdjacentHTML("beforeend", cn); // fill led types (credit @netmindz) diff --git a/wled00/set.cpp b/wled00/set.cpp index 81273ab5a4..1c0f53ee04 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -232,8 +232,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } awmode = request->arg(aw).toInt(); uint16_t whiteK = request->hasArg(wk) ? (uint16_t)request->arg(wk).toInt() : 0; - // Reject out-of-range or sub-1900K Kelvin values; 0 means "neutral/legacy" - if (whiteK != 0 && (whiteK < 1900 || whiteK > 10000)) whiteK = 0; + // Reject out-of-range or sub-1000K Kelvin values; 0 means "neutral/legacy" + if (whiteK != 0 && (whiteK < 1000 || whiteK > 10000)) whiteK = 0; uint16_t freq = request->arg(sp).toInt(); if (Bus::isPWM(type)) { switch (freq) { From 5f61870d0779c5d0466e0c1e13cece6657601dac Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Fri, 29 May 2026 12:29:04 -0400 Subject: [PATCH 06/13] Seed W-channel CCT field when re-enabling from a blank value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wkChk() promised to reseed the Kelvin field to 6500 K "from a blank or sub-min value", but parseInt("") is NaN and NaN < 1000 is false, so a cleared field was never reseeded — contradicting the comment. Invert the test to !(parsed >= 1000) so NaN (blank/non-numeric) also seeds, and add an explicit radix per the review. Spotted by CodeRabbit on #5654. Co-authored-by: Claude --- wled00/data/settings_leds.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 3fb2d93ad7..11609b1d43 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -217,7 +217,7 @@ { const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; if (!wke || !wk) return; - if (wke.checked && parseInt(wk.value)<1000) wk.value = 6500; + if (wke.checked && !(parseInt(wk.value, 10) >= 1000)) wk.value = 6500; UI(); } // AI: end From 5312bd0d48c71adaa44ce2bffc6dbca022bb6254 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Fri, 29 May 2026 12:42:40 -0400 Subject: [PATCH 07/13] Relabel W-channel CCT checkbox to reflect what it actually does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Tune RGB to W channel color temperature" implied the setting adjusts RGB, but it primarily changes how the W value is calculated (in Brighter/Accurate/Dual) and only modifies RGB in Accurate mode — in Brighter/Dual the RGB channels are left untouched. Rename to "Correct auto-white for W channel color temperature", which is accurate across all modes and matches the sibling "Auto-calculate W channel from RGB". Co-authored-by: Claude --- wled00/data/settings_leds.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 11609b1d43..58232c5e06 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -630,7 +630,7 @@

Reversed:

Skip first LEDs:

Off Refresh:
-

Auto-calculate W channel from RGB:
+

Auto-calculate W channel from RGB:
`; f.insertAdjacentHTML("beforeend", cn); // fill led types (credit @netmindz) From adefb08df83c845c668dd2bbe423866646a939aa Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Fri, 29 May 2026 13:36:35 -0400 Subject: [PATCH 08/13] Wrap loadCfg W-channel CCT block in standard AI markers The block that derives the WKE checkbox / WK seed from stored wk used a single inline "// AI:" comment instead of the start/end markers the other AI-generated sections use. Wrap it with the standard "// AI: below section was generated by an AI" / "// AI: end" pair for consistency. Comment-only; no behavior change. Spotted by CodeRabbit on #5654. Co-authored-by: Claude --- wled00/data/settings_leds.htm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 58232c5e06..860c615396 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -821,13 +821,16 @@ d.getElementsByName("RF"+i)[0].checked = v.ref; d.getElementsByName("CV"+i)[0].checked = v.rev; d.getElementsByName("AW"+i)[0].value = v.rgbwm; - { // AI: derive WKE checkbox + WK seed from stored wk (0 = feature off) + // AI: below section was generated by an AI + // derive WKE checkbox + WK seed from stored wk (0 = feature off) + { const wkChkEl = d.getElementsByName("WKE"+i)[0]; const wkEl = d.getElementsByName("WK"+i)[0]; const wkv = parseInt(v.wk) | 0; if (wkChkEl) wkChkEl.checked = wkv > 0; if (wkEl) wkEl.value = wkv > 0 ? wkv : 6500; } + // AI: end d.getElementsByName("WO"+i)[0].value = (v.order>>4) & 0x0F; d.getElementsByName("SP"+i)[0].value = v.freq; d.getElementsByName("LA"+i)[0].value = v.ledma; From 9965ca6047dbdb19fc9729fa9afe4e801d8defe8 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Sat, 30 May 2026 10:07:50 -0400 Subject: [PATCH 09/13] Restrict W-channel CCT correction to true RGBW bus types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fixed W-LED color temperature only makes sense for single-white RGBW buses. Dual-white CCT buses (RGBCW, RGB+CCT) have a variable white point set via the CCT/white-balance control, and non-RGB buses have nothing to derive the correction from — so the control was previously shown (gated on hasW only, like "Auto-calculate W channel from RGB") for bus types where it is meaningless or redundant. Tighten the UI gate to hasW && hasRGB && !hasCCT so the control appears only for true RGBW types. Also guard autoWhiteCalc: route _hasCCT and !_hasRgb buses through the legacy fast path even when wk != 0, so a hand-edited config can't trigger the per-channel-cap math on a bus type it doesn't apply to. The _whiteKelvin == 0 check stays first to keep the common feature-off path branch-cheap. Co-authored-by: Claude --- wled00/bus_manager.cpp | 13 ++++++++----- wled00/data/settings_leds.htm | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 24d707f25a..5ba7fd8e66 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -128,11 +128,14 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) } else if (aWM == RGBW_MODE_MAX) { w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel - } else if (_whiteKelvin == 0) { - // Fast path: per-bus W-LED CCT feature is off. Identical to the - // pre-feature behavior — pick darkest RGB channel as W and (for - // ACCURATE) subtract it equally. Avoids three divisions per pixel - // in the default case, since most strips never enable the feature. + } else if (_whiteKelvin == 0 || _hasCCT || !_hasRgb) { + // Fast path: per-bus W-LED CCT feature is off, OR the bus type can't + // use it — dual-white CCT buses have a variable white point set via + // the CCT control (not a single fixed Kelvin), and non-RGB buses have + // nothing to derive the correction from. Identical to the pre-feature + // behavior: pick darkest RGB channel as W and (for ACCURATE) subtract + // it equally. Also avoids three divisions per pixel in the common + // default case, since most strips never enable the feature. w = r < g ? (r < b ? r : b) : (g < b ? g : b); if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } } else { diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 860c615396..0c053be1e1 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -386,19 +386,22 @@ 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 // AI: below section was generated by an AI - // W-channel CCT controls are only meaningful when autoWhiteCalc - // uses the per-channel-cap path that consumes _wR/_wG/_wB — - // i.e. AW mode is Brighter (1), Accurate (2), or Dual (3, where - // manual w==0 falls through to the Brighter path). Hide the - // whole toggle otherwise. The Kelvin input lives in a child - // block that's shown only when the checkbox is on; the input - // is disabled (and so not submitted) when off, so the backend - // stores wk=0 and the legacy autoWhite path is used. + // W-channel CCT controls are only meaningful for true single-white + // RGBW buses (hasW && hasRGB && !hasCCT) AND when autoWhiteCalc uses + // the per-channel-cap path that consumes _wR/_wG/_wB — i.e. AW mode + // is Brighter (1), Accurate (2), or Dual (3, where manual w==0 falls + // through to the Brighter path). Hide the whole toggle otherwise. The + // Kelvin input lives in a child block that's shown only when the + // checkbox is on; the input is disabled (and so not submitted) when + // off, so the backend stores wk=0 and the legacy autoWhite path is used. { const awEl = d.Sf["AW"+n]; const awv = awEl ? parseInt(awEl.value) : 0; const wkBox = gId("dig"+n+"wk"); - if (wkBox) wkBox.style.display = (hasW(t) && (awv === 1 || awv === 2 || awv === 3)) ? "inline" : "none"; + // only true single-white RGBW types: a fixed W-LED color temperature is + // meaningless for dual-white CCT buses (variable white point) and for + // non-RGB buses (nothing to derive the correction from) + if (wkBox) wkBox.style.display = (hasW(t) && hasRGB(t) && !hasCCT(t) && (awv === 1 || awv === 2 || awv === 3)) ? "inline" : "none"; const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n], wkv = gId("dig"+n+"wkv"); if (wke && wk) { wk.disabled = !wke.checked; From c26fc178ec28619b49bd9e87d1fea50e0c4db059 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Sat, 30 May 2026 10:27:11 -0400 Subject: [PATCH 10/13] Disambiguate W-channel CCT gating comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "W-channel CCT controls" was ambiguous — "CCT" overlaps with WLED's separate dual-white white-balance system. Quote the actual checkbox label ("Correct auto-white for W channel color temperature") so the comment names this specific control unambiguously. Comment-only. Co-authored-by: Claude --- wled00/data/settings_leds.htm | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 0c053be1e1..9e1ba8a403 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -386,14 +386,15 @@ 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 // AI: below section was generated by an AI - // W-channel CCT controls are only meaningful for true single-white - // RGBW buses (hasW && hasRGB && !hasCCT) AND when autoWhiteCalc uses - // the per-channel-cap path that consumes _wR/_wG/_wB — i.e. AW mode - // is Brighter (1), Accurate (2), or Dual (3, where manual w==0 falls - // through to the Brighter path). Hide the whole toggle otherwise. The - // Kelvin input lives in a child block that's shown only when the - // checkbox is on; the input is disabled (and so not submitted) when - // off, so the backend stores wk=0 and the legacy autoWhite path is used. + // The "Correct auto-white for W channel color temperature" control is + // only meaningful for true single-white RGBW buses (hasW && hasRGB && + // !hasCCT) AND when autoWhiteCalc uses the per-channel-cap path that + // consumes _wR/_wG/_wB — i.e. AW mode is Brighter (1), Accurate (2), or + // Dual (3, where manual w==0 falls through to the Brighter path). Hide + // the whole toggle otherwise. The Kelvin input lives in a child block + // that's shown only when the checkbox is on; the input is disabled (and + // so not submitted) when off, so the backend stores wk=0 and the legacy + // autoWhite path is used. { const awEl = d.Sf["AW"+n]; const awv = awEl ? parseInt(awEl.value) : 0; From 18bdf30b7403d9ccc2fcaa7d401e7e421ade30e8 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Sat, 30 May 2026 10:48:01 -0400 Subject: [PATCH 11/13] Align W-channel feature comments to "W channel color temperature" The backend comments described the new feature as "W-channel CCT", which is ambiguous (collides with WLED's separate dual-white white-balance CCT system) and doesn't match the actual identifiers (_whiteKelvin, wk, WK, WKE). Rename to "W channel color temperature" across the comments added in this PR, matching the UI label. Pre-existing _cct/_cctBlend/ setSegmentCCT comments (the real white-balance system) are left as-is. Comment-only. Co-authored-by: Claude --- wled00/bus_manager.cpp | 2 +- wled00/bus_manager.h | 4 ++-- wled00/cfg.cpp | 2 +- wled00/set.cpp | 2 +- wled00/xml.cpp | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 5ba7fd8e66..dab0dcbbb1 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -129,7 +129,7 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { } else if (aWM == RGBW_MODE_MAX) { w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel } else if (_whiteKelvin == 0 || _hasCCT || !_hasRgb) { - // Fast path: per-bus W-LED CCT feature is off, OR the bus type can't + // Fast path: per-bus W channel color temperature feature is off, OR the bus type can't // use it — dual-white CCT buses have a variable white point set via // the CCT control (not a single fixed Kelvin), and non-RGB buses have // nothing to derive the correction from. Identical to the pre-feature diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index c84746b8db..500a069d50 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -228,7 +228,7 @@ class Bus { uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; - uint16_t _whiteKelvin; // physical W-channel CCT in Kelvin (0 = neutral/legacy behavior) + uint16_t _whiteKelvin; // physical W channel color temperature in Kelvin (0 = neutral/legacy behavior) uint8_t _wR; // cached W LED RGB equivalent (255,255,255 when _whiteKelvin==0) uint8_t _wG; uint8_t _wB; @@ -472,7 +472,7 @@ struct BusConfig { uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; - uint16_t whiteKelvin; // physical W-channel CCT in Kelvin (0 = neutral/legacy behavior) + uint16_t whiteKelvin; // physical W channel color temperature in Kelvin (0 = neutral/legacy behavior) uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; uint16_t frequency; uint8_t milliAmpsPerLed; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index a9a9a42a47..7746ccf4d7 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -238,7 +238,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; - uint16_t whiteK = elm[F("wk")] | 0; // physical W-channel CCT in K (0 = neutral/legacy) + uint16_t whiteK = elm[F("wk")] | 0; // physical W channel color temperature in K (0 = neutral/legacy) uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT; uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) diff --git a/wled00/set.cpp b/wled00/set.cpp index 1c0f53ee04..6c4f0bcbb8 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -205,7 +205,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip first N LEDs char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //refresh required char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode - char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W-channel CCT (Kelvin) + char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W channel color temperature (Kelvin) char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //channel swap char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 76f79f5444..00a084a948 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -371,8 +371,8 @@ void getSettingsJS(byte subPage, Print& settingsScript) char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip 1st LED char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //off refresh char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode - char wke[5] = "WKE"; wke[3] = offset+s; wke[4] = 0; //W-channel CCT enabled (UI checkbox) - char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W-channel CCT (Kelvin) + char wke[5] = "WKE"; wke[3] = offset+s; wke[4] = 0; //W channel color temperature enabled (UI checkbox) + char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W channel color temperature (Kelvin) char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //swap channels char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current From 988d833d60a404e35c9cb9687e131fb05bcb7cc3 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Sat, 30 May 2026 15:48:40 -0400 Subject: [PATCH 12/13] Optimize autoWhiteCalc hot path with precomputed reciprocals When the per-bus W channel color temperature feature is enabled, the per-channel-cap path divided each RGB channel by the runtime-variable _wR/_wG/_wB once per pixel (3 real divisions; the /255 terms use a constant divisor the compiler already strength-reduces). Integer division is multi-cycle on ESP32/ESP8266. Precompute Q15 fixed-point reciprocals of _wR/_wG/_wB once in setWhiteKelvin and replace the per-pixel divisions with a multiply + shift. The reciprocals are floor-biased so the derived w cap is never larger than the exact value, preserving underflow safety. Verified across all 65280 (channel,_wX) pairs (max error 1, no over-estimate) and 5.68M full-pipeline cases (<=1/255 output delta). S=15 keeps the worst-case product (channel=255, _wX=1) inside uint32 with ~50% margin at the same accuracy as S=16. Addresses maintainer review feedback on hot-path speed. Co-authored-by: Claude --- wled00/bus_manager.cpp | 26 +++++++++++++++++++++----- wled00/bus_manager.h | 3 +++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index dab0dcbbb1..13be789364 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -111,6 +111,16 @@ void Bus::setWhiteKelvin(uint16_t k) { colorKtoRGB(k, rgb); _wR = rgb[0]; _wG = rgb[1]; _wB = rgb[2]; } + // Precompute fixed-point reciprocals (Q15) so autoWhiteCalc's hot path can + // replace the per-pixel division (channel*255)/_wX with (channel*_rwX)>>15. + // floor() here under-estimates the reciprocal, so the derived w cap can only + // come out <= the true floor value — never larger — keeping the subtraction + // underflow-safe (verified exact across all channel/_wX combinations: max + // error 1, no over-estimate; max product 255*_rwX fits uint32). _rwX==0 is the + // "channel does not constrain w" sentinel, matching the _wX==0 guard below. + _rwR = _wR ? ((255U << 15) / _wR) : 0; + _rwG = _wG ? ((255U << 15) / _wG) : 0; + _rwB = _wB ? ((255U << 15) / _wB) : 0; } // AI: end @@ -145,11 +155,17 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { // underflow when subtracting the W LED's RGB contribution. Floor // division composes back through the subtract — i.e. // floor((r*255)/_wR) * _wR <= r*255 — so the subtraction is safe. - // _wB is 0 at/below 1900 K (and _wG could reach 0 at extreme lows), - // hence the per-channel zero guards. - unsigned wMaxR = _wR ? (r * 255U) / _wR : 255U; - unsigned wMaxG = _wG ? (g * 255U) / _wG : 255U; - unsigned wMaxB = _wB ? (b * 255U) / _wB : 255U; + // Hot path: the (channel*255)/_wX divisions are replaced by the Q15 + // reciprocals precomputed in setWhiteKelvin (multiply + shift, no + // per-pixel divide). _rwX is floor-biased so wMax never over-estimates, + // keeping the cap underflow-safe. _rwX==0 means _wX==0 (channel doesn't + // constrain w — _wB is 0 at/below 1900 K, _wG only at extreme lows), + // matching the previous per-channel zero guards. The /255 in the + // subtraction stays: 255 is a compile-time constant the compiler already + // strength-reduces, so it isn't an actual division. + unsigned wMaxR = _rwR ? (r * _rwR) >> 15 : 255U; + unsigned wMaxG = _rwG ? (g * _rwG) >> 15 : 255U; + unsigned wMaxB = _rwB ? (b * _rwB) >> 15 : 255U; unsigned wCap = wMaxR < wMaxG ? (wMaxR < wMaxB ? wMaxR : wMaxB) : (wMaxG < wMaxB ? wMaxG : wMaxB); if (wCap > 255U) wCap = 255U; w = wCap; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 500a069d50..bdd49cf65b 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -232,6 +232,9 @@ class Bus { uint8_t _wR; // cached W LED RGB equivalent (255,255,255 when _whiteKelvin==0) uint8_t _wG; uint8_t _wB; + uint32_t _rwR; // Q15 reciprocal of _wR (floor((255<<15)/_wR), 0 if _wR==0) for autoWhiteCalc hot path + uint32_t _rwG; + uint32_t _rwB; //struct { //using bitfield struct adds abour 250 bytes to binary size bool _reversed;// : 1; bool _valid;// : 1; From d9881b831bb0add24bc58d4d442ebb995b9032f2 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Tue, 9 Jun 2026 14:54:11 -0400 Subject: [PATCH 13/13] Address review: init reciprocal cache, honor global auto-white override - Initialize _rwR/_rwG/_rwB in the Bus constructor init list (Q15 reciprocal of 255, consistent with _wR/_wG/_wB=255). They were the only cached members left uninitialized; harmless today since they are only read once _whiteKelvin != 0 (set together with them in setWhiteKelvin), but defensive. - Show the per-bus W channel color temperature control when EITHER the per-bus auto-white mode OR the global auto-white override (Bus::_gAWM) is Brighter/ Accurate/Dual, and add onchange="UI()" to the global override select so it updates live. Previously the control ignored the global override; the per-bus AW selector stays visible under an override, so the Kelvin sub-control now matches it rather than vanishing. Spotted by CodeRabbit on #5654. Co-authored-by: Claude --- wled00/bus_manager.h | 3 +++ wled00/data/settings_leds.htm | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index bdd49cf65b..1bea5ef5f9 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -126,6 +126,9 @@ class Bus { , _wR(255) , _wG(255) , _wB(255) + , _rwR(1u << 15) // Q15 reciprocal of _wR=255 (kept consistent though unused while _whiteKelvin==0) + , _rwG(1u << 15) + , _rwB(1u << 15) { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 9e1ba8a403..5eec46c15d 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -398,11 +398,19 @@ { const awEl = d.Sf["AW"+n]; const awv = awEl ? parseInt(awEl.value) : 0; + // The per-channel-cap path runs when the auto-white mode is Brighter/ + // Accurate/Dual, which can be set per-bus (AW) or via the global override + // (AW; 255=Disabled). The per-bus AW selector stays visible even under a + // global override, so keep this control consistent with it: show when EITHER + // the per-bus mode OR the global override is one of those modes. + const gAWel = d.Sf["AW"]; + const gAW = gAWel ? parseInt(gAWel.value) : 255; + const isCap = (m) => (m === 1 || m === 2 || m === 3); const wkBox = gId("dig"+n+"wk"); // only true single-white RGBW types: a fixed W-LED color temperature is // meaningless for dual-white CCT buses (variable white point) and for // non-RGB buses (nothing to derive the correction from) - if (wkBox) wkBox.style.display = (hasW(t) && hasRGB(t) && !hasCCT(t) && (awv === 1 || awv === 2 || awv === 3)) ? "inline" : "none"; + if (wkBox) wkBox.style.display = (hasW(t) && hasRGB(t) && !hasCCT(t) && (isCap(awv) || isCap(gAW))) ? "inline" : "none"; const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n], wkv = gId("dig"+n+"wkv"); if (wke && wk) { wk.disabled = !wke.checked; @@ -1123,7 +1131,7 @@

Color & White

White Balance correction:
Global override for Auto-calculate white: -