From 31f5518c47fc517d7b13462758118e7845722437 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Sat, 3 Apr 2021 02:30:33 +0200 Subject: [PATCH] New implementation for RGB colors. It needs a round of code cleanup, but it is functional as-is. --- light.py | 6 +- rgb_light.h | 153 ++++++++++++++++++++++++++++++++++-- yeelight_bs2_light_output.h | 22 +++--- 3 files changed, 159 insertions(+), 22 deletions(-) diff --git a/light.py b/light.py index 74ce17d..ceb734c 100644 --- a/light.py +++ b/light.py @@ -27,6 +27,9 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) yield light.register_light(var, config) + led_white = yield cg.get_variable(config[CONF_WHITE]) + cg.add(var.set_white_output(led_white)) + led_red = yield cg.get_variable(config[CONF_RED]) cg.add(var.set_red_output(led_red)) @@ -36,9 +39,6 @@ def to_code(config): led_blue = yield cg.get_variable(config[CONF_BLUE]) cg.add(var.set_blue_output(led_blue)) - led_white = yield cg.get_variable(config[CONF_WHITE]) - cg.add(var.set_white_output(led_white)) - master1 = yield cg.get_variable(config[CONF_MASTER1]) cg.add(var.set_master1_output(master1)) diff --git a/rgb_light.h b/rgb_light.h index 13cb8a8..94841e9 100644 --- a/rgb_light.h +++ b/rgb_light.h @@ -2,6 +2,7 @@ #include #include +#include namespace esphome { namespace rgbww { @@ -14,8 +15,8 @@ struct RGB { }; struct RGBPoint { - RGB rgb_low; - RGB rgb_high; + RGB low; + RGB high; }; using RGBRing = std::array; @@ -29,15 +30,15 @@ using RGBCircle = std::array; * The base for this table are measurements against the original * device firmware, using the RGB color circle as used in * Home Assistant as the color space model. - + * * This circle has 7 colored rings around a white center point. * The outer ring, with the highest saturation, is numbered as 1. * The inner ring around the white center point is numbered as 7. - + * * For each ring, there are 24 color positions, starting at the * color red (0°), going around the circle clockwise via * green (120°) and blue (240°). - + * * For each color position, two RGB measurements are registered: * - one defining the duty cycles at 1% brightness * - one defining the duty cycles at 100% brightness @@ -45,7 +46,6 @@ using RGBCircle = std::array; static const RGBCircle rgb_circle_ {{ // Ring 1, min value RGB component value = 0 {{ - {{ 0.8998, 0.9997, 0.9997 }, { 0.0000, 0.9997, 0.9997 }}, // 0° [255,0,0] (red) {{ 0.8727, 0.9404, 0.9682 }, { 0.0000, 0.6758, 0.9539 }}, // 15° [255,0,63] {{ 0.8727, 0.8967, 0.9677 }, { 0.0000, 0.2389, 0.9506 }}, // 30° [255,0,126] @@ -235,7 +235,7 @@ static const RGBCircle rgb_circle_ {{ }} }}; -class YetAnotherRGBLight +class RGBLight { public: float red = 0; @@ -243,12 +243,149 @@ public: float blue = 0; float white = 0; - void set_color(float temperature, float brightness) + void set_color(float red, float green, float blue, float brightness, float state) { + // 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 ring_level = 7.0f * rgb_min; + auto ring_level_a = floor(ring_level); + auto ring_level_b = ceil(ring_level); + + // While the default color circle in Home Assistant presents only a + // subset of colors, it is possible to request colors outside this + // subset as well. Therefore, the ring level might contain a fractional + // value instead of a plain integer. To accomodate for this, + // interpolation will be done to get the final outputs. + // We'll start here by determining the ring above and below the + // ring level. + auto ring_a = rgb_circle_[ring_level_a]; + auto ring_b = rgb_circle_[ring_level_b]; + + // Now we have the two rings to work with, we'll have to look at the + // positions on these rings to determine the RGB value to use for + // each ring. Here, we have to accomodate as well for the fact that + // we only have a subset of all colors available in the configuration + // tables. Therefore, some interpolation is done here as well. + + // The ring_pos is basically a hue representation of the requested + // RGB color. This is expressed as a number of degrees around the + // color circle, starting with red (at 0°). Since we have 24 + // measurements for each ring, each measurement covers 360°/24 = 15°. + // Using that knowledge, the measurements to work with can be picked + // from the rings. + auto ring_pos = ring_pos_(red, green, blue) / 15.0f; + auto ring_pos_x = floor(ring_pos); + auto ring_pos_y = ceil(ring_pos); + + // Find RGB values for ring a. + auto rgb_a_x = ring_a[ring_pos_x]; + auto rgb_a_y = ring_a[ring_pos_y > 23 ? 0 : ring_pos_y]; + RGBPoint rgbp_a; + if (ring_pos_x == ring_pos_y) { + rgbp_a.low.red = rgb_a_x.low.red; + rgbp_a.low.green = rgb_a_x.low.green; + rgbp_a.low.blue = rgb_a_x.low.blue; + rgbp_a.high.red = rgb_a_x.high.red; + rgbp_a.high.green = rgb_a_x.high.green; + rgbp_a.high.blue = rgb_a_x.high.blue; + } else { + auto d_value = ring_pos - ring_pos_x; + rgbp_a.low.red = rgb_a_x.low.red + d_value * (rgb_a_y.low.red - rgb_a_x.low.red); + rgbp_a.low.green = rgb_a_x.low.green + d_value * (rgb_a_y.low.green - rgb_a_x.low.green); + rgbp_a.low.blue = rgb_a_x.low.blue + d_value * (rgb_a_y.low.blue - rgb_a_x.low.blue); + rgbp_a.high.red = rgb_a_x.high.red + d_value * (rgb_a_y.high.red - rgb_a_x.high.red); + rgbp_a.high.green = rgb_a_x.high.green + d_value * (rgb_a_y.high.green - rgb_a_x.high.green); + rgbp_a.high.blue = rgb_a_x.high.blue + d_value * (rgb_a_y.high.blue - rgb_a_x.high.blue); + } + + // Find RGB values for ring b. + auto rgb_b_x = ring_b[ring_pos_x]; + auto rgb_b_y = ring_b[ring_pos_y > 23 ? 0 : ring_pos_y]; + RGBPoint rgbp_b; + if (ring_pos_x == ring_pos_y) { + rgbp_b.low.red = rgb_b_x.low.red; + rgbp_b.low.green = rgb_b_x.low.green; + rgbp_b.low.blue = rgb_b_x.low.blue; + rgbp_b.high.red = rgb_b_x.high.red; + rgbp_b.high.green = rgb_b_x.high.green; + rgbp_b.high.blue = rgb_b_x.high.blue; + } else { + auto d_value = ring_pos - ring_pos_x; + rgbp_b.low.red = rgb_b_x.low.red + d_value * (rgb_b_y.low.red - rgb_b_x.low.red); + rgbp_b.low.green = rgb_b_x.low.green + d_value * (rgb_b_y.low.green - rgb_b_x.low.green); + rgbp_b.low.blue = rgb_b_x.low.blue + d_value * (rgb_b_y.low.blue - rgb_b_x.low.blue); + rgbp_b.high.red = rgb_b_x.high.red + d_value * (rgb_b_y.high.red - rgb_b_x.high.red); + rgbp_b.high.green = rgb_b_x.high.green + d_value * (rgb_b_y.high.green - rgb_b_x.high.green); + rgbp_b.high.blue = rgb_b_x.high.blue + d_value * (rgb_b_y.high.blue - rgb_b_x.high.blue); + } + + // Now we have the RGB values to use for the two rings, we can + // apply the requested brightness to the RGB values. Brightness + // values 0.01 to 1.00 make the RGB values scale linearly. In our + // RGB values, we have the low (0.01) and high (1.00) value for + // the RGB values. Combined with the brightness input, the required + // RGB values can be computed. + RGB rgb_a; + rgb_a.red = rgbp_a.low.red + (brightness - 0.01) * (rgbp_a.high.red - rgbp_a.low.red); + rgb_a.green = rgbp_a.low.green + (brightness - 0.01) * (rgbp_a.high.green - rgbp_a.low.green); + rgb_a.blue = rgbp_a.low.blue + (brightness - 0.01) * (rgbp_a.high.blue - rgbp_a.low.blue); + RGB rgb_b; + rgb_b.red = rgbp_b.low.red + (brightness - 0.01) * (rgbp_b.high.red - rgbp_b.low.red); + rgb_b.green = rgbp_b.low.green + (brightness - 0.01) * (rgbp_b.high.green - rgbp_b.low.green); + rgb_b.blue = rgbp_b.low.blue + (brightness - 0.01) * (rgbp_b.high.blue - rgbp_b.low.blue); + + // Almost there! We now have the correct RGB values for the + // two rings that we were looking at. The last step will interpolate + // these two values based on the ring level. + RGB rgb; + if (ring_level_a == ring_level_b) { + rgb.red = rgb_a.red; + rgb.green = rgb_a.green; + rgb.blue = rgb_a.blue; + } else { + auto d_value = ring_level - ring_level_a; + rgb.red = rgb_a.red + d_value * (rgb_b.red - rgb_a.red); + rgb.green = rgb_a.green + d_value * (rgb_b.green - rgb_a.green); + rgb.blue = rgb_a.blue + d_value * (rgb_b.blue - rgb_a.blue); + } + if (rgb.red < 0.01f) { + rgb.red = 0.0f; + } + + this->red = rgb.red; + this->green = rgb.green; + this->blue = rgb.blue; + this->white = 0.0f; + + ESP_LOGD("rgb", "RGB [%f,%f,%f]", rgb.red, rgb.green, rgb.blue); } +protected: + /** + * Returns the position on an RGB ring in degrees (0 - 359). + */ + float ring_pos_(float red, float green, float blue) { + auto rgb_min = min(min(red, green), blue); + auto rgb_max = max(max(red, green), blue); + auto delta = rgb_max - rgb_min; + float pos; + if (delta == 0.0f) + pos = 0.0f; + else if (red == rgb_max) + pos = 60.0f * fmod((green - blue) / delta, 6); + else if (green == rgb_max) + pos = 60.0f * ((blue - red) / delta + 2.0f); + else + pos = 60.0f * ((red - green) / delta + 4.0f); + if (pos < 0) + pos = pos + 360; + return pos; + } }; + } // namespace yeelight_bs2 } // namespace rgbww } // namespace esphome diff --git a/yeelight_bs2_light_output.h b/yeelight_bs2_light_output.h index 109243b..f64b754 100644 --- a/yeelight_bs2_light_output.h +++ b/yeelight_bs2_light_output.h @@ -22,6 +22,11 @@ // not be a problem. #define TRANSITION_TO_OFF_BUGFIX +// The PWM frequencies as used by the original device +// for driving the LED circuitry. +const float RGB_PWM_FREQUENCY = 3000.0f; +const float WHITE_PWM_FREQUENCY = 9765.0f; + namespace esphome { namespace rgbww { @@ -31,15 +36,6 @@ namespace rgbww { static const int HOME_ASSISTANT_MIRED_MIN = 153; static const int HOME_ASSISTANT_MIRED_MAX = 588; - // The PWM frequencies as used by the original device - // for driving the LED circuitry. - const float RGB_PWM_FREQUENCY = 3000.0f; - // I measured 10kHz for this channel, but making this 10000.0f results - // in the blue channel failing. So possibly this is the actual - // frequency to use (it's the frequency that provides a 13 bit - // bith depth to the PWM channel). - const float WHITE_PWM_FREQUENCY = 9765.0f; - class YeelightBS2LightOutput : public Component, public light::LightOutput { public: @@ -73,7 +69,11 @@ namespace rgbww { void set_white_output(ledc::LEDCOutput *white) { white_ = white; - white_->set_frequency(WHITE_PWM_FREQUENCY); + // Quick fix; when using 10kHz like the original device + // firmware, the blue channel will use that frequency + // instead, causing issues in the RGB color settings. + // This looks like an issue with the ledc component. + white_->set_frequency(RGB_PWM_FREQUENCY); } void set_master1_output(gpio::GPIOBinaryOutput *master1) { @@ -209,7 +209,7 @@ namespace rgbww { red_->set_level(rgb_light_.red); green_->set_level(rgb_light_.green); blue_->set_level(rgb_light_.blue); - white_->set_level(rgb_light_.white); + white_->turn_off(); } void turn_on_in_white_mode_(float temperature, float brightness)