From 2b06f1dd1a75936efaf1e242c364f23b5838844a Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Sun, 8 Dec 2024 12:40:54 +0200 Subject: [PATCH] Reimplement LED blinker to support RGB LEDs --- src/sensesp/system/led_blinker.cpp | 181 +++++------------- src/sensesp/system/led_blinker.h | 225 +++++++++++++++++++---- src/sensesp/system/pwm_output.cpp | 65 ------- src/sensesp/system/pwm_output.h | 58 +++++- src/sensesp/system/system_status_led.cpp | 94 +++++++--- src/sensesp/system/system_status_led.h | 106 +++++++++-- src/sensesp_app.h | 29 ++- 7 files changed, 452 insertions(+), 306 deletions(-) delete mode 100644 src/sensesp/system/pwm_output.cpp diff --git a/src/sensesp/system/led_blinker.cpp b/src/sensesp/system/led_blinker.cpp index 173f64a56..ddfd3326d 100644 --- a/src/sensesp/system/led_blinker.cpp +++ b/src/sensesp/system/led_blinker.cpp @@ -1,154 +1,63 @@ - -#include "sensesp.h" - #include "led_blinker.h" -#include "sensesp_app.h" - namespace sensesp { -#define max(a, b) ((a) > (b) ? (a) : (b)) - -BaseBlinker::BaseBlinker(int pin) : pin_{pin} { - pinMode(pin, OUTPUT); - event_loop()->onDelay(1, [this]() { this->tick(); }); -} +LEDPatternFragment frag_solid_color(uint32_t duration_ms, const CRGB& color) { + return LEDPatternFragment(duration_ms, + [color](uint32_t, CRGB& crgb) { crgb = color; }); +} + +LEDPatternFragment frag_linear_fade(uint32_t duration_ms, + uint32_t fade_duration_ms, + const CRGB& target_color) { + LEDPatternFragment fragment(duration_ms, [target_color, fade_duration_ms]( + uint32_t elapsed_ms, + CRGB& crgb) { + static CRGB from_color; + static unsigned long last_elapsed_ms = 0; + if (elapsed_ms < last_elapsed_ms) { + from_color = crgb; + } + last_elapsed_ms = elapsed_ms; + if (elapsed_ms >= fade_duration_ms) { + crgb = target_color; + return; + } + crgb = blend(from_color, target_color, elapsed_ms * 256 / fade_duration_ms); + }); -/** - * Turn the LED on or off. - */ -void BaseBlinker::set_state(bool state) { - this->state_ = state; - digitalWrite(pin_, state); - update_counter_++; + return fragment; } -/** - * Invert the current LED state. - */ -void BaseBlinker::flip_state() { this->set_state(!this->state_); } - -/** - * Flip the LED off and on for `duration` milliseconds. - */ -void BaseBlinker::blip(int duration) { - // indicator for a blip being in progress - static bool blipping = false; - - // only allow one blip at a time - if (blipping) { - return; - } - blipping = true; - - bool const orig_state = this->state_; - this->set_state(false); - int const current_counter = this->update_counter_; - event_loop()->onDelay( - duration, [this, duration, orig_state, current_counter]() { - // only update if no-one has touched the LED in the meanwhile - if (this->update_counter_ == current_counter) { - this->set_state(true); - int const new_counter = this->update_counter_; - event_loop()->onDelay( - duration, [this, orig_state, new_counter]() { - // again, only update if no-one has touched the LED - if (this->update_counter_ == new_counter) { - this->set_state(orig_state); - } - blipping = false; - }); +LEDPatternFragment frag_linear_invert(uint32_t duration_ms, bool reverse) { + return LEDPatternFragment( + duration_ms, [duration_ms, reverse](uint32_t elapsed_ms, CRGB& crgb) { + // Blend the color from current color to inverted + if (reverse) { + crgb = blend(CRGB(255, 255, 255) - crgb, crgb, + elapsed_ms * 256 / duration_ms); } else { - blipping = false; + crgb = blend(crgb, CRGB(255, 255, 255) - crgb, + elapsed_ms * 256 / duration_ms); } }); } -/** - * Enable or disable the blinker. - */ -void BaseBlinker::set_enabled(bool state) { - bool const was_enabled = this->enabled_; - this->enabled_ = state; - if (this->enabled_) { - this->tick(); - } else { - this->set_state(false); - if (was_enabled) { - event_->remove(event_loop()); +LEDPatternFragment frag_blend(uint32_t duration_ms, const CRGB& target_color, + bool reverse) { + return LEDPatternFragment(duration_ms, [duration_ms, reverse, target_color]( + uint32_t elapsed_ms, CRGB& crgb) { + if (reverse) { + crgb = blend(target_color, crgb, elapsed_ms * 255 / duration_ms); + } else { + crgb = blend(crgb, target_color, elapsed_ms * 255 / duration_ms); } - } -} - -PeriodicBlinker::PeriodicBlinker(int pin, unsigned int period) - : BaseBlinker(pin), period_{period} {} - -EvenBlinker::EvenBlinker(int pin, unsigned int period) - : PeriodicBlinker(pin, period) {} - -void EvenBlinker::tick() { - if (!enabled_) { - return; - } - this->flip_state(); - event_ = event_loop()->onDelay( - period_, [this]() { this->tick(); }); -} - -RatioBlinker::RatioBlinker(int pin, unsigned int period, float ratio) - : PeriodicBlinker(pin, period), ratio_{ratio} {} - -void RatioBlinker::tick() { - if (!enabled_) { - return; - } - this->flip_state(); - int const on_duration = ratio_ * period_; - int const off_duration = max(0, period_ - on_duration); - unsigned int const ref_duration = - state_ == false ? off_duration : on_duration; - event_ = event_loop()->onDelay( - ref_duration, [this]() { this->tick(); }); -} - -PatternBlinker::PatternBlinker(int pin, int pattern[]) - : BaseBlinker(pin), pattern_{pattern} {} - -/** - * Set a new blink pattern. Patterns are arrays of ints, with - * PATTERN_END as the last value. Each number in the pattern indicates - * the length of that segment, in milliseconds. The first number indicates - * an ON duration, the second an OFF duration, and so on. - */ -void PatternBlinker::set_pattern(int pattern[]) { - this->pattern_ = pattern; - this->restart(); -} - -void PatternBlinker::tick() { - if (!enabled_) { - return; - } - // When pattern[pattern_ptr] == PATTERN_END, that's the end of the pattern, - // so start over at the beginning. - if (pattern_[pattern_ptr_] == PATTERN_END) { - pattern_ptr_ = 0; - } - // odd indices indicate times when LED should be OFF, even when ON - bool const new_state = (pattern_ptr_ % 2) == 0; - this->set_state(new_state); - event_ = event_loop()->onDelay( - pattern_[pattern_ptr_++], [this]() { this->tick(); }); + }); } -void PatternBlinker::restart() { - state_ = false; - pattern_ptr_ = 0; - if (event_ != NULL) { - event_->remove(event_loop()); - event_ = NULL; - this->tick(); - } +LEDPatternFragment frag_nop(uint32_t duration_ms) { + // No operation + return LEDPatternFragment(duration_ms, [](uint32_t, CRGB&) {}); } } // namespace sensesp diff --git a/src/sensesp/system/led_blinker.h b/src/sensesp/system/led_blinker.h index 087347411..c6c6c541c 100644 --- a/src/sensesp/system/led_blinker.h +++ b/src/sensesp/system/led_blinker.h @@ -1,12 +1,15 @@ #ifndef SENSESP_SYSTEM_LED_BLINKER_H_ #define SENSESP_SYSTEM_LED_BLINKER_H_ +#include +#include #include #include "sensesp/signalk/signalk_ws_client.h" namespace sensesp { +#define max(a, b) ((a) > (b) ? (a) : (b)) #define PATTERN_END (-1) /** @@ -14,11 +17,74 @@ namespace sensesp { */ class BaseBlinker { public: - BaseBlinker(int pin); - void set_state(bool state); - void flip_state(); - void blip(int duration = 20); - void set_enabled(bool state); + BaseBlinker(int pin) : pin_{pin} { + pinMode(pin, OUTPUT); + event_loop()->onDelay(1, [this]() { this->tick(); }); + } + /** + * Turn the LED on or off. + */ + void set_state(bool state) { + this->state_ = state; + digitalWrite(pin_, state); + update_counter_++; + } + + /** + * Invert the current LED state. + */ + void flip_state() { this->set_state(!this->state_); } + + /** + * Flip the LED off and on for `duration` milliseconds. + */ + void blip(int duration = 20) { + // indicator for a blip being in progress + static bool blipping = false; + + // only allow one blip at a time + if (blipping) { + return; + } + blipping = true; + + bool const orig_state = this->state_; + this->set_state(false); + int const current_counter = this->update_counter_; + event_loop()->onDelay( + duration, [this, duration, orig_state, current_counter]() { + // only update if no-one has touched the LED in the meanwhile + if (this->update_counter_ == current_counter) { + this->set_state(true); + int const new_counter = this->update_counter_; + event_loop()->onDelay(duration, [this, orig_state, new_counter]() { + // again, only update if no-one has touched the LED + if (this->update_counter_ == new_counter) { + this->set_state(orig_state); + } + blipping = false; + }); + } else { + blipping = false; + } + }); + } + + /** + * Enable or disable the blinker. + */ + void set_enabled(bool state) { + bool const was_enabled = this->enabled_; + this->enabled_ = state; + if (this->enabled_) { + this->tick(); + } else { + this->set_state(false); + if (was_enabled) { + event_->remove(event_loop()); + } + } + } /** * Tick is called whenever the blinker is enabled or when it's time to * change the LED state. @@ -33,58 +99,137 @@ class BaseBlinker { reactesp::Event* event_ = NULL; }; -/** - * @brief A base class for periodic blinkers. - */ -class PeriodicBlinker : public BaseBlinker { +class LEDPattern; +using FragmentCallback = std::function; + +class LEDPatternFragment { public: - PeriodicBlinker(int pin, unsigned int period); - void set_period(unsigned int period) { this->period_ = period; } + LEDPatternFragment(uint32_t duration_ms, FragmentCallback callback) + : duration_ms_(duration_ms), callback_(callback) {} - protected: - unsigned int period_; -}; + // Copy constructor + LEDPatternFragment(const LEDPatternFragment& other) + : duration_ms_(other.duration_ms_), callback_(other.callback_) {} -/** - * @brief An LED blinker class that blinks the LED 50% off, 50% on, - * at a given period. - */ -class EvenBlinker : public PeriodicBlinker { - public: - EvenBlinker(int pin, unsigned int period); - void tick() override final; + // Assignment operator + LEDPatternFragment& operator=(const LEDPatternFragment& other) { + duration_ms_ = other.duration_ms_; + callback_ = other.callback_; + return *this; + } + + uint32_t duration_ms_; + FragmentCallback callback_; }; -/** - * @brief A periodic blinker that defines both the on-ratio - * and the period length. - */ -class RatioBlinker : public PeriodicBlinker { +class LEDPattern { public: - RatioBlinker(int pin, unsigned int period, float ratio = 0.); - void tick() override final; - void set_ratio(unsigned int ratio) { this->ratio_ = ratio; } + LEDPattern() {} + LEDPattern(const std::vector& fragments) + : fragments_(fragments) {} + LEDPattern(const std::initializer_list& fragments) + : fragments_(fragments), current_fragment_idx_(0), fragment_begin_ms_(0) {} + + // Assignment operator + LEDPattern& operator=(const LEDPattern& other) { + fragments_ = other.fragments_; + return *this; + } + + bool apply(CRGB& crgb, bool oneshot = false) { + // Initialize fragment begin time if it's the first time + if (fragment_begin_ms_ == 0) { + fragment_begin_ms_ = millis(); + } + unsigned long current_fragment_duration_ms = + fragments_[current_fragment_idx_].duration_ms_; + while (millis() - fragment_begin_ms_ >= current_fragment_duration_ms) { + current_fragment_idx_++; + if (current_fragment_idx_ >= fragments_.size()) { + current_fragment_idx_ = 0; + if (oneshot) { + return false; + } + } + fragment_begin_ms_ += current_fragment_duration_ms; + current_fragment_duration_ms = + fragments_[current_fragment_idx_].duration_ms_; + } + // Call the callback function for the current fragment, passing the elapsed + // time and a reference to the CRGB object + fragments_[current_fragment_idx_].callback_(millis() - fragment_begin_ms_, + crgb); + return true; + } protected: - float ratio_; + std::vector fragments_; + uint32_t current_fragment_idx_ = 0; + unsigned long fragment_begin_ms_ = 0; }; /** - * @brief A blinker that blinks the LED according to a defined - * repeating pattern. + * @brief Blink the LED with a pattern specified by a vector of callback + * functions. + * + * LED blinker class that blinks the LED with a pattern specified by a vector of + * callback functions. Each callback function is called in sequence, with the + * duration of each callback function specified in the LEDPatternFragment + * object. + * + * In addition to the pattern, there can be one or more transient modifiers that + * modify the LED color during the pattern. These modifiers are applied in + * sequence on top of the pattern. They can be used e.g. to change the color, + * dim the LED, or apply a brief fade effect to indicate e.g. a button press or + * received data. + * */ -class PatternBlinker : public BaseBlinker { +class LEDBlinker { public: - PatternBlinker(int pin, int pattern[]); - void tick() override final; - void set_pattern(int pattern[]); - void restart(); + LEDBlinker(CRGB& crgb, LEDPattern pattern, std::function show_func) + : crgb_(crgb), pattern_(pattern), show_func_(show_func) {} + void tick() { + // Always start with the last color + crgb_ = last_color_; + pattern_.apply(crgb_); + last_color_ = crgb_; + std::list::iterator it = modifiers_.begin(); + while (it != modifiers_.end()) { + LEDPattern* mod = &*it; + bool result = mod->apply(crgb_, true); + if (!result) { + it = modifiers_.erase(it); + } else { + it++; + } + } + show_func_(); + } + + void set_pattern(const LEDPattern& pattern) { pattern_ = pattern; } + + void add_modifier(const LEDPattern& modifier) { + modifiers_.push_back(modifier); + } protected: - int* pattern_; - unsigned int pattern_ptr_ = 0; + CRGB& crgb_; + CRGB last_color_ = CRGB::Black; // Previous set color before modifiers + LEDPattern pattern_; + std::list modifiers_ = {}; + + std::function show_func_; }; +LEDPatternFragment frag_solid_color(uint32_t duration_ms, const CRGB& color); +LEDPatternFragment frag_linear_fade(uint32_t duration_ms, + uint32_t fade_duration_ms, + const CRGB& target_color); +LEDPatternFragment frag_linear_invert(uint32_t duration_ms, + bool reverse = false); +LEDPatternFragment frag_blend(uint32_t duration_ms, const CRGB& target_color, + bool reverse = false); +LEDPatternFragment frag_nop(uint32_t duration_ms); } // namespace sensesp #endif diff --git a/src/sensesp/system/pwm_output.cpp b/src/sensesp/system/pwm_output.cpp deleted file mode 100644 index 88a5a0170..000000000 --- a/src/sensesp/system/pwm_output.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "sensesp.h" - -#include "pwm_output.h" - -#include - -namespace sensesp { - -// For info on frequency and resolution for ESP32, see -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html#ledc-api-supported-range-frequency-duty-resolution - -std::map PWMOutput::channel_to_pin_; - -PWMOutput::PWMOutput(int pin, int pwm_channel, int channel_frequency, - int channel_resolution) - : ValueConsumer(), - pwm_channel_{static_cast(pwm_channel)}, - channel_frequency_{channel_frequency}, - channel_resolution_{channel_resolution}, - pwmrange_{static_cast(pow(2, channel_resolution) - 1)} { - if (pin >= 0) { - pwm_channel_ = assign_channel(pin, pwm_channel); - } -} - -void PWMOutput::set(const float& new_value) { set_pwm(new_value); } - -int PWMOutput::assign_channel(int pin, int pwm_channel) { - if (pwm_channel == -1) { - // Do a search for the next available channel - std::map::iterator it; - pwm_channel = 0; - do { - pwm_channel++; - it = channel_to_pin_.find(pwm_channel); - } while (it != channel_to_pin_.end()); - } - - channel_to_pin_[pwm_channel] = pin; - - ESP_LOGD(__FILENAME__, "PWM channel %d assigned to pin %d", pwm_channel, pin); - - pinMode(pin, OUTPUT); - ledcSetup(pwm_channel, channel_frequency_, channel_resolution_); - ledcAttachPin(pin, pwm_channel); - - return pwm_channel; -} - -void PWMOutput::set_pwm(float value) { - std::map::iterator it; - it = channel_to_pin_.find(pwm_channel_); - if (it != channel_to_pin_.end()) { - int pin = it->second; - int const output_val = value * pwmrange_; - ESP_LOGD(__FILENAME__, "Outputting %d to pwm channel %d (pin %d)", - output_val, pwm_channel_, pin); - ledcWrite(pwm_channel_, output_val); - } else { - ESP_LOGW(__FILENAME__, "No pin assigned to channel %d. Ignoring set_pwm()", - pwm_channel_); - } -} - -} // namespace sensesp diff --git a/src/sensesp/system/pwm_output.h b/src/sensesp/system/pwm_output.h index dcc046d93..5c7ce33f5 100644 --- a/src/sensesp/system/pwm_output.h +++ b/src/sensesp/system/pwm_output.h @@ -42,21 +42,34 @@ class PWMOutput : public ValueConsumer { * next unassigned pin. */ PWMOutput(int pin = -1, int pwm_channel = -1, int channel_frequency = 5000, - int channel_resolution = 13); + int channel_resolution = 13) + : ValueConsumer(), + pwm_channel_{static_cast(pwm_channel)}, + channel_frequency_{channel_frequency}, + channel_resolution_{channel_resolution}, + pwmrange_{static_cast(pow(2, channel_resolution) - 1)} { + if (pin >= 0) { + pwm_channel_ = assign_channel(pin, pwm_channel); + } + } /** * Sets the duty cycle of the specified pwm_channel to new_value. If * pwm_channel is zero, the channel assigned when the PWMOutput instance * was instantiated will be used. */ - virtual void set(const float& new_value) override; + virtual void set(const float& new_value) override { set_pwm(new_value); } protected: int channel_frequency_; int channel_resolution_; int pwmrange_; - static std::map channel_to_pin_; + static std::map& channel_map() { + static std::map channel_to_pin_; + return channel_to_pin_; + } + uint8_t pwm_channel_{}; /** @@ -68,14 +81,49 @@ class PWMOutput : public ValueConsumer { * @returns The actual pwm channel assigned to the GPIO pin * to the pin */ - int assign_channel(int pin, int pwm_channel = -1); + int assign_channel(int pin, int pwm_channel = -1) { + if (pwm_channel == -1) { + // Do a search for the next available channel + std::map::iterator it; + pwm_channel = 0; + do { + pwm_channel++; + it = channel_map().find(pwm_channel); + } while (it != channel_map().end()); + } + + channel_map()[pwm_channel] = pin; + + ESP_LOGD(__FILENAME__, "PWM channel %d assigned to pin %d", pwm_channel, + pin); + + pinMode(pin, OUTPUT); + ledcSetup(pwm_channel, channel_frequency_, channel_resolution_); + ledcAttachPin(pin, pwm_channel); + + return pwm_channel; + } /** * Sets duty cycle on specified pwm channel * @param value A number between 0.0 and 1.0, where 1.0 is the maximum * duty cycle the output pin supports. */ - void set_pwm(float value); + void set_pwm(float value) { + std::map::iterator it; + it = channel_map().find(pwm_channel_); + if (it != channel_map().end()) { + int pin = it->second; + int const output_val = value * pwmrange_; + // ESP_LOGD(__FILENAME__, "Setting PWM channel %d to %d", pwm_channel_, + // output_val); + ledcWrite(pwm_channel_, output_val); + } else { + ESP_LOGW(__FILENAME__, + "No pin assigned to channel %d. Ignoring set_pwm()", + pwm_channel_); + } + } }; } // namespace sensesp diff --git a/src/sensesp/system/system_status_led.cpp b/src/sensesp/system/system_status_led.cpp index 297e58750..474650642 100644 --- a/src/sensesp/system/system_status_led.cpp +++ b/src/sensesp/system/system_status_led.cpp @@ -2,58 +2,94 @@ namespace sensesp { -// These patterns indicate how many milliseconds the led should be on and off, -// repeating until PATTERN_END is reached - -// FIXME: These patterns eat up memory even if the LED -// would be disabled - -// PATTERN: *___________________ -int no_ap_pattern[] = {50, 950, PATTERN_END}; +// PATTERN: **__________________ +LEDPattern no_ap_pattern = {frag_linear_fade(100, 100, CRGB::Red), + frag_linear_fade(950, 200, CRGB::Black)}; // PATTERN: ******______________ -int wifi_disconnected_pattern[] = {300, 700, PATTERN_END}; +LEDPattern wifi_disconnected_pattern = { + frag_linear_fade(300, 100, CRGB::Red), + frag_linear_fade(700, 200, CRGB::Black), +}; // PATTERN: **************______ -int wifi_connected_pattern[] = {700, 300, PATTERN_END}; +LEDPattern wifi_connected_pattern = { + frag_linear_fade(700, 200, CRGB::Yellow), + frag_linear_fade(300, 100, CRGB::Black), +}; // PATTERN: *************_*_*_*_ -int ws_connecting_pattern[] = {650, 50, 50, 50, 50, 50, 50, 50, PATTERN_END}; - -// PATTERN: ******************** -int wifimanager_pattern[] = {1000, 0, PATTERN_END}; +LEDPattern ws_connecting_pattern = { + frag_linear_fade(650, 200, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), +}; // PATTERN: ******************__ -int ws_connected_pattern[] = {900, 100, PATTERN_END}; +LEDPattern ws_connected_pattern = { + frag_linear_fade(900, 200, CRGB::Green), + frag_linear_fade(100, 100, CRGB::Black), +}; // PATTERN: *_*_*_*_____________ -int ws_disconnected_pattern[] = {50, 50, 50, 50, 50, 50, 50, 650, PATTERN_END}; +LEDPattern ws_disconnected_pattern = { + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(50, 50, CRGB::Black), + frag_linear_fade(50, 50, CRGB::Yellow), + frag_linear_fade(650, 200, CRGB::Black), +}; // PATTERN: ****____ -int ws_authorizing_pattern[] = {200, 200, PATTERN_END}; +LEDPattern ws_authorizing_pattern = { + frag_linear_fade(200, 200, CRGB::Yellow), + frag_linear_fade(200, 200, CRGB::Black), +}; -SystemStatusLed::SystemStatusLed(int pin) - : blinker_(std::unique_ptr( - new PatternBlinker(pin, no_ap_pattern))) {} +LEDPattern blip_pattern = { + frag_blend(50, CRGB::Black), + frag_blend(50, CRGB::Black, true), +}; -void SystemStatusLed::set_wifi_no_ap() { blinker_->set_pattern(no_ap_pattern); } -void SystemStatusLed::set_wifi_disconnected() { - blinker_->set_pattern(wifi_disconnected_pattern); +void BaseSystemStatusLed::set_wifi_no_ap() { + std::vector no_ap_frags; + no_ap_frags.push_back(frag_linear_fade(100, 100, CRGB::Red)); + no_ap_frags.push_back(frag_linear_fade(950, 200, CRGB::Black)); + LEDPattern no_ap_pattern(no_ap_frags); + + blinker_->set_pattern(no_ap_pattern); + ESP_LOGD("SystemStatusLed", "pattern set to no_ap"); } -void SystemStatusLed::set_wifimanager_activated() { - blinker_->set_pattern(wifimanager_pattern); + +void BaseSystemStatusLed::set_wifi_disconnected() { + blinker_->set_pattern(wifi_disconnected_pattern); } -void SystemStatusLed::set_ws_disconnected() { + +void BaseSystemStatusLed::set_ws_disconnected() { blinker_->set_pattern(ws_disconnected_pattern); } -void SystemStatusLed::set_ws_authorizing() { +void BaseSystemStatusLed::set_ws_authorizing() { blinker_->set_pattern(ws_authorizing_pattern); } -void SystemStatusLed::set_ws_connecting() { +void BaseSystemStatusLed::set_ws_connecting() { blinker_->set_pattern(ws_connecting_pattern); } -void SystemStatusLed::set_ws_connected() { +void BaseSystemStatusLed::set_ws_connected() { blinker_->set_pattern(ws_connected_pattern); } +ValueConsumer& BaseSystemStatusLed::get_delta_tx_count_consumer() { + static LambdaConsumer delta_tx_count_consumer_{ + [this](int) { blinker_->add_modifier(blip_pattern); }}; + return delta_tx_count_consumer_; +} + } // namespace sensesp diff --git a/src/sensesp/system/system_status_led.h b/src/sensesp/system/system_status_led.h index 14ae9c596..428cea265 100644 --- a/src/sensesp/system/system_status_led.h +++ b/src/sensesp/system/system_status_led.h @@ -2,31 +2,60 @@ #define SENSESP_SRC_SENSESP_SYSTEM_SYSTEM_STATUS_LED_H_ #include +#include #include "lambda_consumer.h" #include "led_blinker.h" +#include "pwm_output.h" #include "sensesp/controllers/system_status_controller.h" namespace sensesp { +// LED patterns. It isn't strictly speaking necessary to have these public, +// but it makes it easier to test the LED functionality. +extern LEDPattern no_ap_pattern; +extern LEDPattern wifi_disconnected_pattern; +extern LEDPattern wifi_connected_pattern; +extern LEDPattern ws_connecting_pattern; +extern LEDPattern ws_connected_pattern; +extern LEDPattern ws_disconnected_pattern; +extern LEDPattern ws_authorizing_pattern; + /** * @brief Consumes the networking and websocket states and delta counts - * and updates the device LED accordingly. Inherit this class and override - * the methods to customize the behavior. + * and updates the device LED accordingly. + * + * This is an abstract class that can be implemented for different types of + * LEDs. */ -class SystemStatusLed { +class BaseSystemStatusLed { protected: - std::unique_ptr blinker_; + CRGB leds_[1] = {CRGB::Black}; + + std::unique_ptr blinker_; + + virtual void show() = 0; + + virtual void set_brightness(uint8_t brightness) = 0; virtual void set_wifi_no_ap(); virtual void set_wifi_disconnected(); - virtual void set_wifimanager_activated(); - virtual void set_ws_disconnected(); virtual void set_ws_authorizing(); virtual void set_ws_connecting(); virtual void set_ws_connected(); + public: + BaseSystemStatusLed() { + blinker_ = std::unique_ptr( + new LEDBlinker(leds_[0], no_ap_pattern, [this]() { this->show(); })); + + event_loop()->onRepeat(5, [this]() { + this->blinker_->tick(); + this->show(); + }); + } + LambdaConsumer system_status_consumer_{ [this](SystemStatus status) { switch (status) { @@ -36,9 +65,6 @@ class SystemStatusLed { case SystemStatus::kWifiDisconnected: this->set_wifi_disconnected(); break; - case SystemStatus::kWifiManagerActivated: - this->set_wifimanager_activated(); - break; case SystemStatus::kSKWSDisconnected: this->set_ws_disconnected(); break; @@ -51,21 +77,69 @@ class SystemStatusLed { case SystemStatus::kSKWSConnected: this->set_ws_connected(); break; + default: + break; } }}; - LambdaConsumer delta_tx_count_consumer_{ - [this](int) { blinker_->blip(); }}; + ValueConsumer& get_system_status_consumer() { + return system_status_consumer_; + } + + ValueConsumer& get_delta_tx_count_consumer(); +}; + +// TODO: Instead of setting PWM output manually, it would be great to +// instead implement a single-LED FastLED controller class. That would +// allow supporting features like dithering etc. +/** + * @brief Monochromatic system status LED. + * + */ +class SystemStatusLed : public BaseSystemStatusLed { public: - SystemStatusLed(int pin); + SystemStatusLed(uint8_t pin, uint8_t brightness = 255) + : pwm_output_{PWMOutput(pin, -1, 2000, 8)}, + BaseSystemStatusLed(), + brightness_{brightness} {} - ValueConsumer& get_system_status_consumer() { - return system_status_consumer_; + protected: + PWMOutput pwm_output_; + uint8_t brightness_; + void show() override { + // Convert the RGB color to a single brightness value using the + // perceptual luminance formula. + float value = ((0.2126 * leds_[0].r + // Red contribution + 0.7152 * leds_[0].g + // Green contribution + 0.0722 * leds_[0].b // Blue contribution + ) * + brightness_ / (255.0 * 256.0)); + pwm_output_.set(value); } - ValueConsumer& get_delta_tx_count_consumer() { - return delta_tx_count_consumer_; + + void set_brightness(uint8_t brightness) override { brightness_ = brightness; } +}; + +// Direct use of FastLED breaks when the WiFi client is enabled. Thus, +// use the Arduino ESP32 Core native neopixelWrite function instead. That +// seems to work with WiFi as well. + +class RGBSystemStatusLed : public BaseSystemStatusLed { + public: + RGBSystemStatusLed(uint8_t pin, uint8_t brightness = 40) + : BaseSystemStatusLed(), pin_{pin} {} + + protected: + uint8_t pin_; + uint8_t brightness_; + void show() override { + neopixelWrite(pin_, brightness_ * leds_[0].r / 255, + brightness_ * leds_[0].g / 255, + brightness_ * leds_[0].b / 255); } + + void set_brightness(uint8_t brightness) override { brightness_ = brightness; } }; } // namespace sensesp diff --git a/src/sensesp_app.h b/src/sensesp_app.h index f69ed4b6e..20a19fbd4 100644 --- a/src/sensesp_app.h +++ b/src/sensesp_app.h @@ -1,14 +1,6 @@ #ifndef SENSESP_APP_H_ #define SENSESP_APP_H_ -#ifdef LED_BUILTIN -#define LED_PIN LED_BUILTIN -#define ENABLE_LED true -#else -#define LED_PIN 0 -#define ENABLE_LED false -#endif - #include "sensesp/controllers/system_status_controller.h" #include "sensesp/net/discovery.h" #include "sensesp/net/http_server.h" @@ -203,12 +195,18 @@ class SensESPApp : public SensESPBaseApp { // create a system status led and connect it if (system_status_led_ == nullptr) { - system_status_led_ = std::make_shared(LED_PIN); +#ifdef PIN_NEOPIXEL + system_status_led_ = std::make_shared(PIN_NEOPIXEL); +#elif defined(LED_BUILTIN) + system_status_led_ = std::make_shared(LED_BUILTIN); +#endif + } + if (system_status_led_ != nullptr) { + this->system_status_controller_->connect_to( + system_status_led_->system_status_consumer_); + this->ws_client_->get_delta_tx_count_producer().connect_to( + system_status_led_->get_delta_tx_count_consumer()); } - this->system_status_controller_->connect_to( - system_status_led_->get_system_status_consumer()); - this->ws_client_->get_delta_tx_count_producer().connect_to( - system_status_led_->get_delta_tx_count_consumer()); // create the button handler if (button_gpio_pin_ != -1) { @@ -243,7 +241,8 @@ class SensESPApp : public SensESPBaseApp { // Event counts uint64_t current_event_count = event_loop_->getEventCount(); uint64_t current_timed_event_count = event_loop_->getTimedEventCount(); - uint64_t current_untimed_event_count = event_loop_->getUntimedEventCount(); + uint64_t current_untimed_event_count = + event_loop_->getUntimedEventCount(); event_count_ui_output_.set(current_event_count); timed_event_count_ui_output_.set(current_timed_event_count); untimed_event_count_ui_output_.set(current_untimed_event_count); @@ -306,7 +305,7 @@ class SensESPApp : public SensESPBaseApp { std::shared_ptr mdns_discovery_; std::shared_ptr http_server_; - std::shared_ptr system_status_led_; + std::shared_ptr system_status_led_; std::shared_ptr system_status_controller_ = std::make_shared(); int button_gpio_pin_ = SENSESP_BUTTON_PIN;