-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Add per-bus white-LED color temperature for accurate auto-white #5654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7e45b4
01fe430
113b3dc
cd00e56
cd6bf0e
38d19b0
5f61870
b42e4e1
5312bd0
15016d6
adefb08
7f7e78a
9965ca6
c26fc17
18bdf30
988d833
d9881b8
5cfdce9
8fcdd34
5d4b69f
2f56fde
33817dc
9009641
011b035
f010e00
7f23417
fbe93c1
fba6509
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,43 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { | |
| cw = (w * cw) / 255; | ||
| } | ||
|
|
||
| // 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. | ||
| 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 | ||
| } else { | ||
| byte rgb[4]; | ||
| colorKtoRGB(k, rgb); | ||
| _wR = rgb[0]; _wG = rgb[1]; _wB = rgb[2]; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| // 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 = WK_Q15_ONE / (_wR + 1U); | ||
| _rwG = WK_Q15_ONE / (_wG + 1U); | ||
| _rwB = WK_Q15_ONE / (_wB + 1U); | ||
| } | ||
| // 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 +149,42 @@ 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 || 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), | ||
| // 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 { | ||
| 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 (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 | ||
| // 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 >> 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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Please let me know if I am misunderstanding what you are referring to as MODE_MIN and MODE_MAX
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are correct, I was mistaken. I originally designed that under the belief that
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change applied in 011b035, but I am still undecided which approach is the right call. Let me know if you would prefer the opposite, i.e. applying the auto-white behavior to all of |
||
| if (aWM == RGBW_MODE_AUTO_ACCURATE) { | ||
| 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 | ||
| } | ||
| c = RGBW32(r, g, b, w); | ||
| } | ||
|
|
@@ -1312,6 +1382,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(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(); | ||||||||||||||||
|
|
@@ -204,6 +205,31 @@ | |||||||||||||||
| }); | ||||||||||||||||
| 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 checkbox is UI-only (no | ||||||||||||||||
| // name, so it is never submitted): the backend knows only WK<n>, where | ||||||||||||||||
| // 0 means off. 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 enabling from 0/blank/sub-min so the UI default matches | ||||||||||||||||
| // the sRGB white point. | ||||||||||||||||
| function wkChk(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<n> 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) | ||||||||||||||||
| { | ||||||||||||||||
|
|
@@ -368,6 +394,42 @@ | |||||||||||||||
| 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 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 | ||||||||||||||||
| // autoWhite path is used. | ||||||||||||||||
| { | ||||||||||||||||
| const awEl = d.Sf["AW"+n]; | ||||||||||||||||
| const awv = awEl ? parseInt(awEl.value) : 0; | ||||||||||||||||
| // 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<n>) 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 Accurate. | ||||||||||||||||
| const gAWel = d.Sf["AW"]; | ||||||||||||||||
| const gAW = gAWel ? parseInt(gAWel.value) : 255; | ||||||||||||||||
| 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 | ||||||||||||||||
| // 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"; | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use the effective auto-white mode for Kelvin control visibility.
Use the effective mode for the Accurate check. Proposed fix const gAWel = d.Sf["AW"];
const gAW = gAWel ? parseInt(gAWel.value) : 255;
const isCap = (m) => (m === 2);
+ const effectiveAW = gAW === 255 ? awv : gAW;
const wkBox = gId("dig"+n+"wk");
- if (wkBox) wkBox.style.display = (hasW(t) && hasRGB(t) && !hasCCT(t) && (isCap(awv) || isCap(gAW))) ? "inline" : "none";
+ if (wkBox) wkBox.style.display = (hasW(t) && hasRGB(t) && !hasCCT(t) && isCap(effectiveAW)) ? "inline" : "none";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| 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"; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| // 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 | ||||||||||||||||
|
|
@@ -588,7 +650,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> </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>Correct auto-white for W channel color temperature: <input type="checkbox" id="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="0" disabled> K</div></div></div> | ||||||||||||||||
| </div>`; | ||||||||||||||||
| f.insertAdjacentHTML("beforeend", cn); | ||||||||||||||||
| // fill led types (credit @netmindz) | ||||||||||||||||
|
|
@@ -779,6 +841,17 @@ | |||||||||||||||
| 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 | ||||||||||||||||
| // stored wk: 0 = feature off; normalise like the backend (1000..10000 else 0) | ||||||||||||||||
| { | ||||||||||||||||
| const wkChkEl = gId("wke"+i); | ||||||||||||||||
| const wkEl = d.getElementsByName("WK"+i)[0]; | ||||||||||||||||
| const raw = parseInt(v.wk, 10) | 0; | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Validate imported Kelvin values before signed 32-bit coercion.
- const raw = parseInt(v.wk, 10) | 0;
- const wkv = (raw >= 1000 && raw <= 10000) ? raw : 0;
+ const raw = Number(v.wk);
+ const wkv = Number.isInteger(raw) && raw >= 1000 && raw <= 10000 ? raw : 0;The firmware-side range check already matches this contract. 🤖 Prompt for AI Agents |
||||||||||||||||
| const wkv = (raw >= 1000 && raw <= 10000) ? raw : 0; | ||||||||||||||||
| if (wkChkEl) wkChkEl.checked = wkv > 0; | ||||||||||||||||
| if (wkEl) wkEl.value = wkv; | ||||||||||||||||
| } | ||||||||||||||||
| // 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; | ||||||||||||||||
|
|
@@ -1067,7 +1140,7 @@ <h3>Color & White</h3> | |||||||||||||||
| White Balance correction: <input type="checkbox" name="CCT"><br> | ||||||||||||||||
| <div id="wc"> | ||||||||||||||||
| Global override for Auto-calculate white: | ||||||||||||||||
| <select name="AW"> | ||||||||||||||||
| <select name="AW" onchange="UI()"> | ||||||||||||||||
| <option value=255>Disabled</option> | ||||||||||||||||
| <option value=0>None</option> | ||||||||||||||||
| <option value=1>Brighter</option> | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: wled/WLED
Length of output: 15553
🏁 Script executed:
Repository: wled/WLED
Length of output: 17409
🤖 get_repo_knowledge executed:
get_repo_knowledge wled/WLED /tmp/coderabbit-repo-knowledge/wled-wled-302fd5a5/learnings /tmp/coderabbit-repo-knowledge/wled-wled-302fd5a5/conventionsLength of output: 41100
Initialize all capability flags in the base
Busconstructor.BusPlaceholderleaves_hasRgb,_hasWhite, and_hasCCTuninitialized.BusHub75Matrixalso leaves_hasCCTuninitialized.BusManager::add()callssetWhiteKelvin()after constructing either bus, and line 111 reads these indeterminate flags. Initialize all three members tofalsein the base constructor; derived constructors can then override them.🤖 Prompt for AI Agents