diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 8b9825c4a5..24d707f25a 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; @@ -112,9 +128,34 @@ 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 { - 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. + // _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; + 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 +1267,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..860c615396 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,27 @@ 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 + // 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"; + 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 +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) @@ -784,6 +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: 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; diff --git a/wled00/set.cpp b/wled00/set.cpp index fb516ac7d6..1c0f53ee04 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-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..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()) {