From 59d0f9dcbd78cf62e828b577175eb54a00f272f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2026 23:44:35 -1000 Subject: [PATCH] [status_led] Disable loop when idle and switch blink timing to deadline-based --- esphome/components/status_led/status_led.cpp | 33 +++++++++++--- esphome/components/status_led/status_led.h | 2 + esphome/core/application.cpp | 45 +++++++++----------- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index a792110eeb..87035f6284 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -7,6 +7,11 @@ namespace status_led { static const char *const TAG = "status_led"; +static constexpr uint32_t ERROR_ON_MS = 150; +static constexpr uint32_t ERROR_OFF_MS = 100; +static constexpr uint32_t WARNING_ON_MS = 250; +static constexpr uint32_t WARNING_OFF_MS = 1250; + StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; } @@ -19,13 +24,29 @@ void StatusLED::dump_config() { LOG_PIN(" Pin: ", this->pin_); } void StatusLED::loop() { - if ((App.get_app_state() & STATUS_LED_ERROR) != 0u) { - this->pin_->digital_write(millis() % 250u < 150u); - } else if ((App.get_app_state() & STATUS_LED_WARNING) != 0u) { - this->pin_->digital_write(millis() % 1500u < 250u); - } else { - this->pin_->digital_write(false); + const uint32_t app_state = App.get_app_state(); + const bool error = (app_state & STATUS_LED_ERROR) != 0u; + const bool warning = (app_state & STATUS_LED_WARNING) != 0u; + if (!error && !warning) { + if (this->led_on_) { + this->led_on_ = false; + this->pin_->digital_write(false); + } + this->disable_loop(); + return; } + const uint32_t now = App.get_loop_component_start_time(); + if ((int32_t) (now - this->next_toggle_) < 0) + return; + this->led_on_ = !this->led_on_; + uint32_t delay; + if (error) { + delay = this->led_on_ ? ERROR_ON_MS : ERROR_OFF_MS; + } else { + delay = this->led_on_ ? WARNING_ON_MS : WARNING_OFF_MS; + } + this->next_toggle_ = now + delay; + this->pin_->digital_write(this->led_on_); } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index a4b5db93d7..16b6dcf736 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -17,6 +17,8 @@ class StatusLED : public Component { protected: GPIOPin *pin_; + uint32_t next_toggle_{0}; + bool led_on_{false}; }; extern StatusLED *global_status_led; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index cd75859880..d83b6bf2be 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -206,7 +206,14 @@ void HOT Application::feed_wdt(uint32_t time) { last_feed = now; #ifdef USE_STATUS_LED if (status_led::global_status_led != nullptr) { - status_led::global_status_led->call(); + auto *sl = status_led::global_status_led; + // If an error or warning bit is set while status_led's loop is disabled, re-enable it + // so it starts blinking again. disable_loop() is called from loop() itself when idle. + if ((sl->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE && + (this->app_state_ & STATUS_LED_MASK) != 0) { + sl->enable_loop(); + } + sl->call(); } #endif } @@ -449,32 +456,12 @@ void Application::enable_pending_loops_() { } } -#ifdef USE_LWIP_FAST_SELECT -bool Application::register_socket(struct lwip_sock *sock) { - // It modifies monitored_sockets_ without locking — must only be called from the main loop. - if (sock == nullptr) - return false; - esphome_lwip_hook_socket(sock); - this->monitored_sockets_.push_back(sock); - return true; -} +#if defined(USE_OTA) && defined(USE_LWIP_FAST_SELECT) +// C trampoline called from lwip_fast_select.c when the listener filter matches. +extern "C" void esphome_wake_ota_component_any_context() { App.wake_ota_component_any_context(); } +#endif -void Application::unregister_socket(struct lwip_sock *sock) { - // It modifies monitored_sockets_ without locking — must only be called from the main loop. - for (size_t i = 0; i < this->monitored_sockets_.size(); i++) { - if (this->monitored_sockets_[i] != sock) - continue; - - // Swap with last element and pop - O(1) removal since order doesn't matter. - // No need to unhook the netconn callback — all LwIP sockets share the same - // static event_callback, and the socket will be closed by the caller. - if (i < this->monitored_sockets_.size() - 1) - this->monitored_sockets_[i] = this->monitored_sockets_.back(); - this->monitored_sockets_.pop_back(); - return; - } -} -#elif defined(USE_HOST) +#ifdef USE_HOST bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking @@ -554,6 +541,12 @@ void Application::yield_with_select_(uint32_t delay_ms) { // ret > 0: socket(s) have data ready - normal and expected // ret == 0: timeout occurred - normal and expected if (ret >= 0) [[likely]] { +#ifdef USE_OTA + // No-op today — host has no esphome OTA platform, so ota_wake_component_ is null. + if (ret > 0) { + this->wake_ota_component_any_context(); + } +#endif // Yield if zero timeout since select(0) only polls without yielding if (delay_ms == 0) [[unlikely]] { yield();