ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
addressable_light.cpp
Go to the documentation of this file.
1#include "addressable_light.h"
2#include "esphome/core/log.h"
3
4#include <algorithm>
5#include <cstdlib>
6
7namespace esphome::light {
8
9ESPHOME_LOG_TAG(TAG, "light.addressable");
10
11#ifdef USE_LIGHT_POWER_ESTIMATION
13 return static_cast<const AddressableLight *>(this->output_)->get_estimated_current_ma();
14}
15
18 if (this->next_write_)
19 static_cast<AddressableLight *>(this->output_)->limit_power_();
20 this->write_if_pending_();
21}
22
24 return this->power_estimation_ == nullptr ? NAN : this->frame_ma_();
25}
26
27// Idle draw plus the weighted levels in the buffer
28float AddressableLight::frame_ma_(float *slack_ma) const {
29 uint32_t sums[4]{};
30 uint32_t lit[4]{};
31 const int32_t count = this->size();
32 for (int32_t i = 0; i < count; i++) {
33 const auto view = this->get_view_internal(i);
34 const uint8_t levels[4] = {view.get_red_raw(), view.get_green_raw(), view.get_blue_raw(), view.get_white_raw()};
35 for (size_t channel = 0; channel < 4; channel++) {
36 sums[channel] += levels[channel];
37 lit[channel] += levels[channel] != 0;
38 }
39 }
40 if (slack_ma != nullptr) {
41 const auto &power = *this->power_estimation_;
42 *slack_ma = 0.0f;
43 for (size_t channel = 0; channel < 4; channel++)
44 *slack_ma += lit[channel] * power.ma_per_level[channel];
45 }
46 return this->draw_ma_(sums);
47}
48
49float AddressableLight::draw_ma_(const uint32_t sums[4]) const {
50 const auto &power = *this->power_estimation_;
51 float ma = power.idle_ma;
52 for (size_t channel = 0; channel < 4; channel++)
53 ma += sums[channel] * power.ma_per_level[channel];
54 return ma;
55}
56
58 auto &power = *this->power_estimation_;
59 power.idle_ma = this->size() * power.idle_ma_per_led;
60 power.last_frame_ma = NAN;
61 if (std::isnan(power.max_current_ma))
62 return;
63 power.budget_ma = power.max_current_ma - power.idle_ma;
64 if (power.budget_ma <= 0.0f) {
65 ESP_LOGW(TAG, "max_current_ma %.0f leaves nothing for the pixels after the idle draw of %.0f mA",
66 power.max_current_ma, power.idle_ma);
67 }
68}
69
70// Rounded up on the way down so a lit channel stays lit, down on the way up so a level does not creep
71static uint8_t rescale_level(uint8_t level, uint8_t scale, uint8_t previous) {
72 if (level == 0)
73 return 0;
74 const uint32_t scaled = static_cast<uint32_t>(level) * scale;
75 const uint32_t moved = scale < previous ? (scaled + previous - 1) / previous : scaled / previous;
76 // A level written past the scale in force (set_raw, a partition over another strip) must not wrap
77 return std::clamp<uint32_t>(moved, 1, 255);
78}
79
80// The frame is moved to the new scale before it goes out, so every write path is limited
82 auto *power = this->power_estimation_;
83 if (power == nullptr || std::isnan(power->budget_ma))
84 return;
85 // A frame the last pass already left in the buffer, as a driver retry sends, is not limited again: its
86 // levels were rounded up on the way down and read as a hair over budget, which would walk the scale down
87 float slack_ma;
88 const float frame_ma = this->frame_ma_(&slack_ma);
89 if (frame_ma == power->last_frame_ma)
90 return;
91 power->last_frame_ma = frame_ma;
92 // The stored levels already carry the limit in force; the scale comes from the draw the frame asked for
93 const uint8_t previous = this->correction_.get_power_brightness();
94 const float intended_ma = (frame_ma - power->idle_ma) * 255.0f / previous;
95 // Never 0, which would black the strip out and swing the next estimate back to full
96 const uint8_t scale = intended_ma <= power->budget_ma
97 ? 255
98 : static_cast<uint8_t>(std::max(power->budget_ma / intended_ma * 255.0f, 1.0f));
99 if (scale == previous)
100 return;
101 // Below full scale the levels were rounded up on the way down, so a frame can read as over budget by up
102 // to one level per lit channel. A drop the frame does not need once that is taken off would only move the
103 // scale under levels that stay where they are, and inflate what they read back as
104 if (previous < 255 && scale < previous && frame_ma - power->idle_ma - slack_ma <= power->budget_ma)
105 return;
107 // One divide per level instead of one per channel; ESP8266 has no hardware divide
108 uint8_t map[256];
109 for (size_t level = 0; level < 256; level++)
110 map[level] = rescale_level(level, scale, previous);
111 uint32_t sums[4]{};
112 for (auto view : *this) {
113 const uint8_t r = map[view.get_red_raw()], g = map[view.get_green_raw()], b = map[view.get_blue_raw()],
114 w = map[view.get_white_raw()];
115 view.set_raw(r, g, b, w);
116 sums[0] += r;
117 sums[1] += g;
118 sums[2] += b;
119 sums[3] += w;
120 }
121 power->last_frame_ma = this->draw_ma_(sums);
122}
123
124#endif
125
127 this->setup();
128#ifdef USE_LIGHT_POWER_ESTIMATION
129 if (this->power_estimation_ != nullptr)
130 this->setup_power_budget_();
131#endif
132
133#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
134 this->set_interval(5000, [this]() {
135 const char *name = this->state_parent_ == nullptr ? "" : LOG_STR_ARG(this->state_parent_->get_log_name());
136 ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_));
137 for (int i = 0; i < this->size(); i++) {
138 auto color = this->get(i);
139 ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(),
140 color.get_blue_raw(), color.get_white_raw());
141 }
142 ESP_LOGVV(TAG, " ");
143 });
144#endif
145}
146
147std::unique_ptr<LightTransformer> AddressableLight::create_default_transition() {
148 return make_unique<AddressableLightTransformer>(*this);
149}
150
152 auto r = to_uint8_scale(val.get_color_brightness() * val.get_red());
153 auto g = to_uint8_scale(val.get_color_brightness() * val.get_green());
154 auto b = to_uint8_scale(val.get_color_brightness() * val.get_blue());
155 auto w = to_uint8_scale(val.get_white());
156 return Color(r, g, b, w);
157}
158
160 auto val = state->current_values;
161 auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
162 this->correction_.set_local_brightness(max_brightness);
163
164 if (this->is_effect_active())
165 return;
166
167 // don't use LightState helper, gamma correction+brightness is handled by ESPColorView
169 this->schedule_show();
170}
171
173 // don't try to transition over running effects.
174 if (this->light_.is_effect_active())
175 return;
176
177 auto end_values = this->target_values_;
179
180 // our transition will handle brightness, disable brightness in correction.
182 this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
183
184 // Uniformity scan is deferred to the first apply() call. start() can run before the underlying
185 // LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
186 // triggering a transition), and reading through ESPColorView would deref a null buffer.
187 this->uniform_start_scanned_ = false;
188 this->uniform_start_is_uniform_ = false;
189}
190
191inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
192 return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256));
193}
194
195optional<LightColorValues> AddressableLightTransformer::apply() {
197
198 // When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the
199 // LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the
200 // effects which respect it.
201 if (this->light_.is_effect_active())
202 return LightColorValues::lerp(this->get_start_values(), this->get_target_values(), smoothed_progress);
203
204 // Use a specialized transition for addressable lights: instead of using a unified transition for
205 // all LEDs, we use the current state of each LED as the start.
206
207 // We can't use a direct lerp smoothing here though - that would require creating a copy of the original
208 // state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating
209 // the delta between the current state and the target state, assuming that the delta represents the rest
210 // of the transition that was to be applied as of the previous transition step, and scaling the delta for
211 // what should be left after the current transition step. In this manner, the delta decays to zero as the
212 // transition progresses.
213 //
214 // Here's an example of how the algorithm progresses in discrete steps:
215 //
216 // At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%.
217 // At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%.
218 // At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%.
219 // At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%.
220 // At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%.
221 // At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%.
222 // At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%.
223 //
224 // Because the color values are quantized to 8 bit resolution after each step, the transition may appear
225 // non-linear when applying small deltas.
226
228 // Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
229 // frame buffer is valid. When every LED already has the same color (the common case: plain
230 // turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
231 // Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
232 // quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
233 // round to stored 0, freezing progress).
234 if (!this->uniform_start_scanned_) {
235 this->uniform_start_scanned_ = true;
236 if (this->light_.size() > 0) {
237 Color first = this->light_[0].get();
238 bool uniform = true;
239 for (int32_t i = 1; i < this->light_.size(); i++) {
240 if (this->light_[i].get() != first) {
241 uniform = false;
242 break;
243 }
244 }
245 if (uniform) {
246 this->uniform_start_color_ = first;
247 this->uniform_start_is_uniform_ = true;
248 }
249 }
250 }
251 if (this->uniform_start_is_uniform_) {
252 // All LEDs started at the same color: compute the interpolated value once and write it to
253 // every LED. No read-back, so each LED's stored byte advances through every gamma threshold
254 // as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
255 //
256 // Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
257 // overwritten on the next apply() here. The fallback path below would have respected them
258 // via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
259 // support, so this is acceptable.
260 // lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
261 const Color &start = this->uniform_start_color_;
262 int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
263 uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
264 uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
265 uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
266 uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
267 for (auto led : this->light_) {
268 led.set_rgbw(r, g, b, w);
269 }
270 } else {
271 int32_t scale =
272 int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
273 for (auto led : this->light_) {
274 led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
275 subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
276 subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
277 subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
278 }
279 }
281 this->light_.schedule_show();
282 }
283
284 return {};
285}
286
287} // namespace esphome::light
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
const LogString * get_log_name() const
Get the name for a "%s" log argument (LOG_STR_ARG).
float draw_ma_(const uint32_t sums[4]) const
virtual ESPColorView get_view_internal(int32_t index) const =0
ESPColorView get(int32_t index)
float frame_ma_(float *slack_ma=nullptr) const
virtual int32_t size() const =0
void update_state(LightState *state) override
std::unique_ptr< LightTransformer > create_default_transition() override
optional< LightColorValues > apply() override
void set_power_brightness(uint8_t power_brightness)
void set_local_brightness(uint8_t local_brightness)
This class represents the color state for a light object.
static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion)
Linearly interpolate between the values in start to the values in end.
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:68
bool next_write_
Whether the light value should be written in the next cycle.
LightOutput * output_
Store the output to allow effects to have more access.
const LightColorValues & get_target_values() const
float get_progress_()
The progress of this transition, on a scale of 0 to 1.
static float smoothed_progress(float x)
const LightColorValues & get_start_values() const
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
mopeka_std_values val[3]
Color color_from_light_color_values(LightColorValues val)
Convert the color information from a LightColorValues object to a Color object (does not apply bright...
ESPHOME_LOG_TAG(TAG, "light.addressable")
constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale)
static float float b
uint8_t red
Definition color.h:31
uint8_t white
Definition color.h:43
uint8_t green
Definition color.h:35
uint8_t blue
Definition color.h:39