ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
addressable_light_effect.h
Go to the documentation of this file.
1#pragma once
2
3#include <type_traits>
4#include <utility>
5
7#include "esphome/core/hal.h"
11
12namespace esphome::light {
13
14inline static int16_t sin16_c(uint16_t theta) {
15 static const uint16_t BASE[] = {0, 6393, 12539, 18204, 23170, 27245, 30273, 32137};
16 static const uint8_t SLOPE[] = {49, 48, 44, 38, 31, 23, 14, 4};
17 uint16_t offset = (theta & 0x3FFF) >> 3; // 0..2047
18 if (theta & 0x4000)
19 offset = 2047 - offset;
20 uint8_t section = offset / 256; // 0..7
21 uint16_t b = BASE[section];
22 uint8_t m = SLOPE[section];
23 uint8_t secoffset8 = uint8_t(offset) / 2;
24 uint16_t mx = m * secoffset8;
25 int16_t y = mx + b;
26 if (theta & 0x8000)
27 return -y;
28 return y;
29}
30inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u) >> 8; }
31
33 public:
34 explicit AddressableLightEffect(const char *name) : LightEffect(name) {}
35 void start_internal() override {
38 this->start();
39 }
40 void stop() override { this->get_addressable_()->set_effect_active(false); }
41 virtual void apply(AddressableLight &it, const Color &current_color) = 0;
42 void apply() override {
43 // not using any color correction etc. that will be handled by the addressable layer through ESPColorCorrection
45 this->apply(*this->get_addressable_(), current_color);
46 }
47
50 uint32_t get_effect_index() const { return this->get_index(); }
51
53 bool is_current_effect() const { return this->is_active() && this->get_addressable_()->is_effect_active(); }
54
55 protected:
57};
58
60 public:
61 AddressableLambdaLightEffect(const char *name, void (*f)(AddressableLight &, Color, bool initial_run),
62 uint32_t update_interval)
63 : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {}
64 void start() override { this->initial_run_ = true; }
65 void apply(AddressableLight &it, const Color &current_color) override {
66 const uint32_t now = millis();
67 if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) {
68 this->last_run_ = now;
69 this->f_(it, current_color, this->initial_run_);
70 this->initial_run_ = false;
71 it.schedule_show();
72 }
73 }
74
75 protected:
76 void (*f_)(AddressableLight &, Color, bool initial_run);
80};
81
83 public:
84 explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {}
85 void apply(AddressableLight &it, const Color &current_color) override {
86 ESPHSVColor hsv;
87 hsv.value = 255;
88 hsv.saturation = 240;
89 uint16_t hue = (millis() * this->speed_) % 0xFFFF;
90 const uint16_t add = 0xFFFF / this->width_;
91 for (auto var : it) {
92 hsv.hue = hue >> 8;
93 var = hsv;
94 hue += add;
95 }
96 it.schedule_show();
97 }
98 void set_speed(uint32_t speed) { this->speed_ = speed; }
99 void set_width(uint16_t width) { this->width_ = width; }
100
101 protected:
103 uint16_t width_{50};
104};
105
113static_assert(std::is_trivially_copyable_v<AddressableColorWipeEffectColor>,
114 "AddressableColorWipeEffectColor is copied out of flash");
115static_assert(sizeof(AddressableColorWipeEffectColor) % sizeof(uint32_t) == 0,
116 "AddressableColorWipeEffectColor must be whole words for ESP8266 flash reads");
117static constexpr uint8_t NO_RANDOM_SLOT = 0xFF;
118
120 public:
121 explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {}
124 void set_colors(const AddressableColorWipeEffectColor *colors, size_t count, Color *random_colors) {
125 this->colors_ = {colors, count};
126 this->random_colors_ = random_colors;
127 }
128 void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; }
129 void set_reverse(bool reverse) { this->reverse_ = reverse; }
130 void apply(AddressableLight &it, const Color &current_color) override {
131 const uint32_t now = millis();
133 return;
134 this->last_add_ = now;
135 if (this->reverse_) {
136 it.shift_left(1);
137 } else {
138 it.shift_right(1);
139 }
140 const AddressableColorWipeEffectColor color = this->entry_(this->at_color_);
141 Color esp_color = this->color_of_(color);
142 if (color.gradient) {
143 size_t next_color_index = (this->at_color_ + 1) % this->colors_.size();
144 const Color next_esp_color = this->color_of_(this->entry_(next_color_index));
145 uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
146 esp_color = esp_color.gradient(next_esp_color, gradient);
147 }
148 if (this->reverse_) {
149 it[-1] = esp_color;
150 } else {
151 it[0] = esp_color;
152 }
153 if (++this->leds_added_ >= color.num_leds) {
154 this->leds_added_ = 0;
155 this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
156 const AddressableColorWipeEffectColor new_color = this->entry_(this->at_color_);
157 if (new_color.random_slot != NO_RANDOM_SLOT) {
159 Color &slot = this->random_colors_[new_color.random_slot];
160 slot.r = c.r;
161 slot.g = c.g;
162 slot.b = c.b;
163 }
164 }
165 it.schedule_show();
166 }
167
168 protected:
171 progmem_memcpy(&entry, &this->colors_[index], sizeof(entry));
172 return entry;
173 }
175 if (entry.random_slot != NO_RANDOM_SLOT)
176 return this->random_colors_[entry.random_slot];
177 return Color(entry.r, entry.g, entry.b, entry.w);
178 }
179
182 size_t at_color_{0};
185 size_t leds_added_{0};
186 bool reverse_{};
187};
188
190 public:
191 explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {}
192 void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
193 void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
194 void apply(AddressableLight &it, const Color &current_color) override {
195 const uint32_t now = millis();
196 if (now - this->last_move_ < this->move_interval_)
197 return;
198
199 const auto num_leds = static_cast<uint32_t>(it.size());
200 if (this->scan_width_ >= num_leds) {
201 it.all() = current_color;
202 it.schedule_show();
203 this->last_move_ = now;
204 return;
205 }
206
207 const uint32_t max_pos = num_leds - this->scan_width_;
208 if (this->at_led_ >= max_pos) {
209 this->at_led_ = max_pos;
210 this->direction_ = false;
211 }
212
213 if (this->direction_) {
214 this->at_led_++;
215 if (this->at_led_ >= max_pos)
216 this->direction_ = false;
217 } else {
218 if (this->at_led_ > 0)
219 this->at_led_--;
220 if (this->at_led_ == 0)
221 this->direction_ = true;
222 }
223 this->last_move_ = now;
224
225 it.all() = Color::BLACK;
226 for (uint32_t i = 0; i < this->scan_width_; i++) {
227 it[this->at_led_ + i] = current_color;
228 }
229
230 it.schedule_show();
231 }
232
233 protected:
238 bool direction_{true};
239};
240
242 public:
243 explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {}
244 void apply(AddressableLight &addressable, const Color &current_color) override {
245 const uint32_t now = millis();
246 uint8_t pos_add = 0;
247 if (now - this->last_progress_ > this->progress_interval_) {
248 const uint32_t pos_add32 = (now - this->last_progress_) / this->progress_interval_;
249 pos_add = pos_add32;
250 this->last_progress_ += pos_add32 * this->progress_interval_;
251 }
252 for (auto view : addressable) {
253 if (view.get_effect_data() != 0) {
254 const uint8_t sine = half_sin8(view.get_effect_data());
255 view = current_color * sine;
256 const uint8_t new_pos = view.get_effect_data() + pos_add;
257 if (new_pos < view.get_effect_data()) {
258 view.set_effect_data(0);
259 } else {
260 view.set_effect_data(new_pos);
261 }
262 } else {
263 view = Color::BLACK;
264 }
265 }
266 while (random_float() < this->twinkle_probability_) {
267 const size_t pos = random_uint32() % addressable.size();
268 if (addressable[pos].get_effect_data() != 0)
269 continue;
270 addressable[pos].set_effect_data(1);
271 }
272 addressable.schedule_show();
273 }
274 void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
275 void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
276
277 protected:
281};
282
284 public:
285 explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {}
286 void apply(AddressableLight &it, const Color &current_color) override {
287 const uint32_t now = millis();
288 uint8_t pos_add = 0;
289 if (now - this->last_progress_ > this->progress_interval_) {
290 pos_add = (now - this->last_progress_) / this->progress_interval_;
291 this->last_progress_ = now;
292 }
293 uint8_t subsine = ((8 * (now - this->last_progress_)) / this->progress_interval_) & 0b111;
294 for (auto view : it) {
295 if (view.get_effect_data() != 0) {
296 const uint8_t x = (view.get_effect_data() >> 3) & 0b11111;
297 const uint8_t color = view.get_effect_data() & 0b111;
298 const uint16_t sine = half_sin8((x << 3) | subsine);
299 if (color == 0) {
300 view = current_color * sine;
301 } else {
302 view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine);
303 }
304 const uint8_t new_x = x + pos_add;
305 if (new_x > 0b11111) {
306 view.set_effect_data(0);
307 } else {
308 view.set_effect_data((new_x << 3) | color);
309 }
310 } else {
311 view = Color(0, 0, 0, 0);
312 }
313 }
314 while (random_float() < this->twinkle_probability_) {
315 const size_t pos = random_uint32() % it.size();
316 if (it[pos].get_effect_data() != 0)
317 continue;
318 const uint8_t color = random_uint32() & 0b111;
319 it[pos].set_effect_data(0b1000 | color);
320 }
321 it.schedule_show();
322 }
323 void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
324 void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
325
326 protected:
330};
331
333 public:
334 explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {}
335 void start() override {
336 auto &it = *this->get_addressable_();
337 it.all() = Color::BLACK;
338 }
339 void apply(AddressableLight &it, const Color &current_color) override {
340 const uint32_t now = millis();
342 return;
343 this->last_update_ = now;
344 // "invert" the fade out parameter so that higher values make fade out faster
345 const uint8_t fade_out_mult = 255u - this->fade_out_rate_;
346 for (auto view : it) {
347 Color target = view.get() * fade_out_mult;
348 if (target.r < 64)
349 target *= 170;
350 view = target;
351 }
352 if (it.size() < 2)
353 return;
354 int last = it.size() - 1;
355 it[0].set(it[0].get() + (it[1].get() * 128));
356 for (int i = 1; i < last; i++) {
357 it[i] = (it[i - 1].get() * 64) + it[i].get() + (it[i + 1].get() * 64);
358 }
359 it[last] = it[last].get() + (it[last - 1].get() * 128);
360 if (random_float() < this->spark_probability_) {
361 const size_t pos = random_uint32() % it.size();
362 if (this->use_random_color_) {
363 it[pos] = Color::random_color();
364 } else {
365 it[pos] = current_color;
366 }
367 }
368 it.schedule_show();
369 }
370 void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
371 void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; }
372 void set_use_random_color(bool random_color) { this->use_random_color_ = random_color; }
373 void set_fade_out_rate(uint8_t fade_out_rate) { this->fade_out_rate_ = fade_out_rate; }
374
375 protected:
376 uint8_t fade_out_rate_{};
381};
382
384 public:
385 explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {}
386 void apply(AddressableLight &it, const Color &current_color) override {
387 const uint32_t now = millis();
388 const uint8_t intensity = this->intensity_;
389 const uint8_t inv_intensity = 255 - intensity;
391 return;
392
393 this->last_update_ = now;
394 uint32_t rng_state = random_uint32();
395 for (auto var : it) {
396 rng_state = (rng_state * 0x9E3779B9) + 0x9E37;
397 const uint8_t flicker = (rng_state & 0xFF) % intensity;
398 // scale down by random factor
399 var = var.get() * (255 - flicker);
400
401 // slowly fade back to "real" value
402 var = (var.get() * inv_intensity) + (current_color * intensity);
403 }
404 it.schedule_show();
405 }
406 void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
407 void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); }
408
409 protected:
412 uint8_t intensity_{13};
413};
414
415} // namespace esphome::light
uint8_t m
Definition bl0906.h:1
Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory).
Definition helpers.h:130
Color color_of_(const AddressableColorWipeEffectColor &entry) const
void apply(AddressableLight &it, const Color &current_color) override
ConstVector< AddressableColorWipeEffectColor > colors_
void set_colors(const AddressableColorWipeEffectColor *colors, size_t count, Color *random_colors)
Codegen only: point at the generated flash table of colors, which must outlive the effect.
AddressableColorWipeEffectColor entry_(size_t index) const
void set_add_led_interval(uint32_t add_led_interval)
void apply(AddressableLight &it, const Color &current_color) override
void apply(AddressableLight &it, const Color &current_color) override
void set_update_interval(uint32_t update_interval)
void(* f_)(AddressableLight &, Color, bool initial_run)
AddressableLambdaLightEffect(const char *name, void(*f)(AddressableLight &, Color, bool initial_run), uint32_t update_interval)
void apply(AddressableLight &it, const Color &current_color) override
uint32_t get_effect_index() const
Get effect index specifically for addressable effects.
virtual void apply(AddressableLight &it, const Color &current_color)=0
bool is_current_effect() const
Check if this is the currently running addressable effect.
virtual void clear_effect_data()=0
ESPColorView get(int32_t index)
void set_effect_active(bool effect_active)
virtual int32_t size() const =0
void apply(AddressableLight &it, const Color &current_color) override
void apply(AddressableLight &it, const Color &current_color) override
void apply(AddressableLight &it, const Color &current_color) override
void set_progress_interval(uint32_t progress_interval)
void set_twinkle_probability(float twinkle_probability)
void apply(AddressableLight &addressable, const Color &current_color) override
virtual void start()
Initialize this LightEffect. Will be called once after creation.
uint32_t get_index() const
Get the index of this effect in the parent light's effect list.
bool is_active() const
Check if this effect is currently active.
LightColorValues remote_values
The remote color values reported to the frontend.
LightOutput * get_output() const
Get the light output associated with this object.
unsigned count
Definition gif.cpp:51
Color color_from_light_color_values(LightColorValues val)
Convert the color information from a LightColorValues object to a Color object (does not apply bright...
FLAG_HAS_TRANSITION float
static float float b
float random_float()
Return a random float between 0 and 1.
Definition helpers.cpp:206
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
uint32_t random_uint32()
Return a random 32-bit unsigned integer.
Definition helpers.cpp:12
size_t size_t pos
Definition helpers.h:1255
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static Color random_color()
Definition color.h:166
Color gradient(const Color &to_color, uint8_t amnt) const
Definition color.cpp:9
uint8_t g
Definition color.h:34
uint8_t b
Definition color.h:38
uint8_t r
Definition color.h:30
static const Color BLACK
Definition color.h:190
One wipe color. Codegen keeps the colors in a flash table, so it is read with progmem_memcpy.
uint8_t random_slot
index into the effect's random colors, or NO_RANDOM_SLOT
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6