Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d7e45b4
Add per-bus W-channel CCT for accurate auto-white calculation
NerdyGriffin May 4, 2026
01fe430
Show W-LED CCT controls in DUAL auto-white mode too
NerdyGriffin May 25, 2026
113b3dc
Add fast path to autoWhiteCalc when per-bus CCT feature is off
NerdyGriffin May 25, 2026
cd00e56
Refine W-channel CCT UI label and split onto separate lines
NerdyGriffin May 28, 2026
cd6bf0e
Lower W-channel CCT minimum from 1900 K to 1000 K
NerdyGriffin May 28, 2026
38d19b0
Merge branch 'main' into claude/wled-cct-conversion-research-qmgU7
softhack007 May 29, 2026
5f61870
Seed W-channel CCT field when re-enabling from a blank value
NerdyGriffin May 29, 2026
b42e4e1
Merge updated PR branch (main sync) into local wkChk fix
NerdyGriffin May 29, 2026
5312bd0
Relabel W-channel CCT checkbox to reflect what it actually does
NerdyGriffin May 29, 2026
15016d6
Merge branch 'main' into claude/wled-cct-conversion-research-qmgU7
NerdyGriffin May 29, 2026
adefb08
Wrap loadCfg W-channel CCT block in standard AI markers
NerdyGriffin May 29, 2026
7f7e78a
Merge updated PR branch (main sync) into local AI-marker fix
NerdyGriffin May 29, 2026
9965ca6
Restrict W-channel CCT correction to true RGBW bus types
NerdyGriffin May 30, 2026
c26fc17
Disambiguate W-channel CCT gating comment
NerdyGriffin May 30, 2026
18bdf30
Align W-channel feature comments to "W channel color temperature"
NerdyGriffin May 30, 2026
988d833
Optimize autoWhiteCalc hot path with precomputed reciprocals
NerdyGriffin May 30, 2026
d9881b8
Address review: init reciprocal cache, honor global auto-white override
NerdyGriffin Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions wled00/bus_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
// 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;
Expand All @@ -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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes the MODE_MIN behaviour. intentional? if so why only min and not MODE_MAX?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was primarily intended for MODE_AUTO_ACCURATE and DUAL because they advertise themselves as an "accurate" translation of RGB into RGBW (and ACCURATE is the mode I use on my daily-driver WLED controllers). But it may be extended to include MODE_MAX if desired, although the RGB + whiteKelvin -> RGBW math might need to be different to match the intent of MODE_MAX.
I originally assumed MODE_MAX didn't need it because it isn't concerned about color accuracy, but I am open to corrections/suggestions.

Please let me know if I am misunderstanding what you are referring to as MODE_MIN and MODE_MAX

if (aWM == RGBW_MODE_AUTO_ACCURATE) {
r -= (w * _wR) / 255; // subtract W LED's R contribution

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still uses division, why not right shift?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it divided by a constant, the compiler already converts that to a shift at compile time. But it can just as easily be explicitly written as a right shift

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);
}
Expand Down Expand Up @@ -1226,6 +1267,7 @@ int BusManager::add(const BusConfig &bc, bool placeholder) {
} else {
busses.push_back(make_unique<BusPwm>(bc));
}
if (!busses.empty()) busses.back()->setWhiteKelvin(bc.whiteKelvin);
return busses.size();
}

Expand Down
15 changes: 14 additions & 1 deletion wled00/bus_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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();
Expand Down
46 changes: 45 additions & 1 deletion wled00/data/settings_leds.htm
Original file line number Diff line number Diff line change
Expand Up @@ -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 (dig<n>wkv) 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<n> 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)<1000) wk.value = 6500;
UI();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// AI: end
// enable and update LED Amps
function enLA(s,n)
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -593,7 +630,7 @@
<div id="dig${s}r" style="display:inline"><br><span id="rev${s}">Reversed</span>: <input type="checkbox" name="CV${s}"></div>
<div id="dig${s}s" style="display:inline"><br>Skip first LEDs: <input type="number" name="SL${s}" min="0" max="255" value="0" oninput="UI()"></div>
<div id="dig${s}f" style="display:inline"><br><span id="off${s}">Off Refresh</span>: <input id="rf${s}" type="checkbox" name="RF${s}"></div>
<div id="dig${s}a" style="display:inline"><br>Auto-calculate W channel from RGB:<br><select name="AW${s}"><option value=0>None</option><option value=1>Brighter</option><option value=2>Accurate</option><option value=3>Dual</option><option value=4>Max</option></select>&nbsp;</div>
<div id="dig${s}a" style="display:inline"><br>Auto-calculate W channel from RGB:<br><select name="AW${s}" onchange="UI()"><option value=0>None</option><option value=1>Brighter</option><option value=2>Accurate</option><option value=3>Dual</option><option value=4>Max</option></select><div id="dig${s}wk" style="display:none"><br>Tune RGB to W channel color temperature: <input type="checkbox" name="WKE${s}" onchange="wkChk('${s}')"><div id="dig${s}wkv" style="display:none"><br>W channel color temperature: <input type="number" name="WK${s}" min="1000" max="10000" step="50" class="l" value="6500" disabled> K</div></div></div>
</div>`;
f.insertAdjacentHTML("beforeend", cn);
// fill led types (credit @netmindz)
Expand Down Expand Up @@ -784,6 +821,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;
Expand Down
6 changes: 5 additions & 1 deletion wled00/set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the first condition is redundant

uint16_t freq = request->arg(sp).toInt();
if (Bus::isPWM(type)) {
switch (freq) {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions wled00/xml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand Down
Loading