From f63f9168ff9fc881fa5bf1b8cd9f77ac334257f1 Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Thu, 18 Mar 2021 01:08:50 -0500 Subject: [PATCH] Add addressable_light display platform (#1272) --- CODEOWNERS | 1 + .../components/addressable_light/__init__.py | 0 .../addressable_light_display.cpp | 67 +++++++++++++++++++ .../addressable_light_display.h | 59 ++++++++++++++++ .../components/addressable_light/display.py | 63 +++++++++++++++++ esphome/const.py | 3 + tests/test4.yaml | 33 +++++++++ 7 files changed, 226 insertions(+) create mode 100644 esphome/components/addressable_light/__init__.py create mode 100644 esphome/components/addressable_light/addressable_light_display.cpp create mode 100644 esphome/components/addressable_light/addressable_light_display.h create mode 100644 esphome/components/addressable_light/display.py diff --git a/CODEOWNERS b/CODEOWNERS index 47cd5f59ca..d6f90fbfe2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,6 +13,7 @@ esphome/core/* @esphome/core # Integrations esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core +esphome/components/addressable_light/* @justfalter esphome/components/animation/* @syndlex esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter diff --git a/esphome/components/addressable_light/__init__.py b/esphome/components/addressable_light/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp new file mode 100644 index 0000000000..2e94e9e082 --- /dev/null +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -0,0 +1,67 @@ +#include "addressable_light_display.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace addressable_light { + +static const char* TAG = "addressable_light.display"; + +int AddressableLightDisplay::get_width_internal() { return this->width_; } +int AddressableLightDisplay::get_height_internal() { return this->height_; } + +void AddressableLightDisplay::setup() { + this->addressable_light_buffer_.resize(this->width_ * this->height_, {0, 0, 0, 0}); +} + +void AddressableLightDisplay::update() { + if (!this->enabled_) + return; + + this->do_update_(); + this->display(); +} + +void AddressableLightDisplay::display() { + bool dirty = false; + uint8_t old_r, old_g, old_b, old_w; + Color* c; + + for (uint32_t offset = 0; offset < this->addressable_light_buffer_.size(); offset++) { + c = &(this->addressable_light_buffer_[offset]); + + light::ESPColorView pixel = (*this->light_)[offset]; + + // Track the original values for the pixel view. If it has changed updating, then + // we trigger a redraw. Avoiding redraws == avoiding flicker! + old_r = pixel.get_red(); + old_g = pixel.get_green(); + old_b = pixel.get_blue(); + old_w = pixel.get_white(); + + pixel.set_rgbw(c->r, c->g, c->b, c->w); + + // If the actual value of the pixel changed, then schedule a redraw. + if (pixel.get_red() != old_r || pixel.get_green() != old_g || pixel.get_blue() != old_b || + pixel.get_white() != old_w) { + dirty = true; + } + } + + if (dirty) { + this->light_->schedule_show(); + } +} + +void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + if (this->pixel_mapper_f_.has_value()) { + // Params are passed by reference, so they may be modified in call. + this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; + } else { + this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; + } +} +} // namespace addressable_light +} // namespace esphome diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h new file mode 100644 index 0000000000..163faf27b0 --- /dev/null +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -0,0 +1,59 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/light/addressable_light.h" + +namespace esphome { +namespace addressable_light { + +class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { + public: + light::AddressableLight *get_light() const { return this->light_; } + + void set_width(int32_t width) { width_ = width; } + void set_height(int32_t height) { height_ = height; } + void set_light(light::LightState *state) { + light_state_ = state; + light_ = static_cast(state->get_output()); + } + void set_enabled(bool enabled) { + if (light_state_) { + if (enabled_ && !enabled) { // enabled -> disabled + // - Tell the parent light to refresh, effectively wiping the display. Also + // restores the previous effect (if any). + light_state_->make_call().set_effect(this->last_effect_).perform(); + + } else if (!enabled_ && enabled) { // disabled -> enabled + // - Save the current effect. + this->last_effect_ = light_state_->get_effect_name(); + // - Disable any current effect. + light_state_->make_call().set_effect(0).perform(); + } + } + enabled_ = enabled; + } + bool get_enabled() { return enabled_; } + + void set_pixel_mapper(std::function &&pixel_mapper_f) { this->pixel_mapper_f_ = pixel_mapper_f; } + void setup() override; + void display(); + + protected: + int get_width_internal() override; + int get_height_internal() override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void update() override; + + light::LightState *light_state_; + light::AddressableLight *light_; + bool enabled_{true}; + int32_t width_; + int32_t height_; + std::vector addressable_light_buffer_; + optional last_effect_; + optional> pixel_mapper_f_; +}; +} // namespace addressable_light +} // namespace esphome diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py new file mode 100644 index 0000000000..e5d3ca3034 --- /dev/null +++ b/esphome/components/addressable_light/display.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, light +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_PAGES, + CONF_ADDRESSABLE_LIGHT_ID, + CONF_HEIGHT, + CONF_WIDTH, + CONF_UPDATE_INTERVAL, + CONF_PIXEL_MAPPER, +) + +CODEOWNERS = ["@justfalter"] + +addressable_light_ns = cg.esphome_ns.namespace("addressable_light") +AddressableLightDisplay = addressable_light_ns.class_( + "AddressableLightDisplay", display.DisplayBuffer, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AddressableLightDisplay), + cv.Required(CONF_ADDRESSABLE_LIGHT_ID): cv.use_id( + light.AddressableLightState + ), + cv.Required(CONF_WIDTH): cv.positive_int, + cv.Required(CONF_HEIGHT): cv.positive_int, + cv.Optional( + CONF_UPDATE_INTERVAL, default="16ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_PIXEL_MAPPER): cv.returning_lambda, + } + ), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + wrapped_light = yield cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID]) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + cg.add(var.set_light(wrapped_light)) + + yield cg.register_component(var, config) + yield display.register_display(var, config) + + if CONF_PIXEL_MAPPER in config: + pixel_mapper_template_ = yield cg.process_lambda( + config[CONF_PIXEL_MAPPER], + [(int, "x"), (int, "y")], + return_type=cg.int_, + ) + cg.add(var.set_pixel_mapper(pixel_mapper_template_)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/const.py b/esphome/const.py index 232449e647..7842f8634c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -53,6 +53,7 @@ CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" CONF_ACTION_ID = "action_id" CONF_ADDRESS = "address" +CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ALPHA = "alpha" CONF_AND = "and" CONF_AP = "ap" @@ -223,6 +224,7 @@ CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" CONF_HEAT_MODE = "heat_mode" CONF_HEATER = "heater" +CONF_HEIGHT = "height" CONF_HIDDEN = "hidden" CONF_HIDE_TIMESTAMP = "hide_timestamp" CONF_HIGH = "high" @@ -388,6 +390,7 @@ CONF_PIN_B = "pin_b" CONF_PIN_C = "pin_c" CONF_PIN_D = "pin_d" CONF_PINS = "pins" +CONF_PIXEL_MAPPER = "pixel_mapper" CONF_PLATFORM = "platform" CONF_PLATFORMIO_OPTIONS = "platformio_options" CONF_PM_1_0 = "pm_1_0" diff --git a/tests/test4.yaml b/tests/test4.yaml index bfeff01e93..1f6eb5442b 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -99,3 +99,36 @@ switch: - platform: tuya id: tuya_switch switch_datapoint: 1 + +light: + - platform: fastled_clockless + id: led_matrix_32x8 + name: "led_matrix_32x8" + chipset: WS2812B + pin: GPIO15 + num_leds: 256 + rgb_order: GRB + default_transition_length: 0s + color_correct: [50%, 50%, 50%] + +display: + - platform: addressable_light + id: led_matrix_32x8_display + addressable_light_id: led_matrix_32x8 + width: 32 + height: 8 + pixel_mapper: |- + if (x % 2 == 0) { + return (x * 8) + y; + } + return (x * 8) + (7 - y); + lambda: |- + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, it.get_width(), it.get_height(), red); + it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); + it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); + it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + rotation: 0° + update_interval: 16ms