From d7e45b4805368829b3581548c33a51213e4fcf0d Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Mon, 4 May 2026 14:41:47 -0400 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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: - From 5d4b69fac77a2c1877dd5094743563ea0370c15b Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Wed, 9 Sep 2026 13:15:38 -0400 Subject: [PATCH 14/22] Make Kelvin auto-white hot path branch-free and division-free Address review feedback on the per-bus W colour temperature path in autoWhiteCalc(): - Cache Q15 reciprocals of (_wX + 1) instead of _wX. The +1 keeps the divisor non-zero, so the three per-channel zero-guard ternaries and the _rwX == 0 sentinel are gone. Range is 128..32768, so the reciprocals shrink from uint32_t to uint16_t. - Compute the per-channel W cap as ((x+1)*256-1) * rw >> 15, the largest w whose contribution (w*(_wX+1))>>8 fits in the channel. - Subtract the W LED's contribution with (w*(_wX+1))>>8, following the (x+1)*bri>>8 convention already used in this file, instead of /255. Verified exhaustively on the host: no underflow for any colour at any Kelvin from 1000 to 10000 K, and outputs match the previous arithmetic to within a few units except below ~2350 K where a near-zero blue coefficient now allows slightly more W for zero-blue inputs (the old cap dropped to 0 there, giving a hard step at 1900 K). Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.cpp | 56 ++++++++++++++++++++---------------------- wled00/bus_manager.h | 12 ++++----- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 69a3fe6cd4..e6eea20815 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -111,16 +111,17 @@ 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; + // Precompute Q15 reciprocals of (_wX+1) so autoWhiteCalc's hot path has no + // per-pixel division and no branches. Scaling by (_wX+1)>>8 instead of + // _wX/255 follows the (x+1)*bri>>8 convention used elsewhere in this file, + // and the +1 keeps the divisor non-zero (so _wB==0 at/below 1900 K needs no + // guard). Range is 32768/256=128 .. 32768/1=32768, which fits uint16_t. + // floor() under-estimates the reciprocal, so the w cap derived from it can + // only be <= the exact value, never larger, keeping the subtraction + // underflow-safe (verified exhaustively over all channel/_wX pairs). + _rwR = 32768U / (_wR + 1U); + _rwG = 32768U / (_wG + 1U); + _rwB = 32768U / (_wB + 1U); } // AI: end @@ -150,29 +151,26 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } } else { // AI: below section was generated by an AI - // 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. - // 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; + // Per-channel cap path (feature on): pick the largest w whose W-LED + // contribution (w*(_wX+1))>>8 does not exceed the channel for every X + // in {R,G,B}, so subtracting that contribution can't underflow. The + // largest such w is ((x+1)*256-1)/(_wX+1); the division is done via the + // Q15 reciprocal of (_wX+1) precomputed in setWhiteKelvin (multiply + + // shift only, no per-pixel divide, no branches). The reciprocal is + // floor-biased so wMax never over-estimates. Max product + // (256*256-1)*32768 fits in 32 bits. A zero coefficient (_wB at/below + // 1900 K) gives reciprocal 32768 and a cap >= 255, i.e. that channel + // does not constrain w — same as the previous explicit guard. + unsigned wMaxR = (((r + 1U) << 8) - 1U) * _rwR >> 15; + unsigned wMaxG = (((g + 1U) << 8) - 1U) * _rwG >> 15; + unsigned wMaxB = (((b + 1U) << 8) - 1U) * _rwB >> 15; 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 + r -= (w * (_wR + 1U)) >> 8; // subtract W LED's R contribution + g -= (w * (_wG + 1U)) >> 8; // subtract W LED's G contribution + b -= (w * (_wB + 1U)) >> 8; // subtract W LED's B contribution } // AI: end } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index fd2fdf2bc8..9cdf496138 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -126,9 +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) + , _rwR(32768 / 256) // Q15 reciprocal of (_wR+1) for _wR=255 (kept consistent though unused while _whiteKelvin==0) + , _rwG(32768 / 256) + , _rwB(32768 / 256) { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; @@ -234,9 +234,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; + uint16_t _rwR; // Q15 reciprocal of (_wR+1), i.e. 32768/(_wR+1) in [128,32768], for autoWhiteCalc hot path + uint16_t _rwG; + uint16_t _rwB; //struct { //using bitfield struct adds abour 250 bytes to binary size bool _reversed;// : 1; bool _valid;// : 1; From 2f56fde079a51c339aebbd28f3ec45c91a1689af Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Wed, 9 Sep 2026 14:05:55 -0400 Subject: [PATCH 15/22] Order Bus Kelvin members by size to avoid padding Group the three uint16_t reciprocals with the existing 16-bit members and the three uint8_t W-LED coefficients with the bools, removing the padding byte the previous order introduced before _rwR. Constructor initializer list reordered to match declaration order. Verified with clang's record-layout dump on a 32-bit mirror of the class: data size drops from 30 to 29 bytes; sizeof(Bus) stays 32, but derived classes gain a byte of usable tail padding. Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.h | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 9cdf496138..957c08cb1c 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -119,16 +119,16 @@ class Bus { , _NPBbri(255) , _start(start) , _len(std::max(len,(uint16_t)1)) - , _reversed(reversed) - , _valid(false) - , _needsRefresh(refresh) , _whiteKelvin(0) - , _wR(255) - , _wG(255) - , _wB(255) , _rwR(32768 / 256) // Q15 reciprocal of (_wR+1) for _wR=255 (kept consistent though unused while _whiteKelvin==0) , _rwG(32768 / 256) , _rwB(32768 / 256) + , _wR(255) + , _wG(255) + , _wB(255) + , _reversed(reversed) + , _valid(false) + , _needsRefresh(refresh) { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; @@ -230,13 +230,14 @@ class Bus { uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; + // 16-bit members grouped with _start/_len, 8-bit ones with the bools below, to avoid padding 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; uint16_t _rwR; // Q15 reciprocal of (_wR+1), i.e. 32768/(_wR+1) in [128,32768], for autoWhiteCalc hot path uint16_t _rwG; uint16_t _rwB; + 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; From 33817dc380f3cb18dd2162364985ca296e12aab6 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Wed, 9 Sep 2026 15:05:16 -0400 Subject: [PATCH 16/22] Enforce W Kelvin bus eligibility in setter instead of hot path setWhiteKelvin() now stores 0 for buses with CCT, without RGB, or without a W channel, so the per-pixel path in autoWhiteCalc only tests _whiteKelvin == 0. The settings page already hides the control for those bus types, so UI and firmware enforce the same rule. Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index e6eea20815..e732f36caf 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -103,6 +103,12 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { // 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) { + // The correction only makes sense for RGB+W buses with a single fixed white: + // dual-white CCT buses have a variable white point set via the CCT control, + // and buses without RGB or without W have nothing to correct. Force the + // feature off here (once, at configuration) so autoWhiteCalc's per-pixel + // path only has to test _whiteKelvin == 0. + if (_hasCCT || !_hasRgb || !_hasWhite) k = 0; _whiteKelvin = k; if (k == 0) { _wR = _wG = _wB = 255; // legacy: treat W as neutral @@ -139,14 +145,12 @@ 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 || _hasCCT || !_hasRgb) { - // 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 - // 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. + } else if (_whiteKelvin == 0) { + // Fast path: per-bus W channel color temperature feature is off + // (setWhiteKelvin also forces it off for bus types that can't use it). + // Identical to the pre-feature behavior: pick darkest RGB channel as W + // and (for ACCURATE) subtract it equally. Most strips never enable the + // feature, so this is the common default case. w = r < g ? (r < b ? r : b) : (g < b ? g : b); if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } } else { From 90096412b14ddf84ce37611fb2c1a1e85f1a9f44 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Thu, 10 Sep 2026 12:58:29 -0400 Subject: [PATCH 17/22] Drop WKE flag; WK=0 means W Kelvin correction off The settings JSON no longer carries a separate enabled flag: getSettingsJS() emits only WK, where 0 means the correction is off. The checkbox in settings_leds.htm is UI-only (id, no name, never submitted) and is derived from WK once at load by wkInit(). A disabled Kelvin input is not submitted, so the backend already stores 0 when the box is unticked. loadCfg() normalises an imported wk to 1000..10000 or 0, matching set.cpp. Verified on a QuinLED Dig-Uno: fresh load, untick/save/reload (wk=0), re-tick seeds 6500, set 4000/save/reload (wk=4000). Co-Authored-By: Claude Fable 5.1 --- wled00/data/settings_leds.htm | 39 ++++++++++++++++++++++------------- wled00/xml.cpp | 6 ++---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 8e7799d840..6e77e19368 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -51,6 +51,7 @@ d.max_gpio = 50; }, ()=>{ checkSi(); + wkInit(); setABL(); d.Sf.addEventListener("submit", trySubmit); if (d.um_p[0]==-1) d.um_p.shift(); @@ -205,20 +206,29 @@ 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 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. + // Per-bus W-LED color temperature toggle. The checkbox is UI-only (no + // name, so it is never submitted): the backend knows only WK, where + // 0 means off. 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 enabling from 0/blank/sub-min so the UI default matches + // the sRGB white point. function wkChk(n) { - const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; + const wke = gId("wke"+n), wk = d.Sf["WK"+n]; if (!wke || !wk) return; if (wke.checked && !(parseInt(wk.value, 10) >= 1000)) wk.value = 6500; UI(); } + // derive each bus's checkbox from the WK value loaded from the device (once, at load) + function wkInit() + { + d.Sf.querySelectorAll("input[name^=WK]").forEach((wk) => { + const wke = gId("wke"+wk.name.substring(2)); + if (wke) wke.checked = parseInt(wk.value, 10) > 0; + }); + } // AI: end // enable and update LED Amps function enLA(s,n) @@ -410,7 +420,7 @@ // 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) && (isCap(awv) || isCap(gAW))) ? "inline" : "none"; - const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n], wkv = gId("dig"+n+"wkv"); + const wke = gId("wke"+n), wk = d.Sf["WK"+n], wkv = gId("dig"+n+"wkv"); if (wke && wk) { wk.disabled = !wke.checked; if (wkv) wkv.style.display = wke.checked ? "inline" : "none"; @@ -637,7 +647,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) @@ -829,13 +839,14 @@ d.getElementsByName("CV"+i)[0].checked = v.rev; d.getElementsByName("AW"+i)[0].value = v.rgbwm; // AI: below section was generated by an AI - // derive WKE checkbox + WK seed from stored wk (0 = feature off) + // stored wk: 0 = feature off; normalise like the backend (1000..10000 else 0) { - const wkChkEl = d.getElementsByName("WKE"+i)[0]; + const wkChkEl = gId("wke"+i); const wkEl = d.getElementsByName("WK"+i)[0]; - const wkv = parseInt(v.wk) | 0; + const raw = parseInt(v.wk, 10) | 0; + const wkv = (raw >= 1000 && raw <= 10000) ? raw : 0; if (wkChkEl) wkChkEl.checked = wkv > 0; - if (wkEl) wkEl.value = wkv > 0 ? wkv : 6500; + if (wkEl) wkEl.value = wkv; } // AI: end d.getElementsByName("WO"+i)[0].value = (v.order>>4) & 0x0F; diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 33fc1bff76..e014b8c6b0 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -376,8 +376,7 @@ 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 color temperature enabled (UI checkbox) - char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W channel color temperature (Kelvin) + char wk[4] = "WK"; wk[2] = offset+s; wk[3] = 0; //W channel color temperature (Kelvin, 0 = off) 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 @@ -399,8 +398,7 @@ 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,wk,bus->getWhiteKelvin()); printSetFormValue(settingsScript,wo,bus->getColorOrder() >> 4); unsigned speed = bus->getFrequency(); if (bus->isPWM()) { From 011b035aa627c5eca69b06340f8b1c266c364375 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Thu, 10 Sep 2026 13:30:28 -0400 Subject: [PATCH 18/22] Apply W Kelvin correction in Accurate mode only Brighter, and Dual when manual W is 0 (which upstream treats as Brighter), never subtract W from RGB, so the per-channel cap had nothing to correct there; it only changed how much W was used (at 3000 K, +34 on average and up to +145 for two thirds of all colours versus min(r,g,b)). Take the legacy fast path unless the effective auto-white mode is Accurate, and offer the settings control only when the per-bus or global mode is Accurate. Brighter, Dual and Max are now byte-for-byte upstream behaviour regardless of the Kelvin setting. Verified on a QuinLED Dig-Uno: control visibility across all per-bus and global-override modes. Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.cpp | 12 +++++++----- wled00/data/settings_leds.htm | 15 +++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index e732f36caf..7b41dcdfd7 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -145,12 +145,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) { + } else if (_whiteKelvin == 0 || aWM != RGBW_MODE_AUTO_ACCURATE) { // Fast path: per-bus W channel color temperature feature is off - // (setWhiteKelvin also forces it off for bus types that can't use it). - // Identical to the pre-feature behavior: pick darkest RGB channel as W - // and (for ACCURATE) subtract it equally. Most strips never enable the - // feature, so this is the common default case. + // (setWhiteKelvin also forces it off for bus types that can't use it), + // or mode is BRIGHTER / DUAL-with-w==0, which never subtract W from RGB + // so the W-LED colour has nothing to correct. Identical to the + // pre-feature behavior: pick darkest RGB channel as W and (for ACCURATE) + // subtract it equally. Most strips never enable the feature, so this is + // the common default. 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 6e77e19368..04721bcda3 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -398,8 +398,10 @@ // 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 + // consumes _wR/_wG/_wB — i.e. AW mode is Accurate (2). + // Brighter (1) and Dual (3, which is Brighter when manual w==0) never + // subtract W from RGB, so they stay on plain min(r,g,b) and the + // control is not offered for them. 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 @@ -407,14 +409,15 @@ { 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 + // The per-channel-cap path runs only in Accurate mode: Brighter, and Dual + // with w==0, never subtract W from RGB, so there is nothing to correct. + // The mode 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. + // the per-bus mode OR the global override is Accurate. const gAWel = d.Sf["AW"]; const gAW = gAWel ? parseInt(gAWel.value) : 255; - const isCap = (m) => (m === 1 || m === 2 || m === 3); + const isCap = (m) => (m === 2); 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 From f010e005078601f9026e09f89d12c79c8812ba2b Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Thu, 10 Sep 2026 14:32:03 -0400 Subject: [PATCH 19/22] Validate W Kelvin at both ingress points before narrowing Parse wk into a wide signed type, accept 1000..10000 K, otherwise store 0 (off), then narrow to uint16_t. Previously set.cpp narrowed before the range check, so an oversized form value could wrap into range, and cfg.cpp applied no range check at all, so /json/cfg could persist values the settings form rejects. Also drops the redundant != 0 test. The setter itself is unchanged: the range is a UI convention, not a physical limit. Co-Authored-By: Claude Fable 5.1 --- wled00/cfg.cpp | 3 ++- wled00/set.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index c79d18d0dd..9c2443e4f1 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -246,7 +246,8 @@ 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 color temperature in K (0 = neutral/legacy) + int wkRaw = elm[F("wk")] | 0; // physical W channel color temperature in K + uint16_t whiteK = (wkRaw >= 1000 && wkRaw <= 10000) ? (uint16_t)wkRaw : 0; // 0 or out of range = off (same rule as set.cpp) uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT; uint16_t maMax = elm[F("maxpwr")] | (total > 0 ? (ablMilliampsMax * length) / total : ablMilliampsMax); // 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 3278d8c9cb..ae47028c62 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -252,9 +252,10 @@ 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-1000K Kelvin values; 0 means "neutral/legacy" - if (whiteK != 0 && (whiteK < 1000 || whiteK > 10000)) whiteK = 0; + // W channel color temperature: 0 or out of range (1000..10000 K) means off. + // Validate before narrowing so oversized values can't wrap into range. + long wkRaw = request->hasArg(wk) ? request->arg(wk).toInt() : 0; + uint16_t whiteK = (wkRaw >= 1000 && wkRaw <= 10000) ? (uint16_t)wkRaw : 0; uint16_t freq = request->arg(sp).toInt(); if (Bus::isPWM(type)) { switch (freq) { From fbe93c1644f9665888ce745e9c696353a025d469 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Thu, 10 Sep 2026 14:36:55 -0400 Subject: [PATCH 20/22] Add source attribution to AI-marked blocks in bus_manager.cpp Name the tool and the sources the Kelvin auto-white code drew on: colorKtoRGB() (Tanner Helland's approximation) and the existing (x+1)>>8 brightness scaling in this file. No external code was copied. Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 7b41dcdfd7..330dc9e7a2 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -98,7 +98,11 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { cw = (w * cw) / 255; } -// AI: below section was generated by an AI +// AI: below section was generated by an AI (Claude, Anthropic), reviewed by the contributor. +// Sources: Kelvin->RGB comes from colorKtoRGB() in colors.cpp (Tanner Helland's +// approximation, https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html); +// the Q15 reciprocal and (x+1)>>8 scaling follow the brightness scaling already used in this +// file. No external code was copied. // 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. @@ -156,7 +160,9 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { 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 + // AI: below section was generated by an AI (Claude, Anthropic), reviewed by the contributor. + // Sources: none external; the cap/subtract math was derived for this change and verified + // exhaustively on the host (all 2^24 colours x 1000..10000 K), see PR #5654. // Per-channel cap path (feature on): pick the largest w whose W-LED // contribution (w*(_wX+1))>>8 does not exceed the channel for every X // in {R,G,B}, so subtracting that contribution can't underflow. The From fba65094f1731b48d9debcf3e3d8b46409d7d13e Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Fri, 11 Sep 2026 15:44:00 -0400 Subject: [PATCH 21/22] Name the Q15 constants used by the W Kelvin correction Replace the 32768 / 256 initialiser and the bare 32768 and >> 15 literals with WK_Q15_ONE, WK_Q15_SHIFT and WK_RECIP_NEUTRAL, defined once in Bus with a comment explaining the Q15 scale and why the neutral reciprocal is 128. No functional change. Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.cpp | 12 ++++++------ wled00/bus_manager.h | 14 ++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 330dc9e7a2..5d925772b7 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -129,9 +129,9 @@ void Bus::setWhiteKelvin(uint16_t k) { // floor() under-estimates the reciprocal, so the w cap derived from it can // only be <= the exact value, never larger, keeping the subtraction // underflow-safe (verified exhaustively over all channel/_wX pairs). - _rwR = 32768U / (_wR + 1U); - _rwG = 32768U / (_wG + 1U); - _rwB = 32768U / (_wB + 1U); + _rwR = WK_Q15_ONE / (_wR + 1U); + _rwG = WK_Q15_ONE / (_wG + 1U); + _rwB = WK_Q15_ONE / (_wB + 1U); } // AI: end @@ -173,9 +173,9 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { // (256*256-1)*32768 fits in 32 bits. A zero coefficient (_wB at/below // 1900 K) gives reciprocal 32768 and a cap >= 255, i.e. that channel // does not constrain w — same as the previous explicit guard. - unsigned wMaxR = (((r + 1U) << 8) - 1U) * _rwR >> 15; - unsigned wMaxG = (((g + 1U) << 8) - 1U) * _rwG >> 15; - unsigned wMaxB = (((b + 1U) << 8) - 1U) * _rwB >> 15; + unsigned wMaxR = (((r + 1U) << 8) - 1U) * _rwR >> WK_Q15_SHIFT; + unsigned wMaxG = (((g + 1U) << 8) - 1U) * _rwG >> WK_Q15_SHIFT; + unsigned wMaxB = (((b + 1U) << 8) - 1U) * _rwB >> WK_Q15_SHIFT; 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 957c08cb1c..98bf7fa016 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -120,9 +120,9 @@ class Bus { , _start(start) , _len(std::max(len,(uint16_t)1)) , _whiteKelvin(0) - , _rwR(32768 / 256) // Q15 reciprocal of (_wR+1) for _wR=255 (kept consistent though unused while _whiteKelvin==0) - , _rwG(32768 / 256) - , _rwB(32768 / 256) + , _rwR(WK_RECIP_NEUTRAL) // matches _wR=255 (kept consistent though unused while _whiteKelvin==0) + , _rwG(WK_RECIP_NEUTRAL) + , _rwB(WK_RECIP_NEUTRAL) , _wR(255) , _wG(255) , _wB(255) @@ -230,9 +230,15 @@ class Bus { uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; + // Q15 fixed point used by the W Kelvin correction in autoWhiteCalc(): _rwX = WK_Q15_ONE / (_wX + 1), + // consumed as (value * _rwX) >> WK_Q15_SHIFT. WK_RECIP_NEUTRAL is the reciprocal for a neutral + // (255) coefficient, i.e. the W LED treated as pure white: 32768 / 256 = 128. + static constexpr unsigned WK_Q15_SHIFT = 15; + static constexpr uint16_t WK_Q15_ONE = 1u << WK_Q15_SHIFT; + static constexpr uint16_t WK_RECIP_NEUTRAL = WK_Q15_ONE / (255 + 1); // 16-bit members grouped with _start/_len, 8-bit ones with the bools below, to avoid padding uint16_t _whiteKelvin; // physical W channel color temperature in Kelvin (0 = neutral/legacy behavior) - uint16_t _rwR; // Q15 reciprocal of (_wR+1), i.e. 32768/(_wR+1) in [128,32768], for autoWhiteCalc hot path + uint16_t _rwR; // Q15 reciprocal of (_wR+1), i.e. WK_Q15_ONE/(_wR+1) in [128,32768], for autoWhiteCalc hot path uint16_t _rwG; uint16_t _rwB; uint8_t _wR; // cached W LED RGB equivalent (255,255,255 when _whiteKelvin==0) From eb60a82cb72ff0974a1834b72945c11833fa5ac7 Mon Sep 17 00:00:00 2001 From: Christian Kunis Date: Tue, 22 Sep 2026 13:30:22 -0400 Subject: [PATCH 22/22] Initialise Bus capability flags; harden imported wk against 32-bit wrap BusPlaceholder never set _hasRgb/_hasWhite/_hasCCT and BusHub75Matrix never set _hasCCT, so the eligibility check added to setWhiteKelvin() read indeterminate memory for those bus types. Initialise all three to false in the base Bus constructor; derived constructors still override. In the settings page config import, parseInt(...) | 0 truncated to signed 32 bits before the range check, so 4294973796 became 6500 and passed. Use Number() and Number.isInteger() so the 1000..10000 rule is applied to the actual value (fractional values are now rejected too). Co-Authored-By: Claude Fable 5.1 --- wled00/bus_manager.h | 3 +++ wled00/data/settings_leds.htm | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 98bf7fa016..4111cb7315 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -129,6 +129,9 @@ class Bus { , _reversed(reversed) , _valid(false) , _needsRefresh(refresh) + , _hasRgb(false) // derived constructors set the real capabilities; + , _hasWhite(false) // defaults keep setWhiteKelvin() well-defined for buses + , _hasCCT(false) // that don't set all three (BusPlaceholder, BusHub75Matrix) { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 04721bcda3..fd2046ba2a 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -846,8 +846,8 @@ { const wkChkEl = gId("wke"+i); const wkEl = d.getElementsByName("WK"+i)[0]; - const raw = parseInt(v.wk, 10) | 0; - const wkv = (raw >= 1000 && raw <= 10000) ? raw : 0; + const raw = Number(v.wk); // no |0: that wraps at 2^32 before the range check + const wkv = (Number.isInteger(raw) && raw >= 1000 && raw <= 10000) ? raw : 0; if (wkChkEl) wkChkEl.checked = wkv > 0; if (wkEl) wkEl.value = wkv; }