From d1b051a6bdc3c3de5b9e1ea14e9c3e1aa1c46e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 13 Jun 2020 01:34:38 +0200 Subject: [PATCH] Add Adalight support (#956) A component to support [Adalight](https://learn.adafruit.com/adalight-diy-ambient-tv-lighting). This allows to control addressable LEDs over UART, by pushing data right into LEDs. The most useful to use [Prismatik](https://github.com/psieg/Lightpack) to create an immersive effect on PC. Co-authored-by: Guillermo Ruffino --- esphome/components/adalight/__init__.py | 24 +++ .../adalight/adalight_light_effect.cpp | 140 ++++++++++++++++++ .../adalight/adalight_light_effect.h | 41 +++++ tests/test1.yaml | 26 +++- tests/test3.yaml | 14 +- 5 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 esphome/components/adalight/__init__.py create mode 100644 esphome/components/adalight/adalight_light_effect.cpp create mode 100644 esphome/components/adalight/adalight_light_effect.h diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py new file mode 100644 index 0000000000..66fae17f1e --- /dev/null +++ b/esphome/components/adalight/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_UART_ID + +DEPENDENCIES = ['uart'] + +adalight_ns = cg.esphome_ns.namespace('adalight') +AdalightLightEffect = adalight_ns.class_( + 'AdalightLightEffect', uart.UARTDevice, AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('adalight', AdalightLightEffect, "Adalight", { + cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent) +}) +def adalight_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + yield uart.register_uart_device(effect, config) + + yield effect diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp new file mode 100644 index 0000000000..1bf357e308 --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -0,0 +1,140 @@ +#include "adalight_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adalight { + +static const char *TAG = "adalight_light_effect"; + +static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; +static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; + +AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void AdalightLightEffect::start() { + AddressableLightEffect::start(); + + last_ack_ = 0; + last_byte_ = 0; + last_reset_ = 0; +} + +void AdalightLightEffect::stop() { + frame_.resize(0); + + AddressableLightEffect::stop(); +} + +int AdalightLightEffect::get_frame_size_(int led_count) const { + // 3 bytes: Ada + // 2 bytes: LED count + // 1 byte: checksum + // 3 bytes per LED + return 3 + 2 + 1 + led_count * 3; +} + +void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { + int buffer_capacity = get_frame_size_(it.size()); + + frame_.clear(); + frame_.reserve(buffer_capacity); +} + +void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void AdalightLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + const uint32_t now = millis(); + + if (now - this->last_ack_ >= ADALIGHT_ACK_INTERVAL) { + ESP_LOGV(TAG, "Sending ACK"); + this->write_str("Ada\n"); + this->last_ack_ = now; + } + + if (!this->last_reset_) { + ESP_LOGW(TAG, "Frame: Reset."); + reset_frame_(it); + blank_all_leds_(it); + this->last_reset_ = now; + } + + if (!this->frame_.empty() && now - this->last_byte_ >= ADALIGHT_RECEIVE_TIMEOUT) { + ESP_LOGW(TAG, "Frame: Receive timeout (size=%zu).", this->frame_.size()); + reset_frame_(it); + blank_all_leds_(it); + } + + if (this->available() > 0) { + ESP_LOGV(TAG, "Frame: Available (size=%d).", this->available()); + } + + while (this->available() != 0) { + uint8_t data; + if (!this->read_byte(&data)) + break; + this->frame_.push_back(data); + this->last_byte_ = now; + + switch (this->parse_frame_(it)) { + case INVALID: + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]); + reset_frame_(it); + break; + + case PARTIAL: + break; + + case CONSUMED: + ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size()); + reset_frame_(it); + break; + } + } +} + +AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableLight &it) { + if (frame_.empty()) + return INVALID; + + // Check header: `Ada` + if (frame_[0] != 'A') + return INVALID; + if (frame_.size() > 1 && frame_[1] != 'd') + return INVALID; + if (frame_.size() > 2 && frame_[2] != 'a') + return INVALID; + + // 3 bytes: Count Hi, Count Lo, Checksum + if (frame_.size() < 6) + return PARTIAL; + + // Check checksum + uint16_t checksum = frame_[3] ^ frame_[4] ^ 0x55; + if (checksum != frame_[5]) + return INVALID; + + // Check if we received the full frame + uint16_t led_count = (frame_[3] << 8) + frame_[4] + 1; + auto buffer_size = get_frame_size_(led_count); + if (frame_.size() < buffer_size) + return PARTIAL; + + // Apply lights + auto accepted_led_count = std::min(led_count, it.size()); + uint8_t *led_data = &frame_[6]; + + for (int led = 0; led < accepted_led_count; led++, led_data += 3) { + auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]); + + it[led].set(light::ESPColor(led_data[0], led_data[1], led_data[2], white)); + } + + return CONSUMED; +} + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h new file mode 100644 index 0000000000..4f77394ebc --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace adalight { + +class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { + public: + AdalightLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + protected: + enum Frame { + INVALID, + PARTIAL, + CONSUMED, + }; + + int get_frame_size_(int led_count) const; + void reset_frame_(light::AddressableLight &it); + void blank_all_leds_(light::AddressableLight &it); + Frame parse_frame_(light::AddressableLight &it); + + protected: + uint32_t last_ack_{0}; + uint32_t last_byte_{0}; + uint32_t last_reset_{0}; + std::vector frame_; +}; + +} // namespace adalight +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b6290c0440..a793d74b5c 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -128,14 +128,20 @@ spi: miso_pin: GPIO23 uart: - tx_pin: GPIO22 - rx_pin: GPIO23 - baud_rate: 115200 - id: uart0 - parity: NONE - data_bits: 8 - stop_bits: 1 - rx_buffer_size: 512 + - tx_pin: GPIO22 + rx_pin: GPIO23 + baud_rate: 115200 + id: uart0 + parity: NONE + data_bits: 8 + stop_bits: 1 + rx_buffer_size: 512 + + - id: adalight_uart + tx_pin: GPIO25 + rx_pin: GPIO26 + baud_rate: 115200 + rx_buffer_size: 1024 ota: safe_mode: True @@ -179,6 +185,8 @@ as3935_spi: cs_pin: GPIO12 irq_pin: GPIO13 +adalight: + sensor: - platform: adc pin: A0 @@ -1177,6 +1185,8 @@ light: if (initial_run) { it[0] = current_color; } + - adalight: + uart_id: adalight_uart - automation: name: Custom Effect sequence: diff --git a/tests/test3.yaml b/tests/test3.yaml index fbfc486fe5..fc61c1ae85 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -183,9 +183,13 @@ spi: miso_pin: GPIO14 uart: - tx_pin: GPIO1 - rx_pin: GPIO3 - baud_rate: 115200 + - tx_pin: GPIO1 + rx_pin: GPIO3 + baud_rate: 115200 + + - id: adalight_uart + rx_pin: GPIO3 + baud_rate: 115200 ota: safe_mode: True @@ -203,6 +207,8 @@ deep_sleep: run_duration: 20s sleep_duration: 50s +adalight: + sensor: - platform: apds9960 type: proximity @@ -708,6 +714,8 @@ light: method: ESP8266_UART0 num_leds: 100 effects: + - adalight: + uart_id: adalight_uart - e131: universe: 1