mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 02:26:01 +00:00
[core] decouple status_led dispatch from WDT feed rate limit
Raising WDT_FEED_INTERVAL_MS to 300 ms in the previous commit also capped the status_led update cadence to ~3 Hz, because status_led::loop() was re-dispatched only from inside feed_wdt_slow_(). That distorts the error blink pattern (ERROR_PERIOD_MS = 250 ms with 150 ms on-window in status_led.cpp) — at a 300 ms dispatch interval, the LED can be sampled entirely inside or entirely outside the on-window on any given period, turning a readable blink into an aliased one. Split the two rate limits so they evolve independently: WDT_FEED_INTERVAL_MS = 300 ms — arch_feed_wdt() rate limit STATUS_LED_DISPATCH_INTERVAL_MS = 100 ms — status_led loop() dispatch The feed_wdt_with_time() hot path now has two independent gate checks (a load + sub + branch each). Fast path on both misses is the common case and remains cheap. feed_wdt_slow_() no longer touches status_led; the status_led re-dispatch moves into a new service_status_led_slow_() that's compiled in only when USE_STATUS_LED is set. No change to behavior on devices without status_led. Devices with status_led get the intended LED cadence restored (100 ms dispatch sits below the 150 ms error on-window and well below the 250 ms warning on- window). Flagged by Copilot review on application.h:242.
This commit is contained in:
@@ -214,11 +214,16 @@ void Application::process_dump_config_() {
|
||||
|
||||
void Application::feed_wdt() {
|
||||
// Cold entry: callers without a millis() timestamp in hand. Fetches the
|
||||
// time and takes the same rate-limit path as feed_wdt_with_time().
|
||||
// time and takes the same rate-limit paths as feed_wdt_with_time().
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
|
||||
this->feed_wdt_slow_(now);
|
||||
}
|
||||
#ifdef USE_STATUS_LED
|
||||
if (now - this->last_status_led_service_ > STATUS_LED_DISPATCH_INTERVAL_MS) {
|
||||
this->service_status_led_slow_(now);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
@@ -226,27 +231,36 @@ void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
|
||||
arch_feed_wdt();
|
||||
this->last_wdt_feed_ = time;
|
||||
#ifdef USE_STATUS_LED
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
auto *sl = status_led::global_status_led;
|
||||
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
|
||||
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
|
||||
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
|
||||
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
|
||||
// its setup() has already run. Re-dispatch only if an error or warning bit has been
|
||||
// set since; otherwise skip entirely.
|
||||
if ((this->app_state_ & STATUS_LED_MASK) == 0)
|
||||
return;
|
||||
sl->enable_loop();
|
||||
} else if (sl_state != COMPONENT_STATE_LOOP) {
|
||||
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
|
||||
return;
|
||||
}
|
||||
sl->loop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
void HOT Application::service_status_led_slow_(uint32_t time) {
|
||||
// Callers (feed_wdt(), feed_wdt_with_time()) have already confirmed the
|
||||
// STATUS_LED_DISPATCH_INTERVAL_MS rate limit was exceeded. Rate-limited
|
||||
// separately from arch_feed_wdt() so the LED blink pattern stays readable
|
||||
// (status_led error blink period is 250 ms) while HAL watchdog pokes can
|
||||
// still run at the much coarser WDT_FEED_INTERVAL_MS cadence.
|
||||
this->last_status_led_service_ = time;
|
||||
if (status_led::global_status_led == nullptr)
|
||||
return;
|
||||
auto *sl = status_led::global_status_led;
|
||||
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
|
||||
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
|
||||
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
|
||||
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
|
||||
// its setup() has already run. Re-dispatch only if an error or warning bit has been
|
||||
// set since; otherwise skip entirely.
|
||||
if ((this->app_state_ & STATUS_LED_MASK) == 0)
|
||||
return;
|
||||
sl->enable_loop();
|
||||
} else if (sl_state != COMPONENT_STATE_LOOP) {
|
||||
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
|
||||
return;
|
||||
}
|
||||
sl->loop();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Application::any_component_has_status_flag_(uint8_t flag) const {
|
||||
// Walk all components (not just looping ones) so non-looping components'
|
||||
// status bits are respected. Only called from the slow-path clear helpers
|
||||
|
||||
@@ -237,7 +237,8 @@ class Application {
|
||||
/// this threshold triggers a real feed naturally.
|
||||
/// Safety margins vs. platform watchdog timeouts:
|
||||
/// - ESP32 task WDT default (5 s): ~16x
|
||||
/// - ESP8266 soft WDT (~1.6 s): ~5x
|
||||
/// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change
|
||||
/// must keep comfortable margin here
|
||||
/// - ESP8266 HW WDT (~6 s): ~20x
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300;
|
||||
|
||||
@@ -245,14 +246,33 @@ class Application {
|
||||
/// timestamp in hand. Out of line to keep call sites tiny.
|
||||
void feed_wdt();
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
/// Dispatch interval for the status LED update. Deliberately shorter than
|
||||
/// WDT_FEED_INTERVAL_MS because the status LED error blink has a 250 ms
|
||||
/// period (status_led.cpp:ERROR_PERIOD_MS) and a 150 ms on-window; the
|
||||
/// dispatch cadence must be short enough to render that blink without
|
||||
/// aliasing. Sampling every 100 ms yields an on/off observation inside
|
||||
/// every error period with headroom for the 250 ms warning on-window.
|
||||
static constexpr uint32_t STATUS_LED_DISPATCH_INTERVAL_MS = 100;
|
||||
#endif
|
||||
|
||||
/// Feed the task watchdog, hot entry. Callers that already have a
|
||||
/// millis() timestamp pay only a load + sub + branch on the common
|
||||
/// (no-op) path. The actual arch feed + status LED update live in
|
||||
/// feed_wdt_slow_.
|
||||
/// (no-op) path. The actual arch feed lives in feed_wdt_slow_.
|
||||
/// When USE_STATUS_LED is compiled in, also gates a separate (shorter)
|
||||
/// interval for dispatching status_led so the LED blink pattern stays
|
||||
/// readable even though arch_feed_wdt pokes are now rate-limited at
|
||||
/// 300 ms. The two rate limits are independent so raising
|
||||
/// WDT_FEED_INTERVAL_MS does not distort the LED cadence.
|
||||
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
|
||||
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
|
||||
this->feed_wdt_slow_(time);
|
||||
}
|
||||
#ifdef USE_STATUS_LED
|
||||
if (static_cast<uint32_t>(time - this->last_status_led_service_) > STATUS_LED_DISPATCH_INTERVAL_MS) [[unlikely]] {
|
||||
this->service_status_led_slow_(time);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void reboot();
|
||||
@@ -418,11 +438,21 @@ class Application {
|
||||
/// Caller must ensure dump_config_at_ < components_.size().
|
||||
void __attribute__((noinline)) process_dump_config_();
|
||||
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates
|
||||
/// last_wdt_feed_, and re-dispatches the status LED. Out of line so the
|
||||
/// inline wrapper stays tiny.
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt() and updates
|
||||
/// last_wdt_feed_. Out of line so the inline wrapper stays tiny. Does NOT
|
||||
/// touch status_led — that's gated separately via service_status_led_slow_
|
||||
/// because the two rate limits have very different safe ranges (~ seconds
|
||||
/// for WDT, < 250 ms for LED blink rendering).
|
||||
void feed_wdt_slow_(uint32_t time);
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
/// Slow path for the status_led dispatch rate limit. Runs the status_led
|
||||
/// component's loop() based on its state (LOOP / LOOP_DONE with status
|
||||
/// bits set), and updates last_status_led_service_. Out of line to keep
|
||||
/// the feed_wdt_with_time hot path a couple of load+branch sequences.
|
||||
void service_status_led_slow_(uint32_t time);
|
||||
#endif
|
||||
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
#ifdef USE_HOST
|
||||
// select() fallback path is too complex to inline (host platform)
|
||||
@@ -476,6 +506,10 @@ class Application {
|
||||
uint32_t last_loop_{0};
|
||||
uint32_t loop_component_start_time_{0};
|
||||
uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path
|
||||
#ifdef USE_STATUS_LED
|
||||
// millis() of most recent status_led dispatch; rate-limits independently of last_wdt_feed_
|
||||
uint32_t last_status_led_service_{0};
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
@@ -620,7 +654,9 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
uint32_t loop_before_end_us = micros();
|
||||
uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap;
|
||||
// Default tail_start to end-of-before so tail_us == 0 on Phase A-only ticks.
|
||||
// Default tail_start to end-of-before so tail_us on Phase A-only ticks
|
||||
// captures only the small gate-check + record_loop_active prefix between
|
||||
// here and the loop_now_us sample below (not strictly zero, but tiny).
|
||||
uint32_t loop_tail_start_us = loop_before_end_us;
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user