diff --git a/CODEOWNERS b/CODEOWNERS index 2658aebdc7..8e0911f2a9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -272,6 +272,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/speaker/* @jesserockz esphome/components/spi/* @esphome/core esphome/components/spi_device/* @clydebarrow +esphome/components/spi_led_strip/* @clydebarrow esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 diff --git a/esphome/components/spi_led_strip/__init__.py b/esphome/components/spi_led_strip/__init__.py new file mode 100644 index 0000000000..850a1f6e02 --- /dev/null +++ b/esphome/components/spi_led_strip/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["spi"] diff --git a/esphome/components/spi_led_strip/light.py b/esphome/components/spi_led_strip/light.py new file mode 100644 index 0000000000..7420b0c929 --- /dev/null +++ b/esphome/components/spi_led_strip/light.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light +from esphome.components import spi +from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_DATA_RATE + +spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip") +SpiLedStrip = spi_led_strip_ns.class_( + "SpiLedStrip", light.AddressableLight, spi.SPIDevice +) + +CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpiLedStrip), + cv.Optional(CONF_NUM_LEDS, default=1): cv.positive_not_null_int, + cv.Optional(CONF_DATA_RATE, default="1MHz"): spi.SPI_DATA_RATE_SCHEMA, + } +).extend(spi.spi_device_schema(False)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + cg.add(var.set_data_rate(spi.SPI_DATA_RATE_OPTIONS[config[CONF_DATA_RATE]])) + cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + await light.register_light(var, config) + await spi.register_spi_device(var, config) + await cg.register_component(var, config) diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h new file mode 100644 index 0000000000..0d8c1c1e1c --- /dev/null +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -0,0 +1,91 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace spi_led_strip { + +static const char *const TAG = "spi_led_strip"; +class SpiLedStrip : public light::AddressableLight, + public spi::SPIDevice { + public: + void setup() { this->spi_setup(); } + + int32_t size() const override { return this->num_leds_; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + void set_num_leds(uint16_t num_leds) { + this->num_leds_ = num_leds; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_size_ = num_leds * 4 + 8; + this->buf_ = allocator.allocate(this->buffer_size_); + if (this->buf_ == nullptr) { + esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(num_leds); + if (this->effect_data_ == nullptr) { + esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds); + this->mark_failed(); + return; + } + memset(this->buf_, 0xFF, this->buffer_size_); + memset(this->buf_, 0, 4); + } + + void dump_config() { + esph_log_config(TAG, "SPI LED Strip:"); + esph_log_config(TAG, " LEDs: %d", this->num_leds_); + if (this->data_rate_ >= spi::DATA_RATE_1MHZ) + esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); + else + esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); + } + + void write_state(light::LightState *state) override { + if (this->is_failed()) + return; + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { + char strbuf[49]; + size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); + memset(strbuf, 0, sizeof(strbuf)); + for (size_t i = 0; i != len; i++) { + sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); + } + esph_log_v(TAG, "write_state: buf = %s", strbuf); + } + this->enable(); + this->write_array(this->buf_, this->buffer_size_); + this->disable(); + } + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + + protected: + light::ESPColorView get_view_internal(int32_t index) const override { + size_t pos = index * 4 + 5; + return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, + this->effect_data_ + index, &this->correction_}; + } + + size_t buffer_size_{}; + uint8_t *effect_data_{nullptr}; + uint8_t *buf_{nullptr}; + uint16_t num_leds_; +}; + +} // namespace spi_led_strip +} // namespace esphome diff --git a/tests/test8.yaml b/tests/test8.yaml index 498da94483..01d12ea330 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -29,6 +29,12 @@ light: name: neopixel-enable internal: false restore_mode: ALWAYS_OFF + - platform: spi_led_strip + num_leds: 4 + color_correct: [80%, 60%, 100%] + id: rgb_led + name: "RGB LED" + data_rate: 8MHz spi: id: spi_id_1