[esp32_rmt_led_strip] Add RGBW channel ordering (#18028)

This commit is contained in:
Pete Keen
2026-08-05 07:57:59 -04:00
committed by GitHub
parent 450232964b
commit 3d093c0ae8
5 changed files with 116 additions and 11 deletions
@@ -255,11 +255,11 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
break;
}
uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3;
uint8_t white = this->is_wrgb_ ? 0 : 3;
uint8_t white = this->is_wrgb_ ? 0 : this->white_index_;
return {this->buf_ + (index * multiplier) + r + this->is_wrgb_,
this->buf_ + (index * multiplier) + g + this->is_wrgb_,
this->buf_ + (index * multiplier) + b + this->is_wrgb_,
return {this->buf_ + (index * multiplier) + r + (white <= r),
this->buf_ + (index * multiplier) + g + (white <= g),
this->buf_ + (index * multiplier) + b + (white <= b),
this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
&this->effect_data_[index],
&this->correction_};
@@ -295,11 +295,22 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
rgb_order = "UNKNOWN";
break;
}
if (this->is_rgbw_ || this->is_wrgb_) {
char rgbw_order[5];
uint8_t white = this->is_wrgb_ ? 0 : this->white_index_;
uint8_t rgb_index = 0;
for (uint8_t i = 0; i < 4; i++) {
rgbw_order[i] = i == white ? 'W' : rgb_order[rgb_index++];
}
rgbw_order[4] = '\0';
ESP_LOGCONFIG(TAG, " RGBW Order: %s", rgbw_order);
} else {
ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order);
}
ESP_LOGCONFIG(TAG,
" RGB Order: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_);
this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -52,6 +52,11 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
void set_rgbw_order(uint8_t white_index) {
this->is_rgbw_ = true;
this->is_wrgb_ = false;
this->white_index_ = white_index;
}
void set_use_dma(bool use_dma) { this->use_dma_ = use_dma; }
void set_use_psram(bool use_psram) { this->use_psram_ = use_psram; }
@@ -91,6 +96,8 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
uint16_t num_leds_;
bool is_rgbw_{false};
bool is_wrgb_{false};
// An index after the RGB channels makes offset adjustment a no-op for three-channel strips.
uint8_t white_index_{3};
bool use_dma_{false};
bool use_psram_{false};
bool invert_out_{false};
@@ -20,6 +20,7 @@ from esphome.const import (
CONF_RMT_SYMBOLS,
CONF_USE_DMA,
)
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -62,6 +63,7 @@ CHIPSETS = {
}
CONF_IS_WRGB = "is_wrgb"
CONF_RGBW_ORDER = "rgbw_order"
CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high"
@@ -70,6 +72,26 @@ CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low"
def _validate_rgbw_order(value: str) -> str:
value = cv.string(value).upper()
if len(value) != 4 or set(value) != set("RGBW"):
raise cv.Invalid("RGBW order must be a permutation of RGBW")
return value
def _split_rgbw_order(rgbw_order: str) -> tuple[str, int]:
return rgbw_order.replace("W", ""), rgbw_order.index("W")
def _validate_rgbw_order_exclusivity(config: ConfigType) -> ConfigType:
if CONF_RGBW_ORDER in config and (config[CONF_IS_RGBW] or config[CONF_IS_WRGB]):
raise cv.Invalid(
f"'{CONF_RGBW_ORDER}' cannot be used with '{CONF_IS_RGBW}' or "
f"'{CONF_IS_WRGB}'"
)
return config
CONFIG_SCHEMA = cv.All(
esp32.only_on_variant(
unsupported=list(esp32_rmt.VARIANTS_NO_RMT),
@@ -80,7 +102,8 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RGBW_ORDER): _validate_rgbw_order,
cv.SplitDefault(
CONF_RMT_SYMBOLS,
esp32=192,
@@ -130,6 +153,8 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
cv.has_exactly_one_key(CONF_RGB_ORDER, CONF_RGBW_ORDER),
_validate_rgbw_order_exclusivity,
)
@@ -173,9 +198,14 @@ async def to_code(config):
)
)
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
if (rgbw_order := config.get(CONF_RGBW_ORDER)) is not None:
rgb_order, white_index = _split_rgbw_order(rgbw_order)
cg.add(var.set_rgb_order(RGB_ORDERS[rgb_order]))
cg.add(var.set_rgbw_order(white_index))
else:
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
if CONF_USE_DMA in config:
@@ -9,7 +9,7 @@ light:
id: led_strip2
pin: ${pin2}
num_leds: 60
rgb_order: RGB
rgbw_order: RWGB
bit0_high: 100us
bit0_low: 100us
bit1_high: 100us
@@ -0,0 +1,57 @@
import pytest
from esphome.components.esp32_rmt_led_strip.light import (
CONF_IS_WRGB,
CONF_RGBW_ORDER,
_split_rgbw_order,
_validate_rgbw_order,
_validate_rgbw_order_exclusivity,
)
import esphome.config_validation as cv
from esphome.const import CONF_IS_RGBW
def test_validate_rgbw_order() -> None:
assert _validate_rgbw_order("rwgb") == "RWGB"
@pytest.mark.parametrize("rgbw_order", ["RGB", "RRGB", "RGBWW"])
def test_validate_rgbw_order_rejects_invalid_order(rgbw_order: str) -> None:
with pytest.raises(cv.Invalid, match="permutation of RGBW"):
_validate_rgbw_order(rgbw_order)
@pytest.mark.parametrize(
("rgbw_order", "expected"),
[
("WRGB", ("RGB", 0)),
("RWGB", ("RGB", 1)),
("GWRB", ("GRB", 1)),
("RGBW", ("RGB", 3)),
],
)
def test_split_rgbw_order(rgbw_order: str, expected: tuple[str, int]) -> None:
assert _split_rgbw_order(rgbw_order) == expected
@pytest.mark.parametrize("conflict", [CONF_IS_RGBW, CONF_IS_WRGB])
def test_rgbw_order_is_mutually_exclusive(conflict: str) -> None:
with pytest.raises(cv.Invalid, match="cannot be used with"):
_validate_rgbw_order_exclusivity(
{
CONF_RGBW_ORDER: "RGBW",
CONF_IS_RGBW: conflict == CONF_IS_RGBW,
CONF_IS_WRGB: conflict == CONF_IS_WRGB,
}
)
@pytest.mark.parametrize("legacy_option", [CONF_IS_RGBW, CONF_IS_WRGB])
def test_rgbw_order_allows_disabled_legacy_options(legacy_option: str) -> None:
config = {
CONF_RGBW_ORDER: "RGBW",
CONF_IS_RGBW: False,
CONF_IS_WRGB: False,
}
config[legacy_option] = False
assert _validate_rgbw_order_exclusivity(config) is config