[status_led] Disable loop when idle and switch blink timing to deadline-based

This commit is contained in:
J. Nick Koston
2026-04-10 23:44:35 -10:00
parent c2af4874f9
commit 59d0f9dcbd
3 changed files with 48 additions and 32 deletions
+27 -6
View File
@@ -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; }
@@ -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)
+19 -26
View File
@@ -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();