271 lines
9.1 KiB
C++
271 lines
9.1 KiB
C++
#pragma once
|
|
#include "esphome.h"
|
|
|
|
namespace esphome {
|
|
namespace wordclock {
|
|
|
|
struct Minute {
|
|
uint8_t minute;
|
|
uint8_t hour_offset;
|
|
std::vector<uint16_t> *segments;
|
|
};
|
|
|
|
struct Hour {
|
|
uint8_t hour;
|
|
std::vector<uint16_t> *segments;
|
|
};
|
|
|
|
struct SegmentCoords {
|
|
uint16_t x1;
|
|
uint16_t y1;
|
|
uint16_t x2;
|
|
uint16_t y2;
|
|
};
|
|
|
|
class BrightnessTransformer {
|
|
public:
|
|
virtual ~BrightnessTransformer() = default;
|
|
|
|
void setup(const uint8_t start_values, const uint8_t target_values, uint32_t length) {
|
|
this->start_time_ = millis();
|
|
this->length_ = length;
|
|
this->start_values_ = start_values;
|
|
this->target_values_ = target_values;
|
|
this->start();
|
|
}
|
|
|
|
/// Indicates 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 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<uint8_t> apply() = 0;
|
|
|
|
/// This will be called after transition is finished.
|
|
virtual void stop() {}
|
|
|
|
void reset() {
|
|
this->start_time_ = millis();
|
|
}
|
|
|
|
const uint8_t &get_start_values() const { return this->start_values_; }
|
|
const uint8_t &get_target_values() const { return this->target_values_; }
|
|
|
|
protected:
|
|
/// The progress of this transition, on a scale of 0 to 1.
|
|
float get_progress_() {
|
|
uint32_t now = esphome::millis();
|
|
if (now < this->start_time_)
|
|
return 0.0f;
|
|
if (now >= this->start_time_ + this->length_)
|
|
return 1.0f;
|
|
return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f);
|
|
}
|
|
|
|
uint32_t start_time_;
|
|
uint32_t length_;
|
|
uint8_t start_values_;
|
|
uint8_t target_values_;
|
|
};
|
|
|
|
class BrightnessTransitionTransformer : public BrightnessTransformer {
|
|
public:
|
|
optional<uint8_t> apply() override {
|
|
float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_());
|
|
return esphome::lerp(v, this->start_values_, this->target_values_);
|
|
}
|
|
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); }
|
|
};
|
|
|
|
|
|
class Wordclock : public esphome::PollingComponent {
|
|
|
|
public:
|
|
// Wordclock();
|
|
Wordclock(
|
|
esphome::time::RealTimeClock *time,
|
|
esphome::addressable_light::AddressableLightDisplay *display,
|
|
esphome::light::AddressableLightState *light_state
|
|
);
|
|
|
|
void setup();
|
|
void update();
|
|
// void setup_color(uint8_t brightness, Color on_color, Color off_color);
|
|
void set_brightness(uint8_t brightness) {
|
|
this->brightness = brightness;
|
|
}
|
|
void set_on_color(Color on_color) {
|
|
this->on_color = on_color;
|
|
}
|
|
void set_off_color(Color off_color) {
|
|
this->off_color = off_color;
|
|
}
|
|
void setup_transitions(uint32_t milliseconds);
|
|
|
|
void add_segment(SegmentCoords segment) {
|
|
this->segments->push_back(segment);
|
|
}
|
|
|
|
void add_static(uint16_t segment_id) {
|
|
this->static_segments->push_back(segment_id);
|
|
}
|
|
|
|
void add_hour(uint8_t hour, std::vector<uint16_t> *segments_ptr) {
|
|
this->hours->push_back(Hour{
|
|
.hour = hour,
|
|
.segments = segments_ptr,
|
|
});
|
|
}
|
|
|
|
void add_minute(uint8_t minute, uint8_t hour_offset, std::vector<uint16_t> *segments_ptr) {
|
|
this->minutes->push_back(Minute{
|
|
.minute = minute,
|
|
.hour_offset = hour_offset,
|
|
.segments = segments_ptr,
|
|
});
|
|
}
|
|
|
|
// void add_hour_segment(uint8_t index, uint16_t segment_id) {
|
|
// this->hours->at(index).segments->push_back(segment_id);
|
|
// }
|
|
// void add_minute_segment(uint8_t index, uint16_t segment_id) {
|
|
// this->minutes->at(index).segments->push_back(segment_id);
|
|
// }
|
|
// void add_hour(uint8_t index, std::vector<uint16_t> *segments);
|
|
// void add_minute(uint8_t index, std::vector<uint16_t> *segments);
|
|
// void add_hour_offset(uint8_t index, int8_t offset) {
|
|
// this->minutes->at(index).hour_offset = offset;
|
|
// }
|
|
protected:
|
|
// ESPHome Components
|
|
esphome::time::RealTimeClock *time;
|
|
esphome::light::AddressableLightState *light_state;
|
|
esphome::addressable_light::AddressableLightDisplay *display;
|
|
|
|
// Time related state
|
|
bool valid_time{false};
|
|
int8_t current_hour{-1};
|
|
int8_t current_minute{-1};
|
|
|
|
// Segments Configuration
|
|
std::vector<SegmentCoords> *segments;
|
|
std::vector<uint16_t> *static_segments;
|
|
std::vector<Minute> *minutes;
|
|
std::vector<Hour> *hours;
|
|
|
|
BrightnessTransitionTransformer *off_transformer;
|
|
BrightnessTransitionTransformer *on_transformer;
|
|
|
|
std::vector<uint16_t> *added_segments;
|
|
std::vector<uint16_t> *removed_segments;
|
|
std::vector<uint16_t> *staying_segments;
|
|
|
|
std::vector<uint16_t> *current_segments;
|
|
std::vector<uint16_t> *previous_segments;
|
|
|
|
uint32_t current_position{0};
|
|
Color get_next_color(uint32_t position, const Color ¤t_color);
|
|
// Color
|
|
// Color get_on_color() {
|
|
// return this->on_color * this->brightness;
|
|
// }
|
|
// Color get_off_color() {
|
|
// return this->off_color * this->brightness;
|
|
// }
|
|
Color on_color {Color(0xFFFFFF)};
|
|
Color off_color {Color(0x000000)};
|
|
uint8_t brightness{255};
|
|
|
|
// Utils
|
|
int8_t find_hour(uint8_t target_value);
|
|
int8_t find_minute(uint8_t target_value);
|
|
void find_difference(std::vector<uint16_t> *a_vec, std::vector<uint16_t> *b_vec);
|
|
// void draw_segment(uint16_t segment_id) {
|
|
// this->draw_segment(segment_id, this->brightness);
|
|
// }
|
|
// void draw_segment(uint16_t segment_id, uint8_t brightness) {
|
|
// this->draw_segment(segment_id, this->on_color * brightness);
|
|
// }
|
|
|
|
// void draw_segment(uint16_t segment_id, Color color) {
|
|
// SegmentCoords s = this->segments->at(segment_id);
|
|
// // ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32);
|
|
// this->display->line(s.x1, s.y1, s.x2, s.y2, color);
|
|
// }
|
|
|
|
void segment_effect_base(uint16_t segment_id, bool to_effect, Color base_color, uint8_t transition_progress) {
|
|
// ESP_LOGD("wordclock.cpp", "brightness[%d] * color[%06x] = %06x", brightness, color.raw_32, (color * brightness).raw_32);
|
|
SegmentCoords s = this->segments->at(segment_id);
|
|
int x1 = s.x1; int y1 = s.y1; int x2 = s.x2; int y2 = s.y2;
|
|
Color color_to_draw;
|
|
Color effect_color;
|
|
|
|
const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
|
|
const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
|
|
int32_t err = dx + dy;
|
|
while (true) {
|
|
effect_color = this->get_next_color(x1 + y1, Color(0));
|
|
if (to_effect) {
|
|
color_to_draw = base_color.gradient(effect_color, transition_progress);
|
|
} else {
|
|
color_to_draw = effect_color.gradient(base_color, transition_progress);
|
|
// c = base_color.gradient(this->get_next_color(x1 + y1, Color(0)), transition_progress);
|
|
}
|
|
|
|
this->display->draw_pixel_at(x1, y1, color_to_draw);
|
|
// this->display->draw_pixel_at(x1, y1, this->get_next_color(this->current_position++, Color(0)));
|
|
if (x1 == x2 && y1 == y2) break;
|
|
int32_t e2 = 2 * err;
|
|
if (e2 >= dy) {
|
|
err += dy;
|
|
x1 += sx;
|
|
}
|
|
if (e2 <= dx) {
|
|
err += dx;
|
|
y1 += sy;
|
|
}
|
|
}
|
|
}
|
|
|
|
void enable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) {
|
|
segment_effect_base(segment_id, true, off_color, transition_progress);
|
|
}
|
|
void disable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) {
|
|
segment_effect_base(segment_id, false, off_color, transition_progress);
|
|
}
|
|
void apply_segment_effect(uint16_t segment_id) {
|
|
segment_effect_base(segment_id, true, Color(0), 255);
|
|
}
|
|
// void disable_segment(uint16_t segment_id, Color off_color, uint8_t transition_progress) {
|
|
// SegmentCoords s = this->segments->at(segment_id);
|
|
// this->display->line(s.x1, s.y1, s.x2, s.y2, color);
|
|
// }
|
|
void start_idle_animation() {
|
|
this->display->fill(this->off_color);
|
|
this->display->set_enabled(false);
|
|
this->light_state->turn_on().set_effect("random_twinkle").perform();
|
|
}
|
|
|
|
void end_idle_animation() {
|
|
this->light_state->turn_off().perform();
|
|
this->display->set_enabled(true);
|
|
this->display->fill(this->off_color);
|
|
this->previous_segments->clear();
|
|
this->current_segments->clear();
|
|
}
|
|
|
|
|
|
// std::vector<std::vector<uint16_t>> *minutes;
|
|
// std::vector<std::vector<uint16_t>> *hours;
|
|
// std::vector<int8_t> *hour_offsets;
|
|
};
|
|
|
|
} // namespace wordclock
|
|
} // namespace esphome
|
|
|