Add E1.31 support (#950)

This adds a `e131` component that allows
to register `e131` addressable light effect.

This uses an internal implementation that
is thread-safe instead of using external libraries.
This commit is contained in:
Kamil Trzciński 2020-06-13 01:17:13 +02:00 committed by GitHub
parent 8aedac81a5
commit 27204aa53c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 494 additions and 0 deletions

View file

@ -0,0 +1,49 @@
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_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS
e131_ns = cg.esphome_ns.namespace('e131')
E131AddressableLightEffect = e131_ns.class_('E131AddressableLightEffect', AddressableLightEffect)
E131Component = e131_ns.class_('E131Component', cg.Component)
METHODS = {
'UNICAST': e131_ns.E131_UNICAST,
'MULTICAST': e131_ns.E131_MULTICAST
}
CHANNELS = {
'MONO': e131_ns.E131_MONO,
'RGB': e131_ns.E131_RGB,
'RGBW': e131_ns.E131_RGBW
}
CONF_UNIVERSE = 'universe'
CONF_E131_ID = 'e131_id'
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(E131Component),
cv.Optional(CONF_METHOD, default='MULTICAST'): cv.one_of(*METHODS, upper=True),
})
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
cg.add(var.set_method(METHODS[config[CONF_METHOD]]))
@register_addressable_effect('e131', E131AddressableLightEffect, "E1.31", {
cv.GenerateID(CONF_E131_ID): cv.use_id(E131Component),
cv.Required(CONF_UNIVERSE): cv.int_range(min=1, max=512),
cv.Optional(CONF_CHANNELS, default='RGB'): cv.one_of(*CHANNELS, upper=True)
})
def e131_light_effect_to_code(config, effect_id):
parent = yield cg.get_variable(config[CONF_E131_ID])
effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
cg.add(effect.set_first_universe(config[CONF_UNIVERSE]))
cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]]))
cg.add(effect.set_e131(parent))
yield effect

View file

@ -0,0 +1,105 @@
#include "e131.h"
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"
#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#endif
namespace esphome {
namespace e131 {
static const char *TAG = "e131";
static const int PORT = 5568;
E131Component::E131Component() {}
E131Component::~E131Component() {
if (udp_) {
udp_->stop();
}
}
void E131Component::setup() {
udp_.reset(new WiFiUDP());
if (!udp_->begin(PORT)) {
ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT);
mark_failed();
}
join_igmp_groups_();
}
void E131Component::loop() {
std::vector<uint8_t> payload;
E131Packet packet;
int universe = 0;
while (uint16_t packet_size = udp_->parsePacket()) {
payload.resize(packet_size);
if (!udp_->read(&payload[0], payload.size())) {
continue;
}
if (!packet_(payload, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size());
continue;
}
if (!process_(universe, packet)) {
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
}
}
}
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
if (light_effects_.count(light_effect)) {
return;
}
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
light_effect->get_first_universe(), light_effect->get_last_universe());
light_effects_.insert(light_effect);
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
join_(universe);
}
}
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
if (!light_effects_.count(light_effect)) {
return;
}
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
light_effect->get_first_universe(), light_effect->get_last_universe());
light_effects_.erase(light_effect);
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
leave_(universe);
}
}
bool E131Component::process_(int universe, const E131Packet &packet) {
bool handled = false;
ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count);
for (auto light_effect : light_effects_) {
handled = light_effect->process_(universe, packet) || handled;
}
return handled;
}
} // namespace e131
} // namespace esphome

View file

@ -0,0 +1,57 @@
#pragma once
#include "esphome/core/component.h"
#include <memory>
#include <set>
#include <map>
class UDP;
namespace esphome {
namespace e131 {
class E131AddressableLightEffect;
enum E131ListenMethod { E131_MULTICAST, E131_UNICAST };
const int E131_MAX_PROPERTY_VALUES_COUNT = 513;
struct E131Packet {
uint16_t count;
uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT];
};
class E131Component : public esphome::Component {
public:
E131Component();
~E131Component();
void setup() override;
void loop() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
public:
void add_effect(E131AddressableLightEffect *light_effect);
void remove_effect(E131AddressableLightEffect *light_effect);
public:
void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; }
protected:
bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet);
bool process_(int universe, const E131Packet &packet);
bool join_igmp_groups_();
void join_(int universe);
void leave_(int universe);
protected:
E131ListenMethod listen_method_{E131_MULTICAST};
std::unique_ptr<UDP> udp_;
std::set<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_;
std::map<int, E131Packet> universe_packets_;
};
} // namespace e131
} // namespace esphome

View file

@ -0,0 +1,90 @@
#include "e131.h"
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"
namespace esphome {
namespace e131 {
static const char *TAG = "e131_addressable_light_effect";
static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1);
E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {}
int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; }
int E131AddressableLightEffect::get_lights_per_universe() const { return MAX_DATA_SIZE / channels_; }
int E131AddressableLightEffect::get_first_universe() const { return first_universe_; }
int E131AddressableLightEffect::get_last_universe() const { return first_universe_ + get_universe_count() - 1; }
int E131AddressableLightEffect::get_universe_count() const {
// Round up to lights_per_universe
auto lights = get_lights_per_universe();
return (get_addressable_()->size() + lights - 1) / lights;
}
void E131AddressableLightEffect::start() {
AddressableLightEffect::start();
if (this->e131_) {
this->e131_->add_effect(this);
}
}
void E131AddressableLightEffect::stop() {
if (this->e131_) {
this->e131_->remove_effect(this);
}
AddressableLightEffect::stop();
}
void E131AddressableLightEffect::apply(light::AddressableLight &it, const light::ESPColor &current_color) {
// ignore, it is run by `E131Component::update()`
}
bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet) {
auto it = get_addressable_();
// check if this is our universe and data are valid
if (universe < first_universe_ || universe > get_last_universe())
return false;
int output_offset = (universe - first_universe_) * get_lights_per_universe();
// limit amount of lights per universe and received
int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), packet.count - 1));
auto input_data = packet.values + 1;
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset,
output_end);
switch (channels_) {
case E131_MONO:
for (; output_offset < output_end; output_offset++, input_data++) {
auto output = (*it)[output_offset];
output.set(light::ESPColor(input_data[0], input_data[0], input_data[0], input_data[0]));
}
break;
case E131_RGB:
for (; output_offset < output_end; output_offset++, input_data += 3) {
auto output = (*it)[output_offset];
output.set(light::ESPColor(input_data[0], input_data[1], input_data[2],
(input_data[0] + input_data[1] + input_data[2]) / 3));
}
break;
case E131_RGBW:
for (; output_offset < output_end; output_offset++, input_data += 4) {
auto output = (*it)[output_offset];
output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], input_data[3]));
}
break;
}
return true;
}
} // namespace e131
} // namespace esphome

View file

@ -0,0 +1,48 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/light/addressable_light_effect.h"
namespace esphome {
namespace e131 {
class E131Component;
struct E131Packet;
enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 };
class E131AddressableLightEffect : public light::AddressableLightEffect {
public:
E131AddressableLightEffect(const std::string &name);
public:
void start() override;
void stop() override;
void apply(light::AddressableLight &it, const light::ESPColor &current_color) override;
public:
int get_data_per_universe() const;
int get_lights_per_universe() const;
int get_first_universe() const;
int get_last_universe() const;
int get_universe_count() const;
public:
void set_first_universe(int universe) { this->first_universe_ = universe; }
void set_channels(E131LightChannels channels) { this->channels_ = channels; }
void set_e131(E131Component *e131) { this->e131_ = e131; }
protected:
bool process_(int universe, const E131Packet &packet);
protected:
int first_universe_{0};
int last_universe_{0};
E131LightChannels channels_{E131_RGB};
E131Component *e131_{nullptr};
friend class E131Component;
};
} // namespace e131
} // namespace esphome

View file

@ -0,0 +1,136 @@
#include "e131.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include <lwip/ip_addr.h>
#include <lwip/igmp.h>
namespace esphome {
namespace e131 {
static const char *TAG = "e131";
static const uint8_t ACN_ID[12] = {0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00};
static const uint32_t VECTOR_ROOT = 4;
static const uint32_t VECTOR_FRAME = 2;
static const uint8_t VECTOR_DMP = 2;
// E1.31 Packet Structure
union E131RawPacket {
struct {
// Root Layer
uint16_t preamble_size;
uint16_t postamble_size;
uint8_t acn_id[12];
uint16_t root_flength;
uint32_t root_vector;
uint8_t cid[16];
// Frame Layer
uint16_t frame_flength;
uint32_t frame_vector;
uint8_t source_name[64];
uint8_t priority;
uint16_t reserved;
uint8_t sequence_number;
uint8_t options;
uint16_t universe;
// DMP Layer
uint16_t dmp_flength;
uint8_t dmp_vector;
uint8_t type;
uint16_t first_address;
uint16_t address_increment;
uint16_t property_value_count;
uint8_t property_values[E131_MAX_PROPERTY_VALUES_COUNT];
} __attribute__((packed));
uint8_t raw[638];
};
// We need to have at least one `1` value
// Get the offset of `property_values[1]`
const long E131_MIN_PACKET_SIZE = reinterpret_cast<long>(&((E131RawPacket *) nullptr)->property_values[1]);
bool E131Component::join_igmp_groups_() {
if (listen_method_ != E131_MULTICAST)
return false;
if (!udp_)
return false;
for (auto universe : universe_consumers_) {
if (!universe.second)
continue;
ip4_addr_t multicast_addr = {
static_cast<uint32_t>(IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))};
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
if (err) {
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
}
}
return true;
}
void E131Component::join_(int universe) {
// store only latest received packet for the given universe
auto consumers = ++universe_consumers_[universe];
if (consumers > 1) {
return; // we already joined before
}
if (join_igmp_groups_()) {
ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe);
}
}
void E131Component::leave_(int universe) {
auto consumers = --universe_consumers_[universe];
if (consumers > 0) {
return; // we have other consumers of the given universe
}
if (listen_method_ == E131_MULTICAST) {
ip4_addr_t multicast_addr = {
static_cast<uint32_t>(IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))};
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
}
ESP_LOGD(TAG, "Left %d universe for E1.31.", universe);
}
bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) {
if (data.size() < E131_MIN_PACKET_SIZE)
return false;
auto sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]);
if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0)
return false;
if (htonl(sbuff->root_vector) != VECTOR_ROOT)
return false;
if (htonl(sbuff->frame_vector) != VECTOR_FRAME)
return false;
if (sbuff->dmp_vector != VECTOR_DMP)
return false;
if (sbuff->property_values[0] != 0)
return false;
universe = htons(sbuff->universe);
packet.count = htons(sbuff->property_value_count);
if (packet.count > E131_MAX_PROPERTY_VALUES_COUNT)
return false;
memcpy(packet.values, sbuff->property_values, packet.count);
return true;
}
} // namespace e131
} // namespace esphome

View file

@ -1048,6 +1048,8 @@ output:
pin: GPIO25
id: dac_output
e131:
light:
- platform: binary
name: "Desk Lamp"
@ -1189,6 +1191,8 @@ light:
red: 0%
green: 100%
blue: 0%
- e131:
universe: 1
- platform: fastled_spi
id: addr2
chipset: WS2801

View file

@ -697,6 +697,8 @@ mcp23017:
mcp23008:
id: mcp23008_hub
e131:
light:
- platform: neopixelbus
name: Neopixelbus Light
@ -705,6 +707,9 @@ light:
variant: SK6812
method: ESP8266_UART0
num_leds: 100
effects:
- e131:
universe: 1
servo:
id: my_servo