diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 8b9825c4a5..13be789364 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -98,6 +98,32 @@ 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]; + } + // 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 + // 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; @@ -112,9 +138,43 @@ 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. + w = r < g ? (r < b ? r : b) : (g < b ? g : b); + if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } } 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 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; + 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); } @@ -1226,6 +1286,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..1bea5ef5f9 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,13 @@ class Bus { , _reversed(reversed) , _valid(false) , _needsRefresh(refresh) + , _whiteKelvin(0) + , _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; }; @@ -162,6 +170,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 +231,13 @@ class Bus { uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; + 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; + 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; @@ -461,6 +478,7 @@ struct BusConfig { uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; + 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; @@ -469,13 +487,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..7746ccf4d7 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 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) @@ -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..5eec46c15d 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -205,6 +205,22 @@ }); 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. + function wkChk(n) + { + const wke = d.Sf["WKE"+n], wk = d.Sf["WK"+n]; + if (!wke || !wk) return; + if (wke.checked && !(parseInt(wk.value, 10) >= 1000)) wk.value = 6500; + UI(); + } + // AI: end // enable and update LED Amps function enLA(s,n) { @@ -369,6 +385,39 @@ 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 + // 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; + // 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) && (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; + if (wkv) wkv.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 +642,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 +833,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: 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; @@ -1072,7 +1131,7 @@

Color & White

White Balance correction:
Global override for Auto-calculate white: - diff --git a/wled00/set.cpp b/wled00/set.cpp index fb516ac7d6..6c4f0bcbb8 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 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 @@ -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-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) { @@ -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..00a084a948 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 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 @@ -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()) {