From bb91890389d19958096281f4c128f0dbfc7c40cb Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 7 Apr 2021 01:54:10 +0200 Subject: [PATCH] New light transition code implemented and it works beautifully! Transitions between RGB colors and RGB to and from white light now look smooth. Some more code cleanup in the next step, but already loving how this is coming together now. --- color_night_light.h | 20 +++---- color_off.h | 27 ++++++++++ color_rgb_light.h | 33 +++++++----- color_translator.h | 72 +++++++++++++++++++++++++ color_white_light.h | 18 +++---- common.h | 36 ++++++++++--- light.h | 128 +++++++++++++++----------------------------- 7 files changed, 211 insertions(+), 123 deletions(-) create mode 100644 color_off.h create mode 100644 color_translator.h diff --git a/color_night_light.h b/color_night_light.h index ef73e4a..d3fd5a9 100644 --- a/color_night_light.h +++ b/color_night_light.h @@ -3,21 +3,23 @@ #include #include +#include "common.h" + namespace esphome { namespace yeelight { namespace bs2 { -class ColorNightLight -{ +class ColorNightLight : public GPIOOutputs { public: - // Based on measurements using the original device firmware. - float red = 0.968f; - float green = 0.968f; - float blue = 0.972f; - float white = 0.0f; + bool set_light_color_values(light::LightColorValues v) { + values = v; + // Based on measurements using the original device firmware. + red = 0.968f; + green = 0.968f; + blue = 0.972f; + white = 0.0f; - void set_color(float red, float green, float blue, float brightness, float state) - { + return true; } }; diff --git a/color_off.h b/color_off.h new file mode 100644 index 0000000..ef0a955 --- /dev/null +++ b/color_off.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "common.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +class ColorOff : public GPIOOutputs { +public: + bool set_light_color_values(light::LightColorValues v) { + values = v; + red = 0.0f; + green = 0.0f; + blue = 0.0f; + white = 0.0f; + + return true; + } +}; + +} // namespace yeelight_bs2 +} // namespace yeelight +} // namespace bs2 diff --git a/color_rgb_light.h b/color_rgb_light.h index afe8a44..f008cb4 100644 --- a/color_rgb_light.h +++ b/color_rgb_light.h @@ -8,6 +8,8 @@ #include #include +#include "common.h" + namespace esphome { namespace yeelight { namespace bs2 { @@ -243,19 +245,15 @@ static const RGBCircle rgb_circle_ {{ }} }}; -class ColorRGBLight -{ +class ColorRGBLight : public GPIOOutputs { public: - float red = 0; - float green = 0; - float blue = 0; - float white = 0; + bool set_light_color_values(light::LightColorValues v) { + values = v; - void set_color(float red, float green, float blue, float brightness) { // Determine the ring level for the color. This is a value between // 0 and 7, determining in what ring of the RGB circle the requested // color resides. - auto rgb_min = min(min(red, green), blue); + auto rgb_min = min(min(v.get_red(), v.get_green()), v.get_blue()); auto level = 7.0f * rgb_min; // While the default color circle in Home Assistant presents only a @@ -266,19 +264,28 @@ public: // Determine duty cycle measurements for the outer ring. auto level_a = floor(level); - set_duty_cycles_(&rgbp_a_, level_a, red, green, blue, brightness, &rgb_a_); + set_duty_cycles_( + &rgbp_a_, level_a, v.get_red(), v.get_green(), v.get_blue(), + v.get_brightness(), &rgb_a_); // Determine duty cycle measurements for the inner ring. auto level_b = ceil(level); - set_duty_cycles_(&rgbp_b_, level_b, red, green, blue, brightness, &rgb_b_); + set_duty_cycles_( + &rgbp_b_, level_a, v.get_red(), v.get_green(), v.get_blue(), + v.get_brightness(), &rgb_b_); // Almost there! We now have the correct duty cycles for the // two rings that we were looking at. In this last step, the // two values are interpolated based on the ring level. auto d = level - level_a; - this->red = rgb_a_.red + d * (rgb_b_.red - rgb_a_.red); - this->green = rgb_a_.green + d * (rgb_b_.green - rgb_a_.green); - this->blue = rgb_a_.blue + d * (rgb_b_.blue - rgb_a_.blue); + red = rgb_a_.red + d * (rgb_b_.red - rgb_a_.red); + green = rgb_a_.green + d * (rgb_b_.green - rgb_a_.green); + blue = rgb_a_.blue + d * (rgb_b_.blue - rgb_a_.blue); + + // The white output channel will always be 0 for RGB. + white = 0.0f; + + return true; } protected: diff --git a/color_translator.h b/color_translator.h new file mode 100644 index 0000000..41d8916 --- /dev/null +++ b/color_translator.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "common.h" +#include "color_off.h" +#include "color_night_light.h" +#include "color_white_light.h" +#include "color_rgb_light.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +/// This class translates LightColorValues into GPIO duty cycles +/// for representing a requested light color. +/// +/// The code handles all known light modes for the device: +/// +/// - off: the light is off +/// - night light: activated when brightness is at its lowest +/// - white light: based on color temperature + brightness +/// - RGB light: based on RGB values + brightness +class ColorTranslator : public GPIOOutputs { +public: + bool set_light_color_values(light::LightColorValues v) { + values = v; + + GPIOOutputs *delegate = nullptr; + + // Well, not much light here! Use the off "color". + if (v.get_state() == 0.0f || v.get_brightness() == 0.0f) { + delegate = off_light_; + } + // At the lowest brightness setting, switch to night light mode. + // In the Yeelight integration in Home Assistant, this feature is + // exposed trough a separate switch. I have found that the switch + // is both confusing and made me run into issues when automating + // the lights. + // I don't simply check for a brightness at or below 0.01 (1%), + // because the lowest brightness setting from Home Assistant + // turns up as 0.011765 in here (which is 3/255). + else if (v.get_brightness() < 0.012f) { + delegate = night_light_; + } + // When white light is requested, then use the color temperature + // white light mode: temperature + brightness. + else if (v.get_white() > 0.0f) { + delegate = white_light_; + } + // Otherwise, use RGB color mode: red, green, blue + brightness. + else { + delegate = rgb_light_; + } + + delegate->set_light_color_values(v); + delegate->copy_to(this); + + return true; + } + +protected: + GPIOOutputs *off_light_ = new ColorOff(); + GPIOOutputs *rgb_light_ = new ColorRGBLight(); + GPIOOutputs *white_light_ = new ColorWhiteLight(); + GPIOOutputs *night_light_ = new ColorNightLight(); +}; + +} // namespace yeelight_bs2 +} // namespace yeelight +} // namespace bs2 diff --git a/color_white_light.h b/color_white_light.h index 46d85c8..cb902b6 100644 --- a/color_white_light.h +++ b/color_white_light.h @@ -59,18 +59,13 @@ static const RGBWLevelsTable rgbw_levels_100_ {{ { 153.0f, 1.000f, 0.000f, 0.187f, 0.335f } }}; -class ColorWhiteLight -{ +class ColorWhiteLight : public GPIOOutputs { public: - float red = 0; - float green = 0; - float blue = 0; - float white = 0; + bool set_light_color_values(light::LightColorValues v) { + values = v; - void set_color(float temperature, float brightness) - { - temperature = clamp_temperature_(temperature); - brightness = clamp_brightness_(brightness); + auto temperature = clamp_temperature_(v.get_color_temperature()); + auto brightness = clamp_brightness_(v.get_brightness()); auto levels_1 = lookup_in_table_(rgbw_levels_1_, temperature); auto levels_100 = lookup_in_table_(rgbw_levels_100_, temperature); @@ -79,6 +74,8 @@ public: green = interpolate_(levels_1.green, levels_100.green, brightness); blue = interpolate_(levels_1.blue, levels_100.blue, brightness); white = interpolate_(levels_1.white, levels_100.white, brightness); + + return true; } protected: @@ -108,6 +105,7 @@ protected: throw std::invalid_argument("received too low temperature"); } + // TODO Use esphome::lerp? float interpolate_(float level_1, float level_100, float brightness) { auto coefficient = (level_100 - level_1) / 0.99f; diff --git a/common.h b/common.h index feee466..2622cf8 100644 --- a/common.h +++ b/common.h @@ -15,12 +15,36 @@ namespace bs2 { /// the original Yeelight firmware. static const int MIRED_MAX = 588; - /// This struct is used to hold GPIO pin duty cycles. - struct DutyCycles { - float red; - float green; - float blue; - float white; + /// This abstract class is used for building classes that translate + /// LightColorValues into the required GPIO pin outputs to represent + /// the requested color on the device. + class GPIOOutputs { + public: + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + float white = 0.0f; + light::LightColorValues values; + + /// Set the red, green, blue, white fields to the PWM duty cycles + /// that are required to represent the requested light color for + /// the provided LightColorValues input. + /// Returns true when the class can handle the input, false otherwise. + virtual bool set_light_color_values(light::LightColorValues v) = 0; + + /// Copy the output values to another GPIOOutputs object. + void copy_to(GPIOOutputs *other) { + other->red = red; + other->green = green; + other->blue = blue; + other->white = white; + other->values = values; + } + + void log(const char *prefix) { + ESP_LOGD(TAG, "%s: RGB=[%f,%f,%f], white=%f", + prefix, red, green, blue, white); + } }; } // namespace bs2 diff --git a/light.h b/light.h index 605b664..aef6643 100644 --- a/light.h +++ b/light.h @@ -22,8 +22,8 @@ namespace bs2 { /// This is an interface definition that is used to extend the /// YeelightBS2LightOutput class with methods to access properties - /// of an active LightTranformer from the YeelightBS2LightOutput - /// class. + /// of an active LightTranformer from the TransitionHandler class. + /// /// The transformer is protected in the light output class, making /// it impossible to access these properties directly from the /// light output class. @@ -36,65 +36,6 @@ namespace bs2 { virtual float get_transformer_progress() = 0; }; - /// This class translates LightColorValues into GPIO duty cycles - /// for representing a requested light color. - class ColorTranslator : public DutyCycles { - public: - void set_light_color_values(light::LightColorValues values) { - // The light is turned off. - if (values.get_state() == 0.0f || values.get_brightness() == 0.0f) { - red = 0.0f; - green = 0.0f; - blue = 0.0f; - white = 0.0f; - return; - } - - // At the lowest brightness setting, switch to night light mode. - // In the Yeelight integration in Home Assistant, this feature is - // exposed trough a separate switch. I have found that the switch - // is both confusing and made me run into issues when automating - // the lights. - // I don't simply check for a brightness at or below 0.01 (1%), - // because the lowest brightness setting from Home Assistant - // turns up as 0.011765 in here (which is 3/255). - if (values.get_brightness() < 0.012f) { - // TODO make the color implementations return a DutyCycles object? - // TODO Use polymorphic color classes? - red = night_light_.red; - green = night_light_.green; - blue = night_light_.blue; - white = night_light_.white; - return; - } - - // White light mode: temperature + brightness. - if (values.get_white() > 0.0f) { - auto temperature = values.get_color_temperature(); - white_light_.set_color(temperature, values.get_brightness()); - red = white_light_.red; - green = white_light_.green; - blue = white_light_.blue; - white = white_light_.white; - return; - } - - // RGB color mode: red, green, blue + brightness. - rgb_light_.set_color( - values.get_red(), values.get_green(), values.get_blue(), - values.get_brightness()); - red = rgb_light_.red; - green = rgb_light_.green; - blue = rgb_light_.blue; - white = rgb_light_.white; - } - - protected: - ColorWhiteLight white_light_; - ColorRGBLight rgb_light_; - ColorNightLight night_light_; - }; - /// This class is used to handle color transition requirements. /// /// When using the default ESPHome logic, transitioning is done by @@ -113,35 +54,43 @@ namespace bs2 { /// This class handles transitions by not varying the light properties /// over time, but by transitioning the LEDC duty cycle output levels /// over time. This matches the behavior of the original firmware. - class TransitionHandler { + class TransitionHandler : public GPIOOutputs { public: TransitionHandler(LightStateDataExposer *exposer) : exposer_(exposer) {} - bool handle() { - if (!do_handle_()) { + bool set_light_color_values(light::LightColorValues values) { + if (!has_active_transition_()) { + start_values = values; active_ = false; return false; } if (is_fresh_transition_()) { - auto start = exposer_->get_transformer_values(); - auto end = exposer_->get_transformer_end_values(); - active_ = true; + start_->set_light_color_values(start_values); + end_->set_light_color_values(exposer_->get_transformer_end_values()); + active_ = true; } + auto progress = exposer_->get_transformer_progress(); + red = esphome::lerp(progress, start_->red, end_->red); + green = esphome::lerp(progress, start_->green, end_->green); + blue = esphome::lerp(progress, start_->blue, end_->blue); + white = esphome::lerp(progress, start_->white, end_->white); + return true; } protected: - LightStateDataExposer *exposer_; bool active_ = false; - DutyCycles start_; - DutyCycles end_; + LightStateDataExposer *exposer_; + light::LightColorValues start_values; + GPIOOutputs *start_ = new ColorTranslator(); + GPIOOutputs *end_ = new ColorTranslator(); /// Checks if this class will handle the light output logic. /// This is the case when a transformer is active and this /// transformer does implement a transitioning effect. - bool do_handle_() { + bool has_active_transition_() { if (!exposer_->has_active_transformer()) return false; if (!exposer_->transformer_is_transition()) @@ -154,11 +103,14 @@ namespace bs2 { /// be in progress or when a new end state is found during an /// ongoing transition. bool is_fresh_transition_() { - bool is_fresh = false; if (active_ == false) { - is_fresh = true; + return true; } - return is_fresh; + auto new_end_values = exposer_->get_transformer_end_values(); + if (new_end_values != end_->values) { + return true; + } + return false; } }; @@ -216,7 +168,7 @@ namespace bs2 { return traits; } - /// Tranlates a requested light state into physicial GPIO outputs. + /// Applies a requested light state to the physicial GPIO outputs. void write_state(light::LightState *state) { auto values = state->current_values; @@ -237,8 +189,14 @@ namespace bs2 { return; } - if (transition_->handle()) { - ESP_LOGD(TAG, "HANDLE transition!"); + if (transition_handler_->set_light_color_values(values)) { + master2_->turn_on(); + master1_->turn_on(); + red_->set_level(transition_handler_->red); + green_->set_level(transition_handler_->green); + blue_->set_level(transition_handler_->blue); + white_->set_level(transition_handler_->white); + return; } #ifdef TRANSITION_TO_OFF_BUGFIX @@ -259,13 +217,13 @@ namespace bs2 { previous_state_ = values.get_state(); #endif - duty_cycles_.set_light_color_values(values); + instant_handler_->set_light_color_values(values); master2_->turn_on(); master1_->turn_on(); - red_->set_level(duty_cycles_.red); - green_->set_level(duty_cycles_.green); - blue_->set_level(duty_cycles_.blue); - white_->set_level(duty_cycles_.white); + red_->set_level(instant_handler_->red); + green_->set_level(instant_handler_->green); + blue_->set_level(instant_handler_->blue); + white_->set_level(instant_handler_->white); } protected: @@ -275,8 +233,8 @@ namespace bs2 { ledc::LEDCOutput *white_; esphome::gpio::GPIOBinaryOutput *master1_; esphome::gpio::GPIOBinaryOutput *master2_; - TransitionHandler *transition_; - ColorTranslator duty_cycles_; // TODO move to own class DefaultHandler + TransitionHandler *transition_handler_; + ColorTranslator *instant_handler_ = new ColorTranslator(); #ifdef TRANSITION_TO_OFF_BUGFIX float previous_state_ = 1; float previous_brightness_ = -1; @@ -287,7 +245,7 @@ namespace bs2 { /// Called by the YeelightBS2LightState class, to set the object that /// can be used to access protected data from the light state object. void set_light_state_data_exposer(LightStateDataExposer *exposer) { - transition_ = new TransitionHandler(exposer); + transition_handler_ = new TransitionHandler(exposer); } };