Modular light transformers (#2124)

This commit is contained in:
Oxan van Leeuwen 2021-08-11 06:51:35 +02:00 committed by GitHub
parent 11477dbc03
commit 46f17bea66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 150 deletions

View file

@ -24,6 +24,10 @@ void AddressableLight::call_setup() {
#endif
}
std::unique_ptr<LightTransformer> AddressableLight::create_default_transition() {
return make_unique<AddressableLightTransformer>(*this);
}
Color esp_color_from_light_color_values(LightColorValues val) {
auto r = to_uint8_scale(val.get_color_brightness() * val.get_red());
auto g = to_uint8_scale(val.get_color_brightness() * val.get_green());
@ -37,66 +41,67 @@ void AddressableLight::write_state(LightState *state) {
auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
this->correction_.set_local_brightness(max_brightness);
this->last_transition_progress_ = 0.0f;
this->accumulated_alpha_ = 0.0f;
if (this->is_effect_active())
return;
// don't use LightState helper, gamma correction+brightness is handled by ESPColorView
this->all() = esp_color_from_light_color_values(val);
}
if (state->transformer_ == nullptr || !state->transformer_->is_transition()) {
// no transformer active or non-transition one
this->all() = esp_color_from_light_color_values(val);
} else {
// transition transformer active, activate specialized transition for addressable effects
// instead of using a unified transition for all LEDs, we use the current state each LED as the
// start. Warning: ugly
void AddressableLightTransformer::start() {
auto end_values = this->target_values_;
this->target_color_ = esp_color_from_light_color_values(end_values);
// We can't use a direct lerp smoothing here though - that would require creating a copy of the original
// state of each LED at the start of the transition
// Instead, we "fake" the look of the LERP by using an exponential average over time and using
// dynamically-calculated alpha values to match the look of the
// our transition will handle brightness, disable brightness in correction.
this->light_.correction_.set_local_brightness(255);
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
}
float new_progress = state->transformer_->get_progress();
float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_);
float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress);
this->last_transition_progress_ = new_progress;
optional<LightColorValues> AddressableLightTransformer::apply() {
// Don't try to transition over running effects, instead immediately use the target values. write_state() and the
// effects pick up the change from current_values.
if (this->light_.is_effect_active())
return this->target_values_;
auto end_values = state->transformer_->get_end_values();
Color target_color = esp_color_from_light_color_values(end_values);
// Use a specialized transition for addressable lights: instead of using a unified transition for
// all LEDs, we use the current state of each LED as the start.
// our transition will handle brightness, disable brightness in correction.
this->correction_.set_local_brightness(255);
target_color *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
// We can't use a direct lerp smoothing here though - that would require creating a copy of the original
// state of each LED at the start of the transition.
// Instead, we "fake" the look of the LERP by using an exponential average over time and using
// dynamically-calculated alpha values to match the look.
float denom = (1.0f - new_smoothed);
float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom;
float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_());
// We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
// We solve this by accumulating the fractional part of the alpha over time.
float alpha255 = alpha * 255.0f;
float alpha255int = floorf(alpha255);
float alpha255remainder = alpha255 - alpha255int;
float denom = (1.0f - smoothed_progress);
float alpha = denom == 0.0f ? 0.0f : (smoothed_progress - this->last_transition_progress_) / denom;
this->accumulated_alpha_ += alpha255remainder;
float alpha_add = floorf(this->accumulated_alpha_);
this->accumulated_alpha_ -= alpha_add;
// We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
// We solve this by accumulating the fractional part of the alpha over time.
float alpha255 = alpha * 255.0f;
float alpha255int = floorf(alpha255);
float alpha255remainder = alpha255 - alpha255int;
alpha255 += alpha_add;
alpha255 = clamp(alpha255, 0.0f, 255.0f);
auto alpha8 = static_cast<uint8_t>(alpha255);
this->accumulated_alpha_ += alpha255remainder;
float alpha_add = floorf(this->accumulated_alpha_);
this->accumulated_alpha_ -= alpha_add;
if (alpha8 != 0) {
uint8_t inv_alpha8 = 255 - alpha8;
Color add = target_color * alpha8;
alpha255 += alpha_add;
alpha255 = clamp(alpha255, 0.0f, 255.0f);
auto alpha8 = static_cast<uint8_t>(alpha255);
for (auto led : *this)
led = add + led.get() * inv_alpha8;
}
if (alpha8 != 0) {
uint8_t inv_alpha8 = 255 - alpha8;
Color add = this->target_color_ * alpha8;
for (auto led : this->light_)
led.set(add + led.get() * inv_alpha8);
}
this->schedule_show();
this->last_transition_progress_ = smoothed_progress;
this->light_.schedule_show();
return {};
}
} // namespace light

View file

@ -8,6 +8,7 @@
#include "esp_range_view.h"
#include "light_output.h"
#include "light_state.h"
#include "transformers.h"
#ifdef USE_POWER_SUPPLY
#include "esphome/components/power_supply/power_supply.h"
@ -53,6 +54,7 @@ class AddressableLight : public LightOutput, public Component {
bool is_effect_active() const { return this->effect_active_; }
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
void write_state(LightState *state) override;
std::unique_ptr<LightTransformer> create_default_transition() override;
void set_correction(float red, float green, float blue, float white = 1.0f) {
this->correction_.set_max_brightness(
Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white)));
@ -70,6 +72,8 @@ class AddressableLight : public LightOutput, public Component {
void call_setup() override;
protected:
friend class AddressableLightTransformer;
bool should_show_() const { return this->effect_active_ || this->next_show_; }
void mark_shown_() {
this->next_show_ = false;
@ -92,6 +96,18 @@ class AddressableLight : public LightOutput, public Component {
power_supply::PowerSupplyRequester power_;
#endif
LightState *state_parent_{nullptr};
};
class AddressableLightTransformer : public LightTransitionTransformer {
public:
AddressableLightTransformer(AddressableLight &light) : light_(light) {}
void start() override;
optional<LightColorValues> apply() override;
protected:
AddressableLight &light_;
Color target_color_{};
float last_transition_progress_{0.0f};
float accumulated_alpha_{0.0f};
};

View file

@ -0,0 +1,12 @@
#include "light_output.h"
#include "transformers.h"
namespace esphome {
namespace light {
std::unique_ptr<LightTransformer> LightOutput::create_default_transition() {
return make_unique<LightTransitionTransformer>();
}
} // namespace light
} // namespace esphome

View file

@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "light_traits.h"
#include "light_state.h"
#include "light_transformer.h"
namespace esphome {
namespace light {
@ -13,6 +14,9 @@ class LightOutput {
/// Return the LightTraits of this LightOutput.
virtual LightTraits get_traits() = 0;
/// Return the default transformer used for transitions.
virtual std::unique_ptr<LightTransformer> create_default_transition();
virtual void setup_state(LightState *state) {}
virtual void write_state(LightState *state) = 0;

View file

@ -1,6 +1,7 @@
#include "light_state.h"
#include "esphome/core/log.h"
#include "light_state.h"
#include "light_output.h"
#include "transformers.h"
namespace esphome {
namespace light {
@ -105,19 +106,19 @@ void LightState::loop() {
// Apply transformer (if any)
if (this->transformer_ != nullptr) {
auto values = this->transformer_->apply();
this->next_write_ = values.has_value(); // don't write if transformer doesn't want us to
if (values.has_value())
this->current_values = *values;
if (this->transformer_->is_finished()) {
this->remote_values = this->current_values = this->transformer_->get_end_values();
this->target_state_reached_callback_.call();
if (this->transformer_->publish_at_end())
this->publish_state();
this->transformer_->stop();
this->transformer_ = nullptr;
} else {
this->current_values = this->transformer_->get_values();
this->remote_values = this->transformer_->get_remote_values();
this->target_state_reached_callback_.call();
}
this->next_write_ = true;
}
// Write state to the light
if (this->next_write_) {
this->output_->write_state(this);
this->next_write_ = false;
@ -217,18 +218,21 @@ void LightState::stop_effect_() {
}
void LightState::start_transition_(const LightColorValues &target, uint32_t length) {
this->transformer_ = make_unique<LightTransitionTransformer>(millis(), length, this->current_values, target);
this->remote_values = this->transformer_->get_remote_values();
this->transformer_ = this->output_->create_default_transition();
this->transformer_->setup(this->current_values, target, length);
this->remote_values = target;
}
void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
LightColorValues end_colors = this->current_values;
LightColorValues end_colors = this->remote_values;
// If starting a flash if one is already happening, set end values to end values of current flash
// Hacky but works
if (this->transformer_ != nullptr)
end_colors = this->transformer_->get_end_values();
this->transformer_ = make_unique<LightFlashTransformer>(millis(), length, end_colors, target);
this->remote_values = this->transformer_->get_remote_values();
end_colors = this->transformer_->get_target_values();
this->transformer_ = make_unique<LightFlashTransformer>(*this);
this->transformer_->setup(end_colors, target, length);
this->remote_values = target;
}
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
@ -240,10 +244,6 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
this->next_write_ = true;
}
void LightState::set_transformer_(std::unique_ptr<LightTransformer> transformer) {
this->transformer_ = std::move(transformer);
}
void LightState::save_remote_values_() {
LightStateRTCState saved;
saved.color_mode = this->remote_values.get_color_mode();

View file

@ -157,9 +157,6 @@ class LightState : public Nameable, public Component {
/// Internal method to set the color values to target immediately (with no transition).
void set_immediately_(const LightColorValues &target, bool set_remote_values);
/// Internal method to start a transformer.
void set_transformer_(std::unique_ptr<LightTransformer> transformer);
/// Internal method to save the current remote_values to the preferences
void save_remote_values_();

View file

@ -1,42 +1,42 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "light_color_values.h"
namespace esphome {
namespace light {
/// Base-class for all light color transformers, such as transitions or flashes.
/// Base class for all light color transformers, such as transitions or flashes.
class LightTransformer {
public:
LightTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values,
const LightColorValues &target_values)
: start_time_(start_time), length_(length), start_values_(start_values), target_values_(target_values) {}
void setup(const LightColorValues &start_values, const LightColorValues &target_values, uint32_t length) {
this->start_time_ = millis();
this->length_ = length;
this->start_values_ = start_values;
this->target_values_ = target_values;
this->start();
}
LightTransformer() = delete;
/// Indicates whether this transformation is finished.
virtual bool is_finished() { return this->get_progress_() >= 1.0f; }
/// Whether this transformation is finished
virtual bool is_finished() { return this->get_progress() >= 1.0f; }
/// This will be called before the transition is started.
virtual void start() {}
/// This will be called to get the current values for output.
virtual LightColorValues get_values() = 0;
/// This will be called while the transformer is active to apply the transition to the light. Can either write to the
/// light directly, or return LightColorValues that will be applied.
virtual optional<LightColorValues> apply() = 0;
/// The values that should be reported to the front-end.
virtual LightColorValues get_remote_values() { return this->get_target_values_(); }
/// This will be called after transition is finished.
virtual void stop() {}
/// The values that should be set after this transformation is complete.
virtual LightColorValues get_end_values() { return this->get_target_values_(); }
const LightColorValues &get_start_values() const { return this->start_values_; }
virtual bool publish_at_end() = 0;
virtual bool is_transition() = 0;
float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
const LightColorValues &get_target_values() const { return this->target_values_; }
protected:
const LightColorValues &get_start_values_() const { return this->start_values_; }
const LightColorValues &get_target_values_() const { return this->target_values_; }
/// The progress of this transition, on a scale of 0 to 1.
float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
uint32_t start_time_;
uint32_t length_;
@ -44,69 +44,5 @@ class LightTransformer {
LightColorValues target_values_;
};
class LightTransitionTransformer : public LightTransformer {
public:
LightTransitionTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values,
const LightColorValues &target_values)
: LightTransformer(start_time, length, start_values, target_values) {
// When turning light on from off state, use colors from new.
if (!this->start_values_.is_on() && this->target_values_.is_on()) {
this->start_values_ = LightColorValues(target_values);
this->start_values_.set_brightness(0.0f);
}
// When changing color mode, go through off state, as color modes are orthogonal and there can't be two active.
if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) {
this->changing_color_mode_ = true;
this->intermediate_values_ = LightColorValues(this->get_start_values_());
this->intermediate_values_.set_state(false);
}
}
LightColorValues get_values() override {
float p = this->get_progress();
// Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off.
if (this->changing_color_mode_ && p > 0.5f &&
this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) {
this->intermediate_values_ = LightColorValues(this->get_end_values());
this->intermediate_values_.set_state(false);
}
LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_;
LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_;
if (this->changing_color_mode_)
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
float v = LightTransitionTransformer::smoothed_progress(p);
return LightColorValues::lerp(start, end, v);
}
bool publish_at_end() override { return false; }
bool is_transition() override { return true; }
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
// transition from 0 to 1 on x = [0, 1]
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
protected:
bool changing_color_mode_{false};
LightColorValues intermediate_values_{};
};
class LightFlashTransformer : public LightTransformer {
public:
LightFlashTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values,
const LightColorValues &target_values)
: LightTransformer(start_time, length, start_values, target_values) {}
LightColorValues get_values() override { return this->get_target_values_(); }
LightColorValues get_end_values() override { return this->get_start_values_(); }
bool publish_at_end() override { return true; }
bool is_transition() override { return false; }
};
} // namespace light
} // namespace esphome

View file

@ -0,0 +1,75 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "light_color_values.h"
#include "light_state.h"
#include "light_transformer.h"
namespace esphome {
namespace light {
class LightTransitionTransformer : public LightTransformer {
public:
void start() override {
// When turning light on from off state, use colors from target state.
if (!this->start_values_.is_on() && this->target_values_.is_on()) {
this->start_values_ = LightColorValues(this->target_values_);
this->start_values_.set_brightness(0.0f);
}
// When changing color mode, go through off state, as color modes are orthogonal and there can't be two active.
if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) {
this->changing_color_mode_ = true;
this->intermediate_values_ = this->start_values_;
this->intermediate_values_.set_state(false);
}
}
optional<LightColorValues> apply() override {
float p = this->get_progress_();
// Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off.
if (this->changing_color_mode_ && p > 0.5f &&
this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) {
this->intermediate_values_ = this->target_values_;
this->intermediate_values_.set_state(false);
}
LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_;
LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_;
if (this->changing_color_mode_)
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
float v = LightTransitionTransformer::smoothed_progress(p);
return LightColorValues::lerp(start, end, v);
}
protected:
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
// transition from 0 to 1 on x = [0, 1]
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
bool changing_color_mode_{false};
LightColorValues intermediate_values_{};
};
class LightFlashTransformer : public LightTransformer {
public:
LightFlashTransformer(LightState &state) : state_(state) {}
optional<LightColorValues> apply() override { return this->get_target_values(); }
// Restore the original values after the flash.
void stop() override {
this->state_.current_values = this->get_start_values();
this->state_.remote_values = this->get_start_values();
this->state_.publish_state();
}
protected:
LightState &state_;
};
} // namespace light
} // namespace esphome