mirror of
https://github.com/esphome/esphome.git
synced 2024-12-26 07:24:54 +01:00
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:
parent
8aedac81a5
commit
27204aa53c
8 changed files with 494 additions and 0 deletions
49
esphome/components/e131/__init__.py
Normal file
49
esphome/components/e131/__init__.py
Normal 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
|
105
esphome/components/e131/e131.cpp
Normal file
105
esphome/components/e131/e131.cpp
Normal 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
|
57
esphome/components/e131/e131.h
Normal file
57
esphome/components/e131/e131.h
Normal 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
|
90
esphome/components/e131/e131_addressable_light_effect.cpp
Normal file
90
esphome/components/e131/e131_addressable_light_effect.cpp
Normal 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 ¤t_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
|
48
esphome/components/e131/e131_addressable_light_effect.h
Normal file
48
esphome/components/e131/e131_addressable_light_effect.h
Normal 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 ¤t_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
|
136
esphome/components/e131/e131_packet.cpp
Normal file
136
esphome/components/e131/e131_packet.cpp
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue