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 <glm.net@gmail.com>
This commit is contained in:
Kamil Trzciński 2020-06-13 01:34:38 +02:00 committed by GitHub
parent 27204aa53c
commit d1b051a6bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 234 additions and 11 deletions

View File

@ -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

View File

@ -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 &current_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<int>(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

View File

@ -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 <vector>
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 &current_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<uint8_t> frame_;
};
} // namespace adalight
} // namespace esphome

View File

@ -128,7 +128,7 @@ spi:
miso_pin: GPIO23 miso_pin: GPIO23
uart: uart:
tx_pin: GPIO22 - tx_pin: GPIO22
rx_pin: GPIO23 rx_pin: GPIO23
baud_rate: 115200 baud_rate: 115200
id: uart0 id: uart0
@ -137,6 +137,12 @@ uart:
stop_bits: 1 stop_bits: 1
rx_buffer_size: 512 rx_buffer_size: 512
- id: adalight_uart
tx_pin: GPIO25
rx_pin: GPIO26
baud_rate: 115200
rx_buffer_size: 1024
ota: ota:
safe_mode: True safe_mode: True
password: 'superlongpasswordthatnoonewillknow' password: 'superlongpasswordthatnoonewillknow'
@ -179,6 +185,8 @@ as3935_spi:
cs_pin: GPIO12 cs_pin: GPIO12
irq_pin: GPIO13 irq_pin: GPIO13
adalight:
sensor: sensor:
- platform: adc - platform: adc
pin: A0 pin: A0
@ -1177,6 +1185,8 @@ light:
if (initial_run) { if (initial_run) {
it[0] = current_color; it[0] = current_color;
} }
- adalight:
uart_id: adalight_uart
- automation: - automation:
name: Custom Effect name: Custom Effect
sequence: sequence:

View File

@ -183,7 +183,11 @@ spi:
miso_pin: GPIO14 miso_pin: GPIO14
uart: uart:
tx_pin: GPIO1 - tx_pin: GPIO1
rx_pin: GPIO3
baud_rate: 115200
- id: adalight_uart
rx_pin: GPIO3 rx_pin: GPIO3
baud_rate: 115200 baud_rate: 115200
@ -203,6 +207,8 @@ deep_sleep:
run_duration: 20s run_duration: 20s
sleep_duration: 50s sleep_duration: 50s
adalight:
sensor: sensor:
- platform: apds9960 - platform: apds9960
type: proximity type: proximity
@ -708,6 +714,8 @@ light:
method: ESP8266_UART0 method: ESP8266_UART0
num_leds: 100 num_leds: 100
effects: effects:
- adalight:
uart_id: adalight_uart
- e131: - e131:
universe: 1 universe: 1