mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[light] Keep strobe and color wipe colors in flash tables
This commit is contained in:
@@ -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 ¤t_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_{};
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user