[light] Keep strobe and color wipe colors in flash tables

This commit is contained in:
J. Nick Koston
2026-10-06 09:04:57 -04:00
committed by GitHub
parent a69c648c6f
commit 4a528b39af
5 changed files with 198 additions and 47 deletions
@@ -3,6 +3,7 @@
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/light/light_state.h"
#include "esphome/components/light/addressable_light.h"
@@ -101,17 +102,24 @@ class AddressableRainbowLightEffect : public AddressableLightEffect {
uint16_t width_{50};
};
/// One wipe color. Codegen keeps the colors in a flash table, so it is read with progmem_memcpy.
struct AddressableColorWipeEffectColor {
uint8_t r, g, b, w;
bool random;
size_t num_leds;
uint8_t random_slot; ///< index into the effect's random colors, or NO_RANDOM_SLOT
bool gradient;
uint32_t num_leds;
};
static constexpr uint8_t NO_RANDOM_SLOT = 0xFF;
class AddressableColorWipeEffect : public AddressableLightEffect {
public:
explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {}
void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; }
/// Codegen only: point at the generated flash table of colors, which must outlive the effect.
/// Random colors keep their current value in a RAM array, one slot per random entry.
void set_colors(const AddressableColorWipeEffectColor *colors, size_t count, Color *random_colors) {
this->colors_ = {colors, count};
this->random_colors_ = random_colors;
}
void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; }
void set_reverse(bool reverse) { this->reverse_ = reverse; }
void apply(AddressableLight &it, const Color &current_color) override {
@@ -124,12 +132,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
} else {
it.shift_right(1);
}
const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
Color esp_color = Color(color.r, color.g, color.b, color.w);
const AddressableColorWipeEffectColor color = this->entry_(this->at_color_);
Color esp_color = this->color_of_(color);
if (color.gradient) {
size_t next_color_index = (this->at_color_ + 1) % this->colors_.size();
const AddressableColorWipeEffectColor &next_color = this->colors_[next_color_index];
const Color next_esp_color = Color(next_color.r, next_color.g, next_color.b, next_color.w);
const Color next_esp_color = this->color_of_(this->entry_(next_color_index));
uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
esp_color = esp_color.gradient(next_esp_color, gradient);
}
@@ -141,19 +148,32 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
if (++this->leds_added_ >= color.num_leds) {
this->leds_added_ = 0;
this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
AddressableColorWipeEffectColor &new_color = this->colors_[this->at_color_];
if (new_color.random) {
const AddressableColorWipeEffectColor new_color = this->entry_(this->at_color_);
if (new_color.random_slot != NO_RANDOM_SLOT) {
Color c = Color::random_color();
new_color.r = c.r;
new_color.g = c.g;
new_color.b = c.b;
Color &slot = this->random_colors_[new_color.random_slot];
slot.r = c.r;
slot.g = c.g;
slot.b = c.b;
}
}
it.schedule_show();
}
protected:
FixedVector<AddressableColorWipeEffectColor> colors_;
AddressableColorWipeEffectColor entry_(size_t index) const {
AddressableColorWipeEffectColor entry;
progmem_memcpy(&entry, &this->colors_[index], sizeof(entry));
return entry;
}
Color color_of_(const AddressableColorWipeEffectColor &entry) const {
if (entry.random_slot != NO_RANDOM_SLOT)
return this->random_colors_[entry.random_slot];
return Color(entry.r, entry.g, entry.b, entry.w);
}
ConstVector<AddressableColorWipeEffectColor> colors_;
Color *random_colors_{nullptr};
size_t at_color_{0};
uint32_t last_add_{0};
uint32_t add_led_interval_{};
+30 -8
View File
@@ -3,6 +3,7 @@
#include <utility>
#include "esphome/core/automation.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "light_effect.h"
@@ -154,10 +155,21 @@ class AutomationLightEffect : public LightEffect {
Trigger<> trig_;
};
/// One strobe step. Codegen keeps the steps in a flash table, so it is read with progmem_memcpy.
struct StrobeLightEffectColor {
LightColorValues color;
float state;
float brightness;
float color_brightness;
float red;
float green;
float blue;
float white;
float color_temperature;
float cold_white;
float warm_white;
uint32_t duration;
uint32_t transition_length;
ColorMode color_mode;
};
class StrobeLightEffect : public LightEffect {
@@ -166,19 +178,22 @@ class StrobeLightEffect : public LightEffect {
void start() override {
// Place the cycle at the end of the last color, so the first apply() switches straight to the first color
this->at_color_ = this->colors_.size() - 1;
this->last_switch_ = millis() - this->colors_.back().duration;
this->last_switch_ = millis() - this->duration_(this->at_color_);
}
void apply() override {
const uint32_t now = millis();
if (now - this->last_switch_ < this->colors_[this->at_color_].duration)
if (now - this->last_switch_ < this->duration_(this->at_color_))
return;
// Switch to next color
this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
auto color = this->colors_[this->at_color_].color;
StrobeLightEffectColor step;
progmem_memcpy(&step, &this->colors_[this->at_color_], sizeof(step));
LightColorValues color(step.color_mode, step.state, step.brightness, step.color_brightness, step.red, step.green,
step.blue, step.white, step.color_temperature, step.cold_white, step.warm_white);
auto call = this->state_->turn_on();
call.from_light_color_values(this->colors_[this->at_color_].color);
call.from_light_color_values(color);
if (!color.is_on()) {
// Don't turn the light off, otherwise the light effect will be stopped
@@ -187,15 +202,22 @@ class StrobeLightEffect : public LightEffect {
}
call.set_publish(false);
call.set_save(false);
call.set_transition_length_if_supported(this->colors_[this->at_color_].transition_length);
call.set_transition_length_if_supported(step.transition_length);
call.perform();
this->last_switch_ = now;
}
void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; }
/// Codegen only: point at the generated flash table of steps, which must outlive the effect.
void set_colors(const StrobeLightEffectColor *colors, size_t count) { this->colors_ = {colors, count}; }
protected:
FixedVector<StrobeLightEffectColor> colors_;
uint32_t duration_(size_t index) const {
uint32_t duration;
progmem_memcpy(&duration, &this->colors_[index].duration, sizeof(duration));
return duration;
}
ConstVector<StrobeLightEffectColor> colors_;
uint32_t last_switch_{0};
size_t at_color_{0};
};
+60 -26
View File
@@ -52,7 +52,6 @@ from .types import (
ColorMode,
FlickerLightEffect,
LambdaLightEffect,
LightColorValues,
LightStateRef,
PulseLightEffect,
RandomLightEffect,
@@ -351,28 +350,26 @@ async def strobe_effect_to_code(config, effect_id):
colors = [
cg.StructInitializer(
StrobeLightEffectColor,
(
"color",
LightColorValues(
color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN),
color[CONF_STATE],
color[CONF_BRIGHTNESS],
color[CONF_COLOR_BRIGHTNESS],
color[CONF_RED],
color[CONF_GREEN],
color[CONF_BLUE],
color[CONF_WHITE],
color.get(CONF_COLOR_TEMPERATURE, 0.0),
color[CONF_COLD_WHITE],
color[CONF_WARM_WHITE],
),
),
("state", color[CONF_STATE]),
("brightness", color[CONF_BRIGHTNESS]),
("color_brightness", color[CONF_COLOR_BRIGHTNESS]),
("red", color[CONF_RED]),
("green", color[CONF_GREEN]),
("blue", color[CONF_BLUE]),
("white", color[CONF_WHITE]),
("color_temperature", color.get(CONF_COLOR_TEMPERATURE, 0.0)),
("cold_white", color[CONF_COLD_WHITE]),
("warm_white", color[CONF_WARM_WHITE]),
("duration", color[CONF_DURATION]),
("transition_length", color[CONF_TRANSITION_LENGTH]),
("color_mode", color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN)),
)
for color in config.get(CONF_COLORS, [])
]
cg.add(var.set_colors(colors))
table = cg.shared_progmem_array(
"light_strobe_colors", StrobeLightEffectColor, cg.ArrayInitializer(*colors)
)
cg.add(var.set_colors(table, len(colors)))
return var
@@ -431,6 +428,16 @@ async def addressable_rainbow_effect_to_code(config, effect_id):
return var
# Matches NO_RANDOM_SLOT in addressable_light_effect.h
NO_RANDOM_SLOT = 0xFF
def _validate_random_slots(colors: list[ConfigType]) -> list[ConfigType]:
if sum(1 for color in colors if color[CONF_RANDOM]) >= NO_RANDOM_SLOT:
raise cv.Invalid(f"At most {NO_RANDOM_SLOT - 1} colors can be random")
return colors
ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COLOR_BRIGHTNESS): cv.percentage,
@@ -459,7 +466,11 @@ ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA = _with_color_option(
{
cv.Optional(
CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]
): cv.ensure_list(ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA),
): cv.All(
cv.ensure_list(ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA),
cv.Length(min=1),
_validate_random_slots,
),
cv.Optional(
CONF_ADD_LED_INTERVAL, default="0.1s"
): cv.positive_time_period_milliseconds,
@@ -471,21 +482,44 @@ async def addressable_color_wipe_effect_to_code(config, effect_id):
cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL]))
cg.add(var.set_reverse(config[CONF_REVERSE]))
colors = []
random_colors = []
for color in config.get(CONF_COLORS, []):
color_brightness = color[CONF_COLOR_BRIGHTNESS]
rgbw = (
int(round(color[CONF_RED] * color_brightness * 255)),
int(round(color[CONF_GREEN] * color_brightness * 255)),
int(round(color[CONF_BLUE] * color_brightness * 255)),
int(round(color[CONF_WHITE] * 255)),
)
random_slot = NO_RANDOM_SLOT
if color[CONF_RANDOM]:
random_slot = len(random_colors)
random_colors.append(rgbw)
colors.append(
cg.StructInitializer(
AddressableColorWipeEffectColor,
("r", int(round(color[CONF_RED] * color_brightness * 255))),
("g", int(round(color[CONF_GREEN] * color_brightness * 255))),
("b", int(round(color[CONF_BLUE] * color_brightness * 255))),
("w", int(round(color[CONF_WHITE] * 255))),
("random", color[CONF_RANDOM]),
("num_leds", color[CONF_NUM_LEDS]),
("r", rgbw[0]),
("g", rgbw[1]),
("b", rgbw[2]),
("w", rgbw[3]),
("random_slot", random_slot),
("gradient", color[CONF_GRADIENT]),
("num_leds", color[CONF_NUM_LEDS]),
)
)
cg.add(var.set_colors(colors))
table = cg.shared_progmem_array(
"light_color_wipe_colors",
AddressableColorWipeEffectColor,
cg.ArrayInitializer(*colors),
)
random_storage = cg.nullptr
if random_colors:
# One RAM slot per random entry; it starts at the configured color, as before
name = f"light__{effect_id.id}__random_colors"
values = ", ".join(f"Color({r}, {g}, {b}, {w})" for r, g, b, w in random_colors)
cg.add_global(cg.RawStatement(f"static Color {name}[] = {{{values}}};"))
random_storage = cg.RawExpression(name)
cg.add(var.set_colors(table, len(colors), random_storage))
return var
@@ -0,0 +1,44 @@
esphome:
name: test-effect-color-tables
esp32:
board: esp32dev
framework:
type: arduino
light:
- platform: fastled_clockless
id: strip_a
chipset: WS2811
pin: GPIO13
num_leds: 10
effects:
- strobe:
name: Strobe A
colors:
- state: true
duration: 500ms
- state: false
duration: 250ms
- addressable_color_wipe:
name: Wipe
colors:
- red: 100%
green: 0%
blue: 0%
num_leds: 2
- random: true
num_leds: 1
- platform: fastled_clockless
id: strip_b
chipset: WS2811
pin: GPIO14
num_leds: 10
effects:
- strobe:
name: Strobe B
colors:
- state: true
duration: 500ms
- state: false
duration: 250ms
@@ -0,0 +1,31 @@
"""Strobe and addressable_color_wipe colors live in flash tables; random wipe colors get RAM slots."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import re
def test_effect_colors_use_flash_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("effect_color_tables.yaml"))
assert (
"static constexpr light::StrobeLightEffectColor light_strobe_colors[] PROGMEM"
in main_cpp
)
# Both strobes use the same steps, so they share one table
assert main_cpp.count("->set_colors(light_strobe_colors, 2);") == 2
assert re.search(
r"static constexpr light::AddressableColorWipeEffectColor light_color_wipe_colors\w*\[\] PROGMEM",
main_cpp,
)
# Only the random entry gets a RAM slot, starting at its configured color
assert re.search(
r"static Color light__\w+__random_colors\[\] = \{Color\(", main_cpp
)
assert ".random_slot = 0," in main_cpp
assert ".random_slot = 255," in main_cpp