Direct WLED Control Custom Component

Hello everyone,

I am trying to use a custom component to control a WLED node directly from an ESPHome node. The component uses UDP for the controls. Here’s the location to the GitHub I’m using Muxa’s.

I’ve worked through a few different errors such as the IPv4 data type (added comments to the python init and cpp docs) but I am getting stuck with the following. I found a few different posts about the functions being defined multiple times is a problem but I am having a hard time understanding exactly how to fix it. I haven’t used CPP a lot either so I am hoping it is something super easy that I am just missing. Here’s the log output from compiling:

Linking .pioenvs/jeep1esp/firmware.elf
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `esphome::wled::WLEDLightOutput::dump_config()':
/data/build/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp:33: multiple definition of `esphome::wled::WLEDLightOutput::dump_config()'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:/data/build/jeep1esp/src/components/wled_output/light/wled_light_output.cpp:33: first defined here
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `esphome::wled::WLEDLightOutput::dump_config()':
/data/build/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp:33: multiple definition of `non-virtual thunk to esphome::wled::WLEDLightOutput::dump_config()'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:/data/build/jeep1esp/src/components/wled_output/light/wled_light_output.cpp:33: first defined here
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `esphome::wled::WLEDLightOutput::on_shutdown()':
/data/build/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp:15: multiple definition of `esphome::wled::WLEDLightOutput::on_shutdown()'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:/data/build/jeep1esp/src/components/wled_output/light/wled_light_output.cpp:15: first defined here
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `non-virtual thunk to esphome::wled::WLEDLightOutput::on_shutdown()':
wled_light_output.cpp:(.text._ZThn4_N7esphome4wled15WLEDLightOutput11on_shutdownEv+0x0): multiple definition of `non-virtual thunk to esphome::wled::WLEDLightOutput::on_shutdown()'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:wled_light_output.cpp:(.text._ZThn4_N7esphome4wled15WLEDLightOutput11on_shutdownEv+0x0): first defined here
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `esphome::wled::WLEDLightOutput::write_state(esphome::light::LightState*)':
/data/build/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp:43: multiple definition of `esphome::wled::WLEDLightOutput::write_state(esphome::light::LightState*)'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:/data/build/jeep1esp/src/components/wled_output/light/wled_light_output.cpp:43: first defined here
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `esphome::wled::WLEDLightOutput::setup()':
/data/build/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp:24: multiple definition of `esphome::wled::WLEDLightOutput::setup()'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:/data/build/jeep1esp/src/components/wled_output/light/wled_light_output.cpp:24: first defined here
/data/cache/platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pioenvs/jeep1esp/src/esphome/components/wled_output/light/wled_light_output.cpp.o: in function `non-virtual thunk to esphome::wled::WLEDLightOutput::setup()':
wled_light_output.cpp:(.text._ZThn4_N7esphome4wled15WLEDLightOutput5setupEv+0x0): multiple definition of `non-virtual thunk to esphome::wled::WLEDLightOutput::setup()'; .pioenvs/jeep1esp/src/components/wled_output/light/wled_light_output.cpp.o:wled_light_output.cpp:(.text._ZThn4_N7esphome4wled15WLEDLightOutput5setupEv+0x0): first defined here

Here’s the .h file:

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/network/ip_address.h"
#include <IPAddress.h>
#include "esphome/components/light/addressable_light.h"

#include <vector>
#include <memory>

#include <WiFi.h>


// class UDP;

namespace esphome {
namespace wled {

class WLEDLightOutput : public light::AddressableLight {
 public:
  explicit WLEDLightOutput(int num_leds) {
    this->num_leds_ = num_leds;
    this->leds_ = new Color[num_leds];  // NOLINT
    this->leds_last_state = new Color[num_leds];  // NOLINT
    for (int i = 0; i < num_leds; i++) {
      this->leds_[i] = Color::BLACK;
      this->leds_last_state[i] = Color::WHITE; // init to differ color to change 
    }
    this->effect_data_ = new uint8_t[num_leds];  // NOLINT
  }


  light::LightTraits get_traits() override {
    auto traits = light::LightTraits();
    traits.set_supported_color_modes({light::ColorMode::RGB});
    return traits;
  }

  void setup();
  void dump_config();
  // void loop() override;
  void write_state(light::LightState *state) override;
  float get_setup_priority() const override { return setup_priority::HARDWARE; }

  void clear_effect_data() override {
    for (int i = 0; i < this->size(); i++)
      this->effect_data_[i] = 0;
  }

  void on_shutdown() override;

  inline int32_t size() const override { return num_leds_; }

  void set_address(network::IPAddress address) { this->address_ = IPAddress((esphome::network::IPAddress)address); }
  void set_port(uint16_t port) { this->port_ = port; }


 protected:
  IPAddress address_;
  uint16_t port_{0};
  std::unique_ptr<WiFiUDP> udp_;

  light::ESPColorView get_view_internal(int32_t index) const override {
    return {&this->leds_[index].r,      &this->leds_[index].g, &this->leds_[index].b, nullptr,
            &this->effect_data_[index], &this->correction_};
  }

  Color *leds_{nullptr};
  Color *leds_last_state{nullptr};
  uint8_t *effect_data_{nullptr};
  int num_leds_{0};
  
  uint32_t write_count_{0};
  uint32_t write_duration_{0};
};

}  // namespace wled
}  // namespace esphome


Here’s the .cpp file:

t#ifdef USE_ARDUINO

#include "wled_light_output.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"

#ifdef USE_ESP32
#include <WiFi.h>
#endif

#ifdef USE_ESP8266
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#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 };

static const char *const TAG = "wled_light_output";

void WLEDLightOutput::on_shutdown() {
  if (udp_) {
    udp_->stop();
    udp_.reset();
  }

  Component::on_shutdown();
}

void WLEDLightOutput::setup() {
  ESP_LOGCONFIG(TAG, "Setting up WLED Light Output...");  

  // Init UDP lazily
  if (!udp_) {
    udp_ = make_unique<WiFiUDP>();
  }
}

void WLEDLightOutput::dump_config() {
  ESP_LOGCONFIG(TAG, "WLED Light Output:");
  ESP_LOGCONFIG(TAG, "  Num LEDs: %u", this->num_leds_);
  //ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_);
}

// void WLEDLightOutput::loop() {
  
// }

void WLEDLightOutput::write_state(light::LightState *state) {
  if (!state->remote_values.is_on())
    return;

  // send packets here

  uint32_t start_micros = micros();

  if (!udp_->beginPacket(address_, port_)) {
    ESP_LOGW(TAG, "Cannot connect WLEDLightOutput to %d.", port_);
    return;
  }

  // Byte 0 of the UDP packet tells the server which realtime protocol to use.
  udp_->write(WARLS);

  // In every protocol, Byte 1 tells the server how many seconds to wait after the last received packet before returning to normal mode, 
  // in practice you should use 1-2 (seconds) here in most cases so that the module returns to normal mode quickly after the end of transmission. 
  // Use 255 to stay on the UDP data without a timeout until a request is requested via another method.
  udp_->write(1);

  for (int i=0; i<this->num_leds_; i++) {

    if (this->leds_last_state[i].raw_32 == this->leds_[i].raw_32)
      continue; // no change in LED

    // After this the LED color information is transmitted like this:
    // WARLS
    // Byte	Description
    // 2 + n*4	LED Index
    // 3 + n*4	Red Value
    // 4 + n*4	Green Value
    // 5 + n*4	Blue Value    

    udp_->write(i); 
    udp_->write(this->leds_[i].r); 
    udp_->write(this->leds_[i].g); 
    udp_->write(this->leds_[i].b);

    this->leds_last_state[i] = this->leds_[i];
  }

  if (!udp_->endPacket()) {
    ESP_LOGW(TAG, "Cannot send packet from WLEDLightOutput to %d.", port_);
    return;
  }

  start_micros = micros() - start_micros;
  this->write_duration_ += start_micros;
  this->write_count_++;

  if (this->write_count_ % 50 == 0) {
    ESP_LOGD(TAG, "Write duration: %d us (%d us avg)", start_micros, this->write_duration_ / this->write_count_);
  }

  // this->mark_shown_();
  // if (state->remote_values.is_on()) {
    
  // }
  this->schedule_show();
}

}  // namespace wled
}  // namespace esphome



And the init.py

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light
from esphome.components.network import IPAddress
from esphome.const import (
    CONF_ADDRESS,
    CONF_PORT,
    CONF_OUTPUT_ID,
    CONF_NUM_LEDS,
    CONF_MAX_REFRESH_RATE,
)

wled_ns = cg.esphome_ns.namespace("wled")
WLEDLightOutput = wled_ns.class_("WLEDLightOutput", light.AddressableLight)

CONFIG_SCHEMA = cv.All(
    light.ADDRESSABLE_LIGHT_SCHEMA.extend(
        {
            cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(WLEDLightOutput),
            cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
            cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
            cv.Required(CONF_ADDRESS): cv.string, # Edited from github
            cv.Optional(CONF_PORT, 21324): cv.positive_not_null_int,
        }
    ).extend(cv.COMPONENT_SCHEMA)
    , cv.only_with_arduino)


async def to_code(config):

    var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS])
    await cg.register_component(var, config)

    if CONF_MAX_REFRESH_RATE in config:
        cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))

    await light.register_light(var, config)
    
    #cg.add(var.add_leds(config[CONF_NUM_LEDS]))

    cg.add(var.set_port(config[CONF_PORT]))
    cg.add(var.set_address(IPAddress(config[CONF_ADDRESS]))); # Edited from github

Any guidance or ideas? I apologize if this is the wrong place or if I didn’t follow best practice with the post, I don’t post much on forums.

Automate on,

DaviBoi

EDIT: Clarification

So… did you even try using the method thats explained in the Esphome documentation before you attempted a stale project on Git that doesn’t appear to be maintained/updated for 3 years??

Even using this method, im confused about what your even doing, talking about cpp and compile errors because, based on the github project you linked too, it didnt even require any cpp coding or is this the log from trying to compile a config with including this project and its their code that is throwing errors??

Step 1. Install the custom component they provide you.

Step 2. Include custom component in your esphome yaml file and use regular Eaphome syntax just like the example they give you so you know that…

No where does it instruct you to modify any of the cpp or .h files they provide you. There are are simply 2 steps you’re required to do, thats it…

You’re probably right with this being an older config. I implemented it with the attached config, included it per the ESPHome documentation and got an error with the ipv4 data type for the IP which is when I had to modify the .py, and .h file. I figured (only found information on the IPAddress Python module, not ESPHome) that the data type for IP inputs changed in the past few years so switched that to the esphome::network::IPAddress data type per the ESPhome documentation and that specific error went away.

Really was just looking for if someone saw a blatant error or if they are using this custom component as I commented and the author hasn’t responded. I guess I’ll have to look at picking this project up if it is that important to me :slight_smile:

The purpose of this component is to control a WLED node directly from an ESPhome node without anything (besides the network) between them.

DaviBoi

Ya, you gotta watch out for those abandoned projects online or even YouTube videos with age.

Ya, i get what your trying to do but, what i dont get and why you didnt answer my question about why your taking this possible solution instead of using the solution they provide you in the Esphome documentation? That stuff is kept updated and is accurate unlike so much that isnt all over the internet…

Why arent you using this? Light Component — ESPHome

1 Like

Geez I totally didn’t see that, thanks a ton for posting that! I saw the integration for directly hooking up to the GPIO but didn’t stumble across the network version.

I wish I would have asked sooner, you just save me a ton of time :slight_smile:

DaviBoi

Oh, no worries man! Just be sure to pay my bill when you get it in the mail so i dont have to send “Bubba” my debt collector… ; )

I havnt used this feature and ive never gotten the impression that its something that people really like to use or be able to figure out how to use it FYI.

Thats not discouragement from me, im just saying.