From 0c0dec2534282aba8b63204ebdf294579e1620de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 13 Jun 2020 01:50:09 +0200 Subject: [PATCH] Add WLED support (#1092) A component to support [WLED](https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control). This allows to control addressable LEDs over WiFi/UDP, by pushing data right into LEDs. The most useful to use [Prismatik](https://github.com/psieg/Lightpack) to create an immersive effect on PC. It supports all WLED protocols: - WARLS - DRGB - DRGBW - DNRGB - WLED Notifier Co-authored-by: Guillermo Ruffino --- esphome/components/wled/__init__.py | 20 ++ esphome/components/wled/wled_light_effect.cpp | 237 ++++++++++++++++++ esphome/components/wled/wled_light_effect.h | 41 +++ tests/test1.yaml | 7 + tests/test3.yaml | 3 + 5 files changed, 308 insertions(+) create mode 100644 esphome/components/wled/__init__.py create mode 100644 esphome/components/wled/wled_light_effect.cpp create mode 100644 esphome/components/wled/wled_light_effect.h diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py new file mode 100644 index 0000000000..1a248e530f --- /dev/null +++ b/esphome/components/wled/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_PORT + +wled_ns = cg.esphome_ns.namespace('wled') +WLEDLightEffect = wled_ns.class_('WLEDLightEffect', AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('wled', WLEDLightEffect, "WLED", { + cv.Optional(CONF_PORT, default=21324): cv.port, +}) +def wled_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_port(config[CONF_PORT])) + + yield effect diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp new file mode 100644 index 0000000000..f161ea15e8 --- /dev/null +++ b/esphome/components/wled/wled_light_effect.cpp @@ -0,0 +1,237 @@ +#include "wled_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace wled { + +// Description of protocols: +// https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control +enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 }; + +const int DEFAULT_BLANK_TIME = 1000; + +static const char *TAG = "wled_light_effect"; + +WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void WLEDLightEffect::start() { + AddressableLightEffect::start(); + + blank_at_ = 0; +} + +void WLEDLightEffect::stop() { + AddressableLightEffect::stop(); + + if (udp_) { + udp_->stop(); + udp_.reset(); + } +} + +void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void WLEDLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // Init UDP lazily + if (!udp_) { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(port_)) { + ESP_LOGE(TAG, "Cannot bind WLEDLightEffect to %d.", port_); + } + } + + while (uint16_t packet_size = udp_->parsePacket()) { + std::vector payload; + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!this->parse_frame_(it, &payload[0], payload.size())) { + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%c/%d).", payload.size(), payload[0], payload[0]); + continue; + } + } + + if (blank_at_ < millis()) { + blank_all_leds_(it); + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } +} + +bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // At minimum frame needs to have: + // 1b - protocol + // 1b - timeout + if (size < 2) { + return false; + } + + uint8_t protocol = payload[0]; + uint8_t timeout = payload[1]; + + payload += 2; + size -= 2; + + switch (protocol) { + case WLED_NOTIFIER: + if (!parse_notifier_frame_(it, payload, size)) + return false; + break; + + case WARLS: + if (!parse_warls_frame_(it, payload, size)) + return false; + break; + + case DRGB: + if (!parse_drgb_frame_(it, payload, size)) + return false; + break; + + case DRGBW: + if (!parse_drgbw_frame_(it, payload, size)) + return false; + break; + + case DNRGB: + if (!parse_dnrgb_frame_(it, payload, size)) + return false; + break; + + default: + return false; + } + + if (timeout == UINT8_MAX) { + blank_at_ = UINT32_MAX; + } else if (timeout > 0) { + blank_at_ = millis() + timeout * 1000; + } else { + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } + + return true; +} + +bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // Packet needs to be empty + return size == 0; +} + +bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: index, r, g, b + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 4) { + uint8_t led = payload[0]; + uint8_t r = payload[1]; + uint8_t g = payload[2]; + uint8_t b = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 3) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b, w + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 4) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + uint8_t w = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b, w)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // offset: high, low + if (size < 2) { + return false; + } + + uint16_t led = (uint16_t(payload[0]) << 8) + payload[1]; + payload += 2; + size -= 2; + + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 3, led++) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +} // namespace wled +} // namespace esphome diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h new file mode 100644 index 0000000000..f1d27b06c7 --- /dev/null +++ b/esphome/components/wled/wled_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +#include +#include + +class UDP; + +namespace esphome { +namespace wled { + +class WLEDLightEffect : public light::AddressableLightEffect { + public: + WLEDLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + void set_port(uint16_t port) { this->port_ = port; } + + protected: + void blank_all_leds_(light::AddressableLight &it); + bool parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + + protected: + uint16_t port_{0}; + std::unique_ptr udp_; + uint32_t blank_at_{0}; + uint32_t dropped_{0}; +}; + +} // namespace wled +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index a793d74b5c..a6c0152928 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -185,6 +185,8 @@ as3935_spi: cs_pin: GPIO12 irq_pin: GPIO13 +wled: + adalight: sensor: @@ -1185,8 +1187,13 @@ light: if (initial_run) { it[0] = current_color; } + + - wled: + port: 11111 + - adalight: uart_id: adalight_uart + - automation: name: Custom Effect sequence: diff --git a/tests/test3.yaml b/tests/test3.yaml index fc61c1ae85..0344aed6de 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -207,6 +207,8 @@ deep_sleep: run_duration: 20s sleep_duration: 50s +wled: + adalight: sensor: @@ -714,6 +716,7 @@ light: method: ESP8266_UART0 num_leds: 100 effects: + - wled: - adalight: uart_id: adalight_uart - e131: