diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py new file mode 100644 index 0000000000..0ba16bc928 --- /dev/null +++ b/esphome/components/e131/__init__.py @@ -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 diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp new file mode 100644 index 0000000000..d107d9f9fc --- /dev/null +++ b/esphome/components/e131/e131.cpp @@ -0,0 +1,105 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#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 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 diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h new file mode 100644 index 0000000000..3f647edbf1 --- /dev/null +++ b/esphome/components/e131/e131.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" + +#include +#include +#include + +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 &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_; + std::set light_effects_; + std::map universe_consumers_; + std::map universe_packets_; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp new file mode 100644 index 0000000000..8657d828c5 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -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 diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h new file mode 100644 index 0000000000..85af4fe7a9 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -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 diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp new file mode 100644 index 0000000000..ca68f5126d --- /dev/null +++ b/esphome/components/e131/e131_packet.cpp @@ -0,0 +1,136 @@ +#include "e131.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include + +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(&((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(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(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 &data, int &universe, E131Packet &packet) { + if (data.size() < E131_MIN_PACKET_SIZE) + return false; + + auto sbuff = reinterpret_cast(&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 diff --git a/tests/test1.yaml b/tests/test1.yaml index 5f8bdd1111..b6290c0440 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -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 diff --git a/tests/test3.yaml b/tests/test3.yaml index 06bfce97a2..fbfc486fe5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -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