FastLED 3.10.8
Loading...
Searching...
No Matches
power_mgt.cpp.hpp
Go to the documentation of this file.
1
3
5#include "led_sysdefs.h" // Must be included first (required by lib8tion.h)
6#include "pixeltypes.h" // CRGB
7#include "controller.h" // CLEDController
8#include "fastpin.h" // Pin
10#include "fl/stl/compiler_control.h" // FL_UNUSED
11#if SKETCH_HAS_LARGE_MEMORY
12#include "fl/math/math.h" // fl::pow, fl::lround — libm-gated wrappers
13#include "fl/stl/array.h"
14#endif
15#include "fl/stl/int.h" // fl::u32, fl::u8
16#include "fl/stl/type_traits.h" // fl::conditional
18#include "power_mgt.h" // Function declarations (to avoid redefinition errors)
19#include "fl/channels/pipeline_binding.h" // colorPipelineHooks (#4344)
22#include "pixel_controller.h"
23#include "fl/stl/singleton.h" // fl::Singleton
24#include "fl/gfx/rgbw.h" // fl::Rgbw, fl::rgb_2_rgbw
25#include "fl/stl/noexcept.h"
26// POWER MANAGEMENT
27
46
47static constexpr float kLinearPowerScalingExponent = 1.0f;
48static constexpr float kPowerScalingExponentEpsilon = 0.0001f;
49
52 // Constant-initialized (constexpr constructor, trivial destructor), so it
53 // needs neither Singleton's lazy placement-new nor a guard: there is no
54 // initialization order to protect and nothing to destroy.
55 static PowerModelRGB model;
56 return model;
57}
58
71
75
80
84
85// Source order, not wire order: RGB, then W or warm-W/cool-W. An undeclared
86// white gets the maximum representable emitter draw, so a managed channel
87// cannot silently under-budget it. Legacy estimates keep their old fallback.
88static void emitter_power_weights(fl::u8 count, fl::u8 (&weights)[5]) FL_NO_EXCEPT {
89 const PowerModelRGB& rgb = gPowerModel();
90 weights[0] = rgb.red_mW;
91 weights[1] = rgb.green_mW;
92 weights[2] = rgb.blue_mW;
93 weights[3] = 0;
94 weights[4] = 0;
95 if (count == 4) {
96 const fl::u8 white = gWhiteEmitterPower().mW;
97 weights[3] = white != 0 ? white : 255;
98 } else if (count == 5) {
100 if (rgbww.declared) {
101 weights[0] = rgbww.model.red_mW;
102 weights[1] = rgbww.model.green_mW;
103 weights[2] = rgbww.model.blue_mW;
104 weights[3] = rgbww.model.warm_white_mW;
105 weights[4] = rgbww.model.white_mW;
106 } else {
107 weights[3] = 255;
108 weights[4] = 255;
109 }
110 }
111}
112
113#if SKETCH_HAS_LARGE_MEMORY
114static constexpr fl::size kPowerScalingTableSize = 256;
115
119struct PowerScalingState {
120 fl::array<fl::u8, kPowerScalingTableSize> forward;
121 fl::array<fl::u8, kPowerScalingTableSize> reverse;
124 fl::u8 max_step = 1;
125
126 PowerScalingState() FL_NO_EXCEPT {
127 reset_identity();
128 }
129
130 void reset_identity() FL_NO_EXCEPT {
131 for (fl::size i = 0; i < kPowerScalingTableSize; ++i) {
132 forward[i] = static_cast<fl::u8>(i);
133 reverse[i] = static_cast<fl::u8>(i);
134 }
135 max_step = 1;
136 }
137};
138
139static PowerScalingState& gPowerScaling() FL_NO_EXCEPT {
141}
142
145static void rebuild_power_scaling_tables(float exponent) FL_NO_EXCEPT {
146 PowerScalingState& state = gPowerScaling();
147 if (!(exponent > 0.0f) ||
149 state.reset_identity();
150 return;
151 }
152
153 // Forward LUT: source brightness -> scaled brightness via pow(x/255, exponent)
154 state.forward[0] = 0;
155 for (fl::size i = 1; i < kPowerScalingTableSize; ++i) {
156 float normalized = static_cast<float>(i) / 255.0f;
157 // Route through fl::powf / fl::lroundf (libm-gated via FL_MATH_USE_LIBM,
158 // see fl/math/math.cpp.hpp). Use the float overloads — fl::pow takes
159 // double, so passing floats would auto-promote and re-anchor the
160 // double-precision soft-FP chain that #3002 is trying to avoid.
161 int mapped = static_cast<int>(
162 fl::lroundf(fl::powf(normalized, exponent) * 255.0f));
163 state.forward[i] = static_cast<fl::u8>(fl::clamp(mapped, 0, 255));
164 }
165 state.forward[255] = 255;
166 state.max_step = 1;
167 for (fl::size i = 1; i < kPowerScalingTableSize; ++i) {
168 const int step = state.forward[i] - state.forward[i - 1];
169 if (step > state.max_step) {
170 state.max_step = static_cast<fl::u8>(step);
171 }
172 }
173
174 // Reverse LUT: scaled brightness -> largest source whose forward value is
175 // <= the scaled value. Floor-inverse, so `unmap_power_value()` never
176 // rounds a budgeted scaled brightness *up* past the power budget.
177 state.reverse[0] = 0;
178 int source = 0;
179 for (int scaled = 1; scaled < static_cast<int>(kPowerScalingTableSize); ++scaled) {
180 while (source < 255 && state.forward[source + 1] <= scaled) {
181 ++source;
182 }
183 state.reverse[scaled] = static_cast<fl::u8>(source);
184 }
185}
186#endif
187
189#if SKETCH_HAS_LARGE_MEMORY
190 return gPowerScaling().forward[brightness];
191#else
192 return brightness;
193#endif
194}
195
196static fl::u8 unmap_power_value(fl::u8 scaled_brightness) FL_NO_EXCEPT {
197#if SKETCH_HAS_LARGE_MEMORY
198 return gPowerScaling().reverse[scaled_brightness];
199#else
200 return scaled_brightness;
201#endif
202}
203
206#if !FL_PLATFORM_HAS_TINY_MEMORY
208#if SKETCH_HAS_LARGE_MEMORY
209 return gPowerScaling().max_step;
210#else
211 return 1;
212#endif
213}
214#endif
215
217 return fl::scale32by8(total_mW, map_power_value(brightness));
218}
219
221
222// Alternate calibration by RAtkins via pre-PSU wattage measurments;
223// these are all probably about 20%-25% too high due to PSU heat losses,
224// but if you're measuring wattage on the PSU input side, this may
225// be a better set of calibrations. (WS2812B)
226// static const uint8_t gRed_mW = 100;
227// static const uint8_t gGreen_mW = 48;
228// static const uint8_t gBlue_mW = 100;
229// static const uint8_t gDark_mW = 12;
230
231
234#define POWER_LED 1
235
238#define POWER_DEBUG_PRINT 0
239
240
241// Power consumed by the MCU
242static const fl::u8 gMCU_mW = 25 * 5; // 25mA @ 5v = 125 mW
243
244static fl::u8 gMaxPowerIndicatorLEDPinNumber = 0; // default = Arduino onboard LED pin. set to zero to skip this.
245
246
247// Span-based version (primary implementation)
249 fl::u32 red32 = 0, green32 = 0, blue32 = 0;
250
251 // Iterate using span's safe indexing
252 for(size_t i = 0; i < leds.size(); i++) {
253 red32 += map_power_value(leds[i].r);
254 green32 += map_power_value(leds[i].g);
255 blue32 += map_power_value(leds[i].b);
256 }
257
258 red32 *= gPowerModel().red_mW;
259 green32 *= gPowerModel().green_mW;
260 blue32 *= gPowerModel().blue_mW;
261
262 red32 >>= 8;
263 green32 >>= 8;
264 blue32 >>= 8;
265
266 fl::u32 total = red32 + green32 + blue32 + (gPowerModel().dark_mW * leds.size());
267
268 return total;
269}
270
272 fl::u8 emitter_count) FL_NO_EXCEPT {
273 if (emitter_count < 3 || emitter_count > 5 ||
274 codes.size() % emitter_count != 0) {
275 return 0;
276 }
277 fl::u8 weights[5];
278 emitter_power_weights(emitter_count, weights);
279 fl::u32 sums[5] = {};
280 for (fl::size i = 0; i < codes.size(); i += emitter_count) {
281 for (fl::u8 channel = 0; channel < emitter_count; ++channel) {
282 sums[channel] += map_power_value(codes[i + channel]);
283 }
284 }
285 fl::u32 total = static_cast<fl::u32>(gPowerModel().dark_mW) *
286 (codes.size() / emitter_count);
287 for (fl::u8 channel = 0; channel < emitter_count; ++channel) {
288 total += (sums[channel] * weights[channel]) >> 8;
289 }
290 return total;
291}
292
293// Pointer-based version (delegates to span version)
294fl::u32 calculate_unscaled_power_mW( const CRGB* ledbuffer, fl::u16 numLeds ) FL_NO_EXCEPT //25354
295{
296 return calculate_unscaled_power_mW(fl::span<const CRGB>(ledbuffer, numLeds));
297}
298
299// Four emitters, using the conversion the encoder will actually run.
300//
301// The three-emitter overload reads the source triple the sketch wrote. For an
302// RGBW controller that triple is an input to `rgb_2_rgbw`, not a description
303// of the drives, and the two disagree by more than rounding: at full white on
304// the shipped default model the source triple reads 249 while the strip draws
305// 348 under `kRGBWMaxBrightness` and 99 under `kRGBWExactColors`. #4156 R3.
306//
307// Scales are passed as 255 because this is the demand at full brightness --
308// the caller scales the result. That is exact for `kRGBWExactColors` and
309// `kRGBWMaxBrightness`, whose conversions commute with the scale to within
310// 0.4% over the measured cases. It is not exact for `kRGBWBoostedWhite`,
311// which reallocates between emitters as the scale drops: demand there runs up
312// to 12.1% above the linear projection at low brightness. Charging four
313// emitters instead of three is the larger correction by far, and the residual
314// is stated rather than hidden -- closing it needs demand evaluated per
315// candidate brightness, which is a pixel walk per step of the limiter's
316// search.
318 const fl::u8 white_mW = gWhiteEmitterPower().mW;
319 // `!rgbw.active()` is a fast path, not a different answer: the only
320 // inactive mode is `kRGBWInvalid`, which `rgb_2_rgbw` dispatches to
321 // null-white -- RGB passed through with w = 0 -- so the loop below would
322 // return the same total. Removing the test changes nothing observable
323 // and costs one conversion per pixel on every plain RGB controller in
324 // the sketch, which the estimator walks once per frame.
325 //
326 // The `white_mW == 0` half *is* behavioural: with no declared white
327 // there is nothing to charge the fourth diode at.
328 if (!rgbw.active() || white_mW == 0) {
329 if (rgbw.active()) {
330 FL_WARN_ONCE("power: controller is in RGBW mode but the power model "
331 "declares no white emitter, so the budget covers three "
332 "of its four diodes. Call "
333 "FastLED.setPowerModel(PowerModelRGBW(...)) to fix it.");
334 }
336 }
337
338 fl::u32 red32 = 0, green32 = 0, blue32 = 0, white32 = 0;
339 for (fl::size i = 0; i < leds.size(); i++) {
340 fl::u8 r = 0, g = 0, b = 0, w = 0;
341 fl::rgb_2_rgbw(rgbw, leds[i].r, leds[i].g, leds[i].b, 255, 255, 255,
342 &r, &g, &b, &w);
343 red32 += map_power_value(r);
344 green32 += map_power_value(g);
345 blue32 += map_power_value(b);
346 white32 += map_power_value(w);
347 }
348
349 red32 *= gPowerModel().red_mW;
350 green32 *= gPowerModel().green_mW;
351 blue32 *= gPowerModel().blue_mW;
352 white32 *= white_mW;
353
354 red32 >>= 8;
355 green32 >>= 8;
356 blue32 >>= 8;
357 white32 >>= 8;
358
359 return red32 + green32 + blue32 + white32 +
360 (gPowerModel().dark_mW * leds.size());
361}
362
363
364// The part of the estimate a brightness scalar cannot touch: every controller
365// IC draws its quiescent current whether or not an emitter is lit, and the MCU
366// draws its own regardless of what the strip is doing. #4156 R4:
367//
368// Fixed idle consumption cannot be reduced by multiplying LED flux.
369//
370// Scaling that baseline along with the emitters is what let the limiter
371// under-report. At 300 WS2812s on the default model the baseline is 1500 mW,
372// and a 2000 mW budget used to be answered with a brightness that draws 3461.
373static fl::u32 fixed_power_mW(fl::u32 led_count) FL_NO_EXCEPT {
374 return static_cast<fl::u32>(gPowerModel().dark_mW) * led_count;
375}
376
377// min(255, floor(255 * num / den)) for den > 0, in 32-bit arithmetic only.
378//
379// The product needs 40 bits, and on AVR a 64-bit divide links ~600 B of
380// libgcc helpers into every sketch that limits power. A quotient of 255 or
381// more is clamped by the only caller anyway, so only num < den matters:
382// eight restoring-division steps give q = floor(256 * num / den) with
383// remainder r, and since 256 * num = q * den + r, the 255 * num answer is
384// q when r >= num and q - 1 otherwise (0 < num - r <= num < den). The
385// doubling is written as a comparison so it cannot overflow for any den.
386fl::u32 power_ratio_of_255(fl::u32 num, fl::u32 den) FL_NO_EXCEPT {
387 if (num >= den) {
388 return 255;
389 }
390 fl::u32 q = 0;
391 fl::u32 r = num;
392 for (fl::u8 bit = 0; bit < 8; ++bit) {
393 q <<= 1;
394 if (r >= den - r) {
395 r -= den - r;
396 q |= 1;
397 } else {
398 r += r;
399 }
400 }
401 return r >= num ? q : q - 1;
402}
403
404// Largest brightness whose *total* demand -- baseline included -- stays inside
405// the budget. Zero when the baseline alone is already over it: no brightness
406// meets the budget then, and answering with a lit strip would promise a bound
407// that cannot be held at any setting.
408static fl::u8 brightness_within_budget(fl::u32 fixed_mW, fl::u32 controllable_mW,
409 fl::u8 target_brightness,
410 fl::u32 max_power_mW) FL_NO_EXCEPT {
411 const fl::u32 requested_mW =
412 fixed_mW + scale_power_for_brightness(controllable_mW, target_brightness);
413 if (requested_mW <= max_power_mW) {
414 return target_brightness;
415 }
416 if (controllable_mW == 0 || max_power_mW <= fixed_mW) {
417 // Nothing left to scale, or the baseline alone is already over the
418 // budget. Either way no brightness meets it, and returning here is
419 // also what keeps the subtraction below from underflowing and the
420 // division from dividing by zero on an all-dark strip.
421 return 0;
422 }
423 // The ratio is taken over the whole 0-255 scaled range, because
424 // `controllable_mW` is the demand at *full* brightness. Scaling it by the
425 // requested brightness first would answer "what fraction of the request
426 // fits", which is smaller than "what brightness fits" by exactly that
427 // fraction -- and the step-down below only ever decreases, so it cannot
428 // recover it. The cap is where the request comes back in.
429 //
430 // The cap cannot actually fire from here: the caller only reaches this
431 // branch when the request is already over budget, which is the same
432 // inequality. It is kept because that argument runs through
433 // `map_power_value`'s rounding, and a helper that clamps to its own
434 // argument is cheaper than depending on that.
435 const fl::u32 headroom_mW = max_power_mW - fixed_mW;
436 const fl::u8 target_scaled = map_power_value(target_brightness);
437 fl::u32 allowed_scaled = power_ratio_of_255(headroom_mW, controllable_mW);
438 if (allowed_scaled > target_scaled) {
439 allowed_scaled = target_scaled;
440 }
441 const fl::u32 recommended_scaled = static_cast<fl::u32>(allowed_scaled);
442 fl::u8 recommended = unmap_power_value(static_cast<fl::u8>(recommended_scaled));
443
444 // Then check the answer instead of trusting the division. Inverting the
445 // ratio in scaled space is off by the rounding of whichever `scale32by8`
446 // the platform compiled -- the FASTLED_SCALE8_FIXED form computes
447 // `i * (s + 1) >> 8`, so the closed form lands about `controllable/256`
448 // over the budget, which was 176 mW of a 5000 mW budget on the default
449 // model. Demand is monotone in brightness, so stepping down until it fits
450 // is exact whatever the rounding, and terminates: 0 always fits here,
451 // because the branch above already returned for a budget under baseline.
452 while (recommended > 0 &&
453 fixed_mW + scale_power_for_brightness(controllable_mW, recommended) >
454 max_power_mW) {
455 --recommended;
456 }
457 return recommended;
458}
459
460fl::u8 calculate_max_brightness_for_power_vmA(const CRGB* ledbuffer, fl::u16 numLeds, fl::u8 target_brightness, fl::u32 max_power_V, fl::u32 max_power_mA) FL_NO_EXCEPT {
461 return calculate_max_brightness_for_power_mW(ledbuffer, numLeds, target_brightness, max_power_V * max_power_mA);
462}
463
464fl::u8 calculate_max_brightness_for_power_mW(const CRGB* ledbuffer, fl::u16 numLeds, fl::u8 target_brightness, fl::u32 max_power_mW) FL_NO_EXCEPT {
465 const fl::u32 total_mW = calculate_unscaled_power_mW( ledbuffer, numLeds);
466 const fl::u32 fixed_mW = fixed_power_mW(numLeds);
467 const fl::u32 controllable_mW = total_mW > fixed_mW ? total_mW - fixed_mW : 0;
468
469 return brightness_within_budget(fixed_mW, controllable_mW, target_brightness,
470 max_power_mW);
471}
472
473// The RGBW estimate, once something has stored an Rgbw into a controller.
474// Reached through a pointer so that the conversion (~1 KB on AVR) links only
475// into sketches that configure RGBW; until then every stored white config is
476// absent, and the RGB estimate is the answer the overload would give.
477typedef fl::u32 (*rgbw_power_fn)(fl::span<const CRGB>, const fl::Rgbw&);
478// FL_LINT_ALLOW_GLOBAL(one zero-initialized pointer in .bss; a Singleton would add its init code to every limiter sketch)
480
484
487 static_cast<fl::size>(controller.size()));
488#if FL_COLOR_PIPELINE_SHARED
489 // Through the pipeline hook, which exists whenever a pipeline does; naming
490 // the estimator directly would link the colour pipeline into sketches that
491 // limit power but never bind a profile. `pipeline` is this estimate's own
492 // reference, so reconfiguring the controller mid-pass cannot free the
493 // pipeline it is walking (#4440).
494 const fl::shared_ptr<fl::StreamingPipelineQ16> pipeline = fl::channelColorPipeline(controller);
496 if (pipeline && hooks.unscaledPowerMilliwatts != nullptr) {
497 // The callback charges already-solved physical emitter bytes. In
498 // particular, the RGBW legacy estimator must not convert them again.
499 const fl::u8 emitters = controller.getRgbww().active()
500 ? 5 : (controller.getRgbw().active() ? 4 : 3);
501 return hooks.unscaledPowerMilliwatts(
502 *pipeline, leds, emitters, &calculate_unscaled_emitter_power_mW);
503 }
504#endif
505 // Below the large-memory tier only this is compiled: a managed channel
506 // there is charged for its source (FL_COLOR_PIPELINE_SHARED).
507 // No stored Rgbw reads as RgbwInvalid, which the overload answers with the
508 // RGB estimate; asking for it directly skips copying the Rgbw value.
509 const fl::Rgbw* rgbw = controller.rgbwConfig();
512}
513
514// Two output codes: the most one `BINARY_DITHER` offset can lift a lit channel
515// once scaled. `d <= 256/s` scales back to at most one code, and the
516// FASTLED_SCALE8_FIXED form `(x * (s + 1)) >> 8` can carry one more; an
517// exhaustive check over every source, scale and offset finds 2 as the maximum.
518static constexpr fl::u32 kDitherReserveCodes = 2;
519
520#if FL_PLATFORM_HAS_TINY_MEMORY
521// TINY tier: no reserve, and none of the 64-bit arithmetic that computes it
522// (#4478). A target with 8 KiB of flash cannot spend 468 B on a bound that
523// only matters when a sketch also limits power.
526 return 0;
527}
528
531 return 0;
532}
533#else
534// Wide enough for the reserve's worst case without 64-bit libgcc helpers on
535// targets where it provably fits: with a 16-bit `fl::size` at most 65535
536// pixels light a channel, each charged at most 255 mW, and below the large
537// tier `max_power_step()` is 1, so the sum is at most
538// 65535 * 3 * 255 * kDitherReserveCodes + 255 < 2^32.
539#if SKETCH_HAS_LARGE_MEMORY
540typedef fl::u64 dither_acc_t;
541#else
542typedef fl::conditional<sizeof(fl::size) <= 2, fl::u32, fl::u64>::type dither_acc_t;
543FL_STATIC_ASSERT(sizeof(fl::size) > 2 ||
544 65535ull * 3u * 255u * kDitherReserveCodes + 255u <= 0xFFFFFFFFull,
545 "dither reserve must fit 32 bits on 16-bit-size targets");
546#endif
547
549 fl::u32 lit_r = 0, lit_g = 0, lit_b = 0;
550 for (fl::size i = 0; i < leds.size(); ++i) {
551 // A zero channel is never dithered: `dither()` returns 0 for it.
552 lit_r += leds[i].r != 0 ? 1u : 0u;
553 lit_g += leds[i].g != 0 ? 1u : 0u;
554 lit_b += leds[i].b != 0 ? 1u : 0u;
555 }
556 const PowerModelRGB& model = gPowerModel();
557 const dither_acc_t per_code =
558 static_cast<dither_acc_t>(lit_r) * model.red_mW +
559 static_cast<dither_acc_t>(lit_g) * model.green_mW +
560 static_cast<dither_acc_t>(lit_b) * model.blue_mW;
561 // Same >> 8 as the estimator, rounded up: this is a bound, and truncating
562 // it would hand back the under-charge it exists to remove.
563 const dither_acc_t scaled = per_code * kDitherReserveCodes * max_power_step();
564 const dither_acc_t reserve = (scaled + 255) >> 8;
565 // Saturate rather than wrap: an under-stated reserve is the failure this
566 // exists to prevent.
567 return reserve > 0xFFFFFFFFu ? 0xFFFFFFFFu : static_cast<fl::u32>(reserve);
568}
569
571 if (controller.getDither() != BINARY_DITHER) {
572 return 0;
573 }
574#if FL_COLOR_PIPELINE_SHARED
575 // A managed channel never takes the legacy offsets (C5); with
576 // BINARY_DITHER it runs the pipeline's temporal dither instead, which
577 // emits floor(x) or floor(x) + 1 of the exact code -- at most one code
578 // above the rounded code its estimate charges. The solve can light a
579 // channel the source left dark, so every channel of every lit pixel is
580 // reserved one code.
581 if (fl::channelColorPipeline(controller)) {
583 static_cast<fl::size>(controller.size()));
584 fl::u32 lit = 0;
585 for (fl::size i = 0; i < leds.size(); ++i) {
586 lit += (leds[i].r | leds[i].g | leds[i].b) != 0 ? 1u : 0u;
587 }
588 const fl::u8 emitters = controller.getRgbww().active()
589 ? 5 : (controller.getRgbw().active() ? 4 : 3);
590 fl::u8 weights[5];
591 emitter_power_weights(emitters, weights);
592 fl::u32 sum_weights = 0;
593 for (fl::u8 i = 0; i < emitters; ++i) {
594 sum_weights += weights[i];
595 }
596 const fl::u64 per_code = static_cast<fl::u64>(lit) * sum_weights;
597 const fl::u64 reserve = (per_code * max_power_step() + 255) >> 8;
598 return reserve > 0xFFFFFFFFu ? 0xFFFFFFFFu
599 : static_cast<fl::u32>(reserve);
600 }
601#endif
602#if !defined(FL_IS_AVR)
603 // Legacy RGBW and RGBWW conversions read the raw pixel, not the
604 // dithered one. Managed channels have already returned above.
605 if (controller.getRgbw().active() || controller.getRgbww().active()) {
606 return 0;
607 }
608#endif
610 controller.leds(), static_cast<fl::size>(controller.size())));
611}
612#endif // FL_PLATFORM_HAS_TINY_MEMORY
613
614#if FL_COLOR_PIPELINE_SHARED
615namespace {
616
617// Candidate-specific legacy demand. Unlike the old unity-demand projection,
618// this runs the same brightness premix and white extraction as PixelController.
619// Dither can raise a lit channel by at most two codes, so charge that upward
620// choice without touching the global/frame phase.
621fl::u32 legacyFramePowerMilliwatts(fl::CLEDController& controller,
623 const fl::u32 count = static_cast<fl::u32>(controller.size());
624 const fl::u8 emitters = controller.getRgbww().active() ? 5 :
625 (controller.getRgbw().active() ? 4 : 3);
626 fl::u8 weights[5];
627 emitter_power_weights(emitters, weights);
628 fl::u64 sums[5] = {};
630 controller.leds(), controller.size(),
631 controller.getAdjustmentData(brightness), DISABLE_DITHER);
632 for (fl::u32 i = 0; i < count; ++i) {
633 fl::u8 codes[5] = {};
634 if (emitters == 5) {
635 pixels.loadAndScaleRGBWWUnordered(controller.getRgbww(),
636 &codes[0], &codes[1], &codes[2], &codes[3], &codes[4]);
637 } else if (emitters == 4) {
638 pixels.loadAndScaleRGBWUnordered(controller.getRgbw(),
639 &codes[0], &codes[1], &codes[2], &codes[3]);
640 } else {
641 pixels.loadAndScaleRGB(&codes[0], &codes[1], &codes[2]);
642 }
643 const CRGB& source = controller.leds()[i];
644 const bool dithered = controller.getDither() == BINARY_DITHER &&
645 (source.r | source.g | source.b) != 0 && brightness != 0;
646 for (fl::u8 c = 0; c < emitters; ++c) {
647 const fl::u8 upper = dithered
648 ? static_cast<fl::u8>(codes[c] > 253 ? 255 : codes[c] + 2)
649 : codes[c];
650 sums[c] += map_power_value(upper);
651 }
652 pixels.advanceData();
653 }
654 fl::u64 total = static_cast<fl::u64>(gPowerModel().dark_mW) * count;
655 for (fl::u8 c = 0; c < emitters; ++c) {
656 total += (sums[c] * weights[c] + 255u) >> 8;
657 }
658 return total > 0xFFFFFFFFu ? 0xFFFFFFFFu : static_cast<fl::u32>(total);
659}
660
661struct ManagedFramePower {
662 fl::shared_ptr<fl::StreamingPipelineQ16> pipeline;
663 fl::ManagedPowerHistogram histogram;
664 fl::PowerCodecPolicy codec;
665 fl::u8 weights[5] = {};
666};
667
668struct FramePowerSnapshot {
669 fl::vector<ManagedFramePower> managed;
670 fl::vector<fl::CLEDController*> legacy;
671};
672
673bool captureFramePowerSnapshot(FramePowerSnapshot* snapshot) FL_NO_EXCEPT {
674 const fl::PowerFrameHooks& hooks = fl::powerFrameHooks();
675 fl::size managed_count = 0;
676 fl::size legacy_count = 0;
678 controller != nullptr; controller = controller->next()) {
679 if (!controller->getEnabled()) continue;
680 if (fl::channelColorPipeline(*controller) && hooks.buildPowerHistogram) {
681 ++managed_count;
682 } else {
683 ++legacy_count;
684 }
685 }
686 // Allocate exactly once, then construct histograms in their final slots.
687 // fl::vector silently leaves size unchanged when allocation fails.
688 snapshot->managed.resize(managed_count);
689 snapshot->legacy.resize(legacy_count);
690 if (snapshot->managed.size() != managed_count ||
691 snapshot->legacy.size() != legacy_count) return false;
692 fl::size managed_index = 0;
693 fl::size legacy_index = 0;
695 controller != nullptr; controller = controller->next()) {
696 if (!controller->getEnabled()) continue;
698 fl::channelColorPipeline(*controller);
699 if (pipeline && hooks.buildPowerHistogram != nullptr) {
700 if (managed_index >= managed_count) return false;
701 ManagedFramePower& entry = snapshot->managed[managed_index++];
702 entry.pipeline = pipeline;
704 const fl::u8 emitters = controller->getRgbww().active() ? 5 :
705 (controller->getRgbw().active() ? 4 : 3);
706 emitter_power_weights(emitters, entry.weights);
707 hooks.buildPowerHistogram(
708 *pipeline,
710 emitters, &entry.histogram);
711 } else {
712 if (legacy_index >= legacy_count) return false;
713 snapshot->legacy[legacy_index++] = controller;
714 }
715 }
716 // A binding/list mutation during capture would invalidate the model.
717 return managed_index == managed_count && legacy_index == legacy_count;
718}
719
720fl::u32 framePowerAtFluxMilliwatts(const FramePowerSnapshot& snapshot,
721 fl::u32 flux_q16,
722 fl::u8 legacy_brightness) FL_NO_EXCEPT {
723 fl::u64 total = gMCU_mW;
725 static_cast<fl::i32>(flux_q16));
726 const fl::PowerFrameHooks& hooks = fl::powerFrameHooks();
727 for (fl::size i = 0; i < snapshot.managed.size(); ++i) {
728 const ManagedFramePower& entry = snapshot.managed[i];
729 const fl::u64 numerator = hooks.histogramPowerNumerator(
730 *entry.pipeline, entry.histogram, flux, entry.codec,
731 entry.weights,
733 total += static_cast<fl::u64>(gPowerModel().dark_mW) *
734 entry.histogram.pixels + ((numerator + 255u) >> 8);
735 }
736 for (fl::size i = 0; i < snapshot.legacy.size(); ++i) {
737 total += legacyFramePowerMilliwatts(*snapshot.legacy[i],
738 legacy_brightness);
739 }
740 return total > 0xFFFFFFFFu ? 0xFFFFFFFFu : static_cast<fl::u32>(total);
741}
742
743} // namespace
744
745fl::FramePowerPlan fl::calculateFramePowerPlan(
746 fl::u8 requested_brightness, fl::u32 budget_mW) FL_NO_EXCEPT {
747 FramePowerPlan plan;
748 plan.mcu_mW = gMCU_mW;
749 FramePowerSnapshot snapshot;
750 if (!captureFramePowerSnapshot(&snapshot)) {
751 // Never solve against a partial set of enabled channels. Zero is the
752 // minimum controllable output; the unknown demand is not certified.
753 plan.flux_q16 = 0;
754 plan.legacy_brightness = 0;
755 plan.modeled_mW = 0xFFFFFFFFu;
756 plan.infeasible = true;
757 plan.limited = requested_brightness != 0;
758 return plan;
759 }
760 const fl::u32 request_flux = fl::FluxScalar::fromBrightness(
761 requested_brightness).rawQ16();
762 const fl::u32 zero_demand = framePowerAtFluxMilliwatts(snapshot, 0, 0);
763 if (zero_demand > budget_mW) {
764 plan.flux_q16 = 0;
765 plan.legacy_brightness = 0;
766 plan.modeled_mW = zero_demand;
767 plan.infeasible = true;
768 plan.limited = requested_brightness != 0;
769 return plan;
770 }
771 fl::u32 low = 0;
772 fl::u32 high = request_flux;
773 const auto legacy_at_flux = [request_flux, requested_brightness](
774 fl::u32 candidate) -> fl::u8 {
775 return candidate == request_flux ? requested_brightness
776 : static_cast<fl::u8>((static_cast<fl::u64>(candidate) * 255u) >> 16);
777 };
778 while (low < high) {
779 const fl::u32 candidate = low + (high - low + 1) / 2;
780 if (framePowerAtFluxMilliwatts(snapshot, candidate,
781 legacy_at_flux(candidate)) <= budget_mW) {
782 low = candidate;
783 } else {
784 high = candidate - 1;
785 }
786 }
787 plan.flux_q16 = low;
788 plan.limited = low < request_flux;
789 // Preserve the caller's exact byte when no limiting was needed. Q16's
790 // representation of b/255 is rounded, so projecting it back with a floor
791 // can otherwise turn even an unlimited b=1 into zero.
792 plan.legacy_brightness = legacy_at_flux(low);
793 plan.modeled_mW = framePowerAtFluxMilliwatts(
794 snapshot, low, plan.legacy_brightness);
795 return plan;
796}
797
798fl::u32 fl::framePowerMCUBaselineMilliwatts() FL_NO_EXCEPT {
799 return gMCU_mW;
800}
801
802namespace {
803bool hasManagedPowerChannel() FL_NO_EXCEPT {
804 for (CLEDController* p = CLEDController::head(); p; p = p->next()) {
805 if (p->getEnabled() && fl::channelColorPipeline(*p)) {
806 return true;
807 }
808 }
809 return false;
810}
811void beginManagedPowerFrame(fl::u8 requested_brightness, fl::u32 budget_mW,
812 fl::u8* legacy_brightness) FL_NO_EXCEPT {
813 const fl::FramePowerPlan plan =
814 fl::calculateFramePowerPlan(requested_brightness, budget_mW);
815 *legacy_brightness = plan.legacy_brightness;
816 fl::PowerFrameHooks& hooks = fl::powerFrameHooks();
817 hooks.frameFluxQ16 = plan.flux_q16;
818 hooks.frameFluxActive = true;
819}
820void endManagedPowerFrame() FL_NO_EXCEPT {
821 fl::PowerFrameHooks& hooks = fl::powerFrameHooks();
822 hooks.frameFluxActive = false;
823 hooks.frameFluxQ16 = 0;
824}
825
826// Registered only when setMaxPowerInMilliWatts() is used. This keeps the
827// ordinary show() body and its linked Blink image free of managed-power code.
828class FramePowerEndListener : public fl::EngineEvents::Listener {
829 public:
830 void onEndFrame() FL_NO_EXCEPT override { endManagedPowerFrame(); }
831 ~FramePowerEndListener() FL_NO_EXCEPT override {
833 }
834};
835
836FramePowerEndListener& framePowerEndListener() FL_NO_EXCEPT {
837 static FramePowerEndListener listener;
838 return listener;
839}
840
841fl::u8 managedShowBrightness(fl::u8 requested_brightness,
842 fl::u32 budget_mW) FL_NO_EXCEPT {
843 if (!hasManagedPowerChannel()) {
844 return calculate_max_brightness_for_power_mW(requested_brightness,
845 budget_mW);
846 }
847#if FASTLED_HAS_ENGINE_EVENTS
848 // addListener() may fail to grow its registry. Never leave a Q16 flux
849 // active across frames if the end-of-frame reset could not be installed.
850 if (!fl::EngineEvents::hasListener(&framePowerEndListener())) {
851 endManagedPowerFrame();
852 return 0;
853 }
854#else
855 // A managed Q16 frame requires a guaranteed reset after encode. Without
856 // frame events, fail closed rather than retaining flux into later frames.
857 return 0;
858#endif
859 fl::u8 legacy_brightness = requested_brightness;
860 beginManagedPowerFrame(requested_brightness, budget_mW,
861 &legacy_brightness);
862 return legacy_brightness;
863}
864} // namespace
865
866const fl::FramePowerDispatch* fl::framePowerDispatch() FL_NO_EXCEPT {
867 static const FramePowerDispatch dispatch = {
868 &calculateFramePowerPlan, &managedShowBrightness,
870#if FASTLED_HAS_ENGINE_EVENTS
871 fl::EngineEvents::addListener(&framePowerEndListener(), 1000000);
872#endif
873 return &dispatch;
874}
875#endif // FL_COLOR_PIPELINE_SHARED
876
877// sets brightness to
878// - no more than target_brightness
879// - no more than max_mW milliwatts
880fl::u8 calculate_max_brightness_for_power_mW( fl::u8 target_brightness, fl::u32 max_power_mW)
882 // gMCU_mW and every controller's dark current are baseline: present at any
883 // brightness, and so kept out of the part the scalar acts on (#4156 R4).
884 fl::u32 fixed_mW = gMCU_mW;
885 fl::u32 controllable_mW = 0;
886
888 while(pCur) {
889 const fl::u32 count = pCur->size();
890 // Through its RGBW conversion, and for a bound colour profile the
891 // solved drives rather than the source (#4344).
892 const fl::u32 unscaled_mW = controller_unscaled_power_mW(*pCur);
893 const fl::u32 dark_mW = fixed_power_mW(count);
894 // Dither's upward reserve does not scale with brightness, so it is
895 // charged with the baseline (#4342).
896 fixed_mW += dark_mW + controller_dither_reserve_mW(*pCur);
897 controllable_mW += unscaled_mW > dark_mW ? unscaled_mW - dark_mW : 0;
898 pCur = pCur->next();
899 }
900
901#if POWER_DEBUG_PRINT == 1
902 Serial.print("power demand at full brightness mW = ");
903 Serial.println( fixed_mW + controllable_mW);
904#endif
905
906 const fl::u32 requested_power_mW =
907 fixed_mW + scale_power_for_brightness(controllable_mW, target_brightness);
908#if POWER_DEBUG_PRINT == 1
909 if( target_brightness != 255 ) {
910 Serial.print("power demand at scaled brightness mW = ");
911 Serial.println( requested_power_mW);
912 }
913 Serial.print("power limit mW = ");
914 Serial.println( max_power_mW);
915#endif
916
917 if( requested_power_mW <= max_power_mW) {
918#if POWER_LED > 0
920 Pin(gMaxPowerIndicatorLEDPinNumber).lo(); // turn the LED off
921 }
922#endif
923#if POWER_DEBUG_PRINT == 1
924 Serial.print("demand is under the limit");
925#endif
926 return target_brightness;
927 }
928
929 const fl::u8 recommended_brightness = brightness_within_budget(
930 fixed_mW, controllable_mW, target_brightness, max_power_mW);
931#if POWER_DEBUG_PRINT == 1
932 Serial.print("recommended brightness # = ");
933 Serial.println( recommended_brightness);
934
935 fl::u32 resultant_power_mW =
936 fixed_mW + scale_power_for_brightness(controllable_mW, recommended_brightness);
937 Serial.print("resultant power demand mW = ");
938 Serial.println( resultant_power_mW);
939
940 Serial.println();
941#endif
942
943#if POWER_LED > 0
945 Pin(gMaxPowerIndicatorLEDPinNumber).hi(); // turn the LED on
946 }
947#endif
948
949 return recommended_brightness;
950}
951
956
957// The RGB half of installing a model. Split out because the white emitter's
958// lifetime is not the same as the RGB model's: declaring an RGB model retracts
959// a white declaration, but changing the exponent must not.
961 gPowerModel() = model;
962#if SKETCH_HAS_LARGE_MEMORY
963 rebuild_power_scaling_tables(model.exponent);
964#else
965 // Small-memory targets ignore non-linear exponent and keep linear behavior;
966 // clamp the stored field so get_power_scaling_exponent() reports the truth.
968#endif
969}
970
973 // An RGB model describes a strip with three emitters. Leaving a white
974 // declaration from an earlier RGBW model standing would charge this one
975 // for a diode the caller just said it does not have.
978}
979
981 apply_rgb_power_model(model.toRGB());
982 gWhiteEmitterPower().mW = model.white_mW;
984}
985
987 // Preserve the legacy folded-RGB estimate while retaining the physical
988 // weights for a managed five-emitter pipeline.
989 apply_rgb_power_model(model.toRGB());
991 gRgbwwEmitterPower().model = model;
993}
994
998
1000 PowerModelRGB model = gPowerModel();
1001 model.exponent = exponent;
1002 // Not `set_power_model`: the exponent is a property of the response
1003 // curve, and changing it is not a statement about how many emitters the
1004 // strip has.
1005 apply_rgb_power_model(model);
1006}
1007
1009 // Authoritative storage lives in the model; on small-memory builds the
1010 // field is clamped to 1.0 by set_power_model.
1011 return gPowerModel().exponent;
1012}
1013
1017
1018// Note: The following deprecated wrapper functions have been moved to FastLED.cpp:
1019// - set_max_power_in_volts_and_milliamps()
1020// - set_max_power_in_milliwatts()
1021// - show_at_max_brightness_for_power()
1022// - delay_at_max_brightness_for_power()
1023// These functions depend on the FastLED singleton and don't belong in this core power calculation file.
CRGB leds[1]
fl::UISlider brightness("Brightness", BRIGHTNESS, 0, 255)
TestState state
CLEDController * controller
Rgbw rgbw
static CLEDController * head() FL_NO_EXCEPT
Get the first LED controller in the linked list of controllers.
VIRTUAL_IF_NOT_AVR int size() const FL_NO_EXCEPT
How many LEDs does this controller manage?
CLEDController * next() FL_NO_EXCEPT
Get the next controller in the linked list after this one.
virtual void onEndFrame() FL_NO_EXCEPT
static void removeListener(Listener *listener) FL_NO_EXCEPT
static void addListener(Listener *listener, int priority=0) FL_NO_EXCEPT
static bool hasListener(Listener *listener) FL_NO_EXCEPT
static FluxScalar fromRawQ16(i32 raw) FL_NO_EXCEPT
A limiter fraction already in s16.16, clamped to [0, 1].
i32 rawQ16() const FL_NO_EXCEPT
static FluxScalar fromBrightness(u8 brightness) FL_NO_EXCEPT
setBrightness(b) as the linear flux scalar b/255.
A linear flux scalar in s16.16.
Definition flux_scalar.h:21
static T & instance() FL_NO_EXCEPT
Definition singleton.h:41
fl::CLEDController CLEDController
#define FL_UNUSED(x)
#define BINARY_DITHER
Enable dithering using binary dithering (only option)
Definition dither_mode.h:16
#define DISABLE_DITHER
Disable dithering.
Definition dither_mode.h:14
Backward compatibility header - use fl/fastpin.h directly.
Functions for red, green, blue, white (RGBW) output.
fl::CRGB CRGB
Definition crgb.h:25
void set_max_power_indicator_LED(fl::u8 pinNumber) FL_NO_EXCEPT
Select a pin with an LED that will be flashed to indicate that power management is pulling down the b...
fl::u32 scale_power_for_brightness(fl::u32 total_mW, fl::u8 brightness) FL_NO_EXCEPT
Applies the configured power-scaling response to a total power value.
fl::u8 calculate_max_brightness_for_power_vmA(const CRGB *ledbuffer, fl::u16 numLeds, fl::u8 target_brightness, fl::u32 max_power_V, fl::u32 max_power_mA) FL_NO_EXCEPT
Determines the highest brightness level you can use and still stay under the specified power budget f...
fl::u32 calculate_unscaled_power_mW(fl::span< const CRGB > leds) FL_NO_EXCEPT
Determines how many milliwatts the current LED data would draw at max brightness (255)
fl::u8 calculate_max_brightness_for_power_mW(const CRGB *ledbuffer, fl::u16 numLeds, fl::u8 target_brightness, fl::u32 max_power_mW) FL_NO_EXCEPT
Determines the highest brightness level you can use and still stay under the specified power budget f...
fl::u32 power_ratio_of_255(fl::u32 num, fl::u32 den) FL_NO_EXCEPT
min(255, floor(255 * num / den)) for den > 0, computed in 32-bit arithmetic.
fl::u32 dither_reserve_mW(fl::span< const CRGB > leds) FL_NO_EXCEPT
The most BINARY_DITHER can add to one latched frame of leds, in mW (#4342, contract R4: "quantization...
fl::u32 controller_dither_reserve_mW(const fl::CLEDController &controller) FL_NO_EXCEPT
dither_reserve_mW() for a controller that dithers, and zero for one that does not: a mode other than ...
fl::u32 controller_unscaled_power_mW(const fl::CLEDController &controller) FL_NO_EXCEPT
One controller's demand at full brightness, in mW, including idle draw – the quantity the limiter sum...
void set_power_scaling_exponent(float exponent) FL_NO_EXCEPT
Set a non-linear brightness-to-power response exponent.
PowerModelRGB get_power_model() FL_NO_EXCEPT
Get current RGB power model.
void set_power_model(const PowerModelRGB &model) FL_NO_EXCEPT
Set custom RGB LED power consumption model.
fl::u32 calculate_unscaled_emitter_power_mW(fl::span< const fl::u8 > codes, fl::u8 emitter_count) FL_NO_EXCEPT
Charge already-solved physical output bytes in source emitter order: RGB, RGBW, or RGB + warm white +...
float get_power_scaling_exponent() FL_NO_EXCEPT
Get the current brightness-to-power response exponent.
fl::u8 get_white_emitter_mW() FL_NO_EXCEPT
The white-emitter draw the caller declared, or zero if they declared only an RGB model.
Determines which platform system definitions to include.
#define FL_WARN_ONCE(...)
Definition log.h:439
constexpr T && forward(typename remove_reference< T >::type &t) FL_NO_EXCEPT
void reverse(Iterator first, Iterator last) FL_NO_EXCEPT
Definition algorithm.h:14
void enable_rgbw_power_estimate() FL_NO_EXCEPT
Lets the power limiter charge a controller's RGBW conversion.
unsigned char u8
Definition stdint.h:131
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr bool almost_equal(T a, T b, U tolerance) FL_NO_EXCEPT
Definition math.h:98
constexpr enable_if< is_fixed_point< T >::value, T >::type clamp(T x, T lo, T hi) FL_NO_EXCEPT
FASTLED_FORCE_INLINE void rgb_2_rgbw(const Rgbw &cfg, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Converts RGB to RGBW using one of the functions.
Definition rgbw.h:406
ColorPipelineHooks & colorPipelineHooks() FL_NO_EXCEPT
The installed hooks.
constexpr enable_if< is_fixed_point< T >::value, T >::type step(T edge, T x) FL_NO_EXCEPT
PowerCodecPolicy powerChannelCodecPolicy(const CLEDController &controller) FL_NO_EXCEPT
Identify the channel's final current/quantization choice without adding a virtual slot or storage to ...
float powf(float base, float exponent) FL_NO_EXCEPT
Definition math.h:444
long lroundf(float value) FL_NO_EXCEPT
Definition math.h:338
Result of the shared frame-budget prepass on large-memory managed output.
Low level pixel data writing class.
Includes defintions for RGB and HSV pixels.
static constexpr fl::u32 kDitherReserveCodes
fl::conditional< sizeof(fl::size)<=2, fl::u32, fl::u64 >::type dither_acc_t
static constexpr float kLinearPowerScalingExponent
static rgbw_power_fn gRgbwPowerEstimate
static PowerModelRGB & gPowerModel() FL_NO_EXCEPT
Global RGB power model (initialized to WS2812 @ 5V defaults, linear response)
static void apply_rgb_power_model(const PowerModelRGB &model) FL_NO_EXCEPT
static constexpr float kPowerScalingExponentEpsilon
static fl::u8 max_power_step() FL_NO_EXCEPT
What one output code can cost at the steepest point of the response, in the 0-255 scaled units calcul...
static void emitter_power_weights(fl::u8 count, fl::u8(&weights)[5]) FL_NO_EXCEPT
static fl::u8 map_power_value(fl::u8 brightness) FL_NO_EXCEPT
static fl::u8 gMaxPowerIndicatorLEDPinNumber
static RgbwwEmitterPower & gRgbwwEmitterPower() FL_NO_EXCEPT
fl::u32(* rgbw_power_fn)(fl::span< const CRGB >, const fl::Rgbw &)
static fl::u32 fixed_power_mW(fl::u32 led_count) FL_NO_EXCEPT
static fl::u8 brightness_within_budget(fl::u32 fixed_mW, fl::u32 controllable_mW, fl::u8 target_brightness, fl::u32 max_power_mW) FL_NO_EXCEPT
static const fl::u8 gMCU_mW
static WhiteEmitterPower & gWhiteEmitterPower() FL_NO_EXCEPT
static fl::u8 unmap_power_value(fl::u8 scaled_brightness) FL_NO_EXCEPT
PowerModelRGBWW model
The white emitter's draw, when the caller declared one.
Functions to limit the power used by FastLED.
#define FL_NO_EXCEPT
#define FL_STATIC_ASSERT(...)
Portable compile-time assertion wrapper.
Pixel controller class.
fl::u8 blue_mW
Blue channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:33
fl::u8 dark_mW
Dark LED baseline power consumption, in milliwatts.
Definition power_mgt.h:34
fl::u8 red_mW
Red channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:31
float exponent
Brightness-to-power response exponent (1.0 = linear)
Definition power_mgt.h:35
fl::u8 green_mW
Green channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:32
RGB LED power consumption model Used for standard 3-channel LEDs (WS2812, WS2812B,...
Definition power_mgt.h:30
RGBW LED power consumption model Used for 4-channel LEDs (SK6812 RGBW and similar).
Definition power_mgt.h:58
fl::u8 white_mW
Cool white channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:103
fl::u8 green_mW
Green channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:101
fl::u8 warm_white_mW
Warm white channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:104
fl::u8 red_mW
Red channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:100
fl::u8 blue_mW
Blue channel power at full brightness (255), in milliwatts.
Definition power_mgt.h:102
RGBWW LED power consumption model (RGB + warm white + cool white).
Definition power_mgt.h:99
#define Serial
Definition serial.h:309