From fdbfac15db79a329629173c1d6ba967f12b43e46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:21:08 -1000 Subject: [PATCH 1/7] [uart] Replace wake-on-RX task+queue with direct ISR callback (#14382) --- .../uart/uart_component_esp_idf.cpp | 101 +++--------------- .../components/uart/uart_component_esp_idf.h | 21 ++-- esphome/core/application.h | 10 ++ esphome/core/lwip_fast_select.c | 14 +++ esphome/core/lwip_fast_select.h | 5 + 5 files changed, 52 insertions(+), 99 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8699d37d7a..631db1e5c7 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -2,7 +2,6 @@ #include "uart_component_esp_idf.h" #include -#include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -10,6 +9,10 @@ #include "driver/gpio.h" #include "soc/gpio_num.h" +#ifdef USE_UART_WAKE_LOOP_ON_RX +#include "esphome/core/application.h" +#endif + #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -107,12 +110,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { esp_err_t err; if (uart_is_driver_installed(this->uart_num_)) { -#ifdef USE_UART_WAKE_LOOP_ON_RX - if (this->rx_event_task_handle_ != nullptr) { - vTaskDelete(this->rx_event_task_handle_); - this->rx_event_task_handle_ = nullptr; - } -#endif err = uart_driver_delete(this->uart_num_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); @@ -120,20 +117,13 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } } -#ifdef USE_UART_WAKE_LOOP_ON_RX - constexpr int event_queue_size = 20; - QueueHandle_t *event_queue_ptr = &this->uart_event_queue_; -#else - constexpr int event_queue_size = 0; - QueueHandle_t *event_queue_ptr = nullptr; -#endif err = uart_driver_install(this->uart_num_, // UART number this->rx_buffer_size_, // RX ring buffer size 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will // block task until all data has been sent out - event_queue_size, // event queue size/depth - event_queue_ptr, // event queue - 0 // Flags used to allocate the interrupt + 0, // event queue size/depth + nullptr, // event queue + 0 // Flags used to allocate the interrupt ); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); @@ -213,8 +203,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { } #ifdef USE_UART_WAKE_LOOP_ON_RX - // Start the RX event task to enable low-latency data notifications - this->start_rx_event_task_(); + // Register ISR callback to wake the main loop when UART data arrives. + // The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to + // wake the main loop task directly — no queue or FreeRTOS task needed. + uart_set_select_notif_callback(this->uart_num_, IDFUARTComponent::uart_rx_isr_callback); #endif // USE_UART_WAKE_LOOP_ON_RX if (dump_config) { @@ -345,71 +337,12 @@ void IDFUARTComponent::flush() { void IDFUARTComponent::check_logger_conflict() {} #ifdef USE_UART_WAKE_LOOP_ON_RX -void IDFUARTComponent::start_rx_event_task_() { - // Create FreeRTOS task to monitor UART events - BaseType_t result = xTaskCreate(rx_event_task_func, // Task function - "uart_rx_evt", // Task name (max 16 chars) - 2240, // Stack size in bytes (~2.2KB); increase if needed for logging - this, // Task parameter (this pointer) - tskIDLE_PRIORITY + 1, // Priority (low, just above idle) - &this->rx_event_task_handle_ // Task handle - ); - - if (result != pdPASS) { - ESP_LOGE(TAG, "Failed to create RX event task"); - return; - } - - ESP_LOGV(TAG, "RX event task started"); -} - -// FreeRTOS task that relays UART ISR events to the main loop. -// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a -// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline. -// IMPORTANT: This task must NOT call any UART wrapper methods (read_array, -// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading -// is done by the main loop. This task only reads from the event queue and -// calls App.wake_loop_threadsafe(). -void IDFUARTComponent::rx_event_task_func(void *param) { - auto *self = static_cast(param); - uart_event_t event; - - ESP_LOGV(TAG, "RX event task running"); - - // Run forever - task lifecycle matches component lifecycle - while (true) { - // Wait for UART events (blocks efficiently) - if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) { - switch (event.type) { - case UART_DATA: - // Data available in UART RX buffer - wake the main loop - ESP_LOGVV(TAG, "Data event: %d bytes", event.size); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); -#endif - break; - - case UART_FIFO_OVF: - case UART_BUFFER_FULL: - // Don't call uart_flush_input() here — this task does not own the read side. - // ESP-IDF examples flush on overflow because the same task handles both events - // and reads, so flush and read are serialized. Here, reads happen on the main - // loop, so flushing from this task races with read_array() and can destroy data - // mid-read. The driver self-heals without an explicit flush: uart_read_bytes() - // calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes - // into the ring buffer and re-enables RX interrupts once space is freed. - ESP_LOGW(TAG, "FIFO overflow or ring buffer full"); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); -#endif - break; - - default: - // Ignore other event types - ESP_LOGVV(TAG, "Event type: %d", event.type); - break; - } - } +// ISR callback invoked by the ESP-IDF UART driver when data arrives. +// Wakes the main loop directly via vTaskNotifyGiveFromISR() — no queue or task needed. +void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, + BaseType_t *task_woken) { + if (uart_select_notif == UART_SELECT_READ_NOTIF) { + Application::wake_loop_isrsafe(task_woken); } } #endif // USE_UART_WAKE_LOOP_ON_RX diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1517eab509..631bf54cd5 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -5,6 +5,9 @@ #include #include "esphome/core/component.h" #include "uart_component.h" +#ifdef USE_UART_WAKE_LOOP_ON_RX +#include +#endif namespace esphome::uart { @@ -12,9 +15,7 @@ namespace esphome::uart { /// /// Thread safety: All public methods must only be called from the main loop. /// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's -/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task -/// (when enabled) must not call any of these methods — it communicates with the -/// main loop exclusively via App.wake_loop_threadsafe(). +/// peek byte state (has_peek_/peek_byte_) is not synchronized. class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -33,9 +34,6 @@ class IDFUARTComponent : public UARTComponent, public Component { void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } -#ifdef USE_UART_WAKE_LOOP_ON_RX - QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } -#endif /** * Load the UART with the current settings. @@ -61,15 +59,8 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // RX notification support — runs on a separate FreeRTOS task. - // IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array, - // write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the - // event queue and call App.wake_loop_threadsafe(). - void start_rx_event_task_(); - static void rx_event_task_func(void *param); - - QueueHandle_t uart_event_queue_; - TaskHandle_t rx_event_task_handle_{nullptr}; + // ISR callback for UART RX data notification — wakes the main loop directly. + static void uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, BaseType_t *task_woken); #endif // USE_UART_WAKE_LOOP_ON_RX }; diff --git a/esphome/core/application.h b/esphome/core/application.h index ba3ce13291..13e0f63885 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -500,6 +500,16 @@ class Application { /// On other platforms: uses UDP loopback socket void wake_loop_threadsafe(); #endif + +#if defined(USE_WAKE_LOOP_THREADSAFE) && defined(USE_LWIP_FAST_SELECT) + /// Wake the main event loop from an ISR. + /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. + /// Only available on platforms with fast select (ESP32, LibreTiny). + /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. + static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { + esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); + } +#endif #endif protected: diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 88cf23b67e..da0f1f337a 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -126,6 +126,12 @@ #include +// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. +// On LibreTiny it's not defined — provide a no-op fallback. +#ifndef IRAM_ATTR +#define IRAM_ATTR +#endif + // Compile-time verification of thread safety assumptions. // On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned // reads/writes up to 32 bits are atomic. @@ -225,4 +231,12 @@ void esphome_lwip_wake_main_loop(void) { } } +// Wake the main loop from an ISR. ISR-safe variant. +void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); + } +} + #endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index b08c946212..b7a70a8f9f 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -28,6 +28,11 @@ void esphome_lwip_hook_socket(int fd); /// NOT ISR-safe — must only be called from task context. void esphome_lwip_wake_main_loop(void); +/// Wake the main loop task from an ISR — costs <1 us. +/// ISR-safe variant using vTaskNotifyGiveFromISR(). +/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. +void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); + #ifdef __cplusplus } #endif From b7cb65ec4996ba6c1b768908555f53586eb37d4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:23:20 -1000 Subject: [PATCH 2/7] [ci] Fix TypeError in ci-custom.py when POST lint checks fail (#14378) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 85a446ba0d..f428eb0821 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -959,7 +959,7 @@ def main(): continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) - run_checks(LINT_POST_CHECKS, "POST") + run_checks(LINT_POST_CHECKS, Path("POST")) for f, errs in sorted(errors.items()): bold = functools.partial(styled, colorama.Style.BRIGHT) From c0781d3680f6c816510f7417eeb4523964e08575 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:08 -1000 Subject: [PATCH 3/7] [ld2450] Use atan2f for angle calculation (#14388) --- esphome/components/ld2450/ld2450.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index d30c164769..5ec3ce47d7 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -168,15 +168,6 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { return dec_val; } -static inline float calculate_angle(float base, float hypotenuse) { - if (base < 0.0f || hypotenuse <= 0.0f) { - return 0.0f; - } - float angle_radians = acosf(base / hypotenuse); - float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v); - return angle_degrees; -} - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -510,11 +501,8 @@ void LD2450Component::handle_periodic_data_() { } #ifdef USE_SENSOR SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); - // ANGLE - angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); - if (tx > 0) { - angle = angle * -1; - } + // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed + angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); #endif #ifdef USE_TEXT_SENSOR From 80e0761bf1c1085296aa785405e57e2c16cc6e38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:31 -1000 Subject: [PATCH 4/7] [ld2450] Use integer dedup for direction text sensor updates (#14386) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- esphome/components/ld2450/ld2450.h | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 5ec3ce47d7..ca64836bf8 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -516,10 +516,11 @@ void LD2450Component::handle_periodic_data_() { } else { direction = DIRECTION_STATIONARY; } - text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; - const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction); - if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) { - tsd->publish_state(dir_str); + if (this->direction_dedup_[index].next(direction)) { + text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; + if (tsd != nullptr) { + tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); + } } #endif diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 30f96c0a9c..518b320b0a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -194,7 +194,8 @@ class LD2450Component : public Component, public uart::UARTDevice { std::array *, MAX_ZONES> zone_moving_target_count_sensors_{}; #endif #ifdef USE_TEXT_SENSOR - std::array direction_text_sensors_{}; + std::array direction_text_sensors_{}; + std::array, MAX_TARGETS> direction_dedup_{}; #endif LazyCallbackManager data_callback_; From 82e620bcf5239bf1ad540986a54c50b2e3689326 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:55 -1000 Subject: [PATCH 5/7] [ld2450] Single-pass zone target counting (#14387) --- esphome/components/ld2450/ld2450.cpp | 22 ++++++++++++---------- esphome/components/ld2450/ld2450.h | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index ca64836bf8..583918e5f5 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -283,16 +283,19 @@ void LD2450Component::loop() { } } -// Count targets in zone -uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) { - uint8_t count = 0; - for (auto &index : this->target_info_) { - if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 && - index.is_moving == is_moving) { - count++; +// Count targets in zone (single pass for both still and moving) +void LD2450Component::count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving) { + still = 0; + moving = 0; + for (auto &target : this->target_info_) { + if (target.x > zone.x1 && target.x < zone.x2 && target.y > zone.y1 && target.y < zone.y2) { + if (target.is_moving) { + moving++; + } else { + still++; + } } } - return count; } // Service reset_radar_zone @@ -540,8 +543,7 @@ void LD2450Component::handle_periodic_data_() { uint8_t zone_moving_targets = 0; uint8_t zone_all_targets = 0; for (index = 0; index < MAX_ZONES; index++) { - zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false); - zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true); + this->count_targets_in_zone_(this->zone_config_[index], zone_still_targets, zone_moving_targets); zone_all_targets = zone_still_targets + zone_moving_targets; // Publish Still Target Count in Zones diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 518b320b0a..39b0ebd9da 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -163,7 +163,7 @@ class LD2450Component : public Component, public uart::UARTDevice { void save_to_flash_(float value); float restore_from_flash_(); bool get_timeout_status_(uint32_t check_millis); - uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); + void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving); uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; From 3f97b3b7066c52bb7df8a9e5e686d290d7e88e23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:27:06 -1000 Subject: [PATCH 6/7] [core] Extract set_status_flag_ helper to deduplicate status_set methods (#14384) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/component.cpp | 25 +++++++++++-------------- esphome/core/component.h | 6 ++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 1fd621ea83..fd4e0d2984 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -383,31 +383,30 @@ bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STA bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } +bool Component::set_status_flag_(uint8_t flag) { + if ((this->component_state_ & flag) != 0) + return false; + this->component_state_ |= flag; + App.app_state_ |= flag; + return true; +} void Component::status_set_warning(const char *message) { - // Don't spam the log. This risks missing different warning messages though. - if ((this->component_state_ & STATUS_LED_WARNING) != 0) + if (!this->set_status_flag_(STATUS_LED_WARNING)) return; - this->component_state_ |= STATUS_LED_WARNING; - App.app_state_ |= STATUS_LED_WARNING; ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); } void Component::status_set_warning(const LogString *message) { - // Don't spam the log. This risks missing different warning messages though. - if ((this->component_state_ & STATUS_LED_WARNING) != 0) + if (!this->set_status_flag_(STATUS_LED_WARNING)) return; - this->component_state_ |= STATUS_LED_WARNING; - App.app_state_ |= STATUS_LED_WARNING; ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { - if ((this->component_state_ & STATUS_LED_ERROR) != 0) + if (!this->set_status_flag_(STATUS_LED_ERROR)) return; - this->component_state_ |= STATUS_LED_ERROR; - App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { @@ -415,10 +414,8 @@ void Component::status_set_error(const char *message) { } } void Component::status_set_error(const LogString *message) { - if ((this->component_state_ & STATUS_LED_ERROR) != 0) + if (!this->set_status_flag_(STATUS_LED_ERROR)) return; - this->component_state_ |= STATUS_LED_ERROR; - App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { diff --git a/esphome/core/component.h b/esphome/core/component.h index 0b0b6345c7..6b920da290 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -299,6 +299,12 @@ class Component { this->component_state_ |= state; } + /// Helper to set a status LED flag on both this component and the app. + /// Returns true if the flag was newly set, false if it was already set. + /// Note: Callers often use the return value to decide whether to log a warning/error, + /// so once a flag is set, subsequent (potentially different) messages may be suppressed. + bool set_status_flag_(uint8_t flag); + /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). From 19bbd39e334332d5ebc3f33a2a435e96ab54555f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 17:06:46 -1000 Subject: [PATCH 7/7] [uart] Enable wake-on-RX by default on ESP32 (#14391) --- esphome/components/uart/__init__.py | 42 +++++----------------- esphome/components/zwave_proxy/__init__.py | 3 -- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a6f4cf5d5f..69db4b44aa 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from logging import getLogger import math import re @@ -117,38 +116,6 @@ MULTI_CONF = True MULTI_CONF_NO_DEFAULT = True -@dataclass -class UARTData: - """State data for UART component configuration generation.""" - - wake_loop_on_rx: bool = False - - -def _get_data() -> UARTData: - """Get UART component data from CORE.data.""" - if DOMAIN not in CORE.data: - CORE.data[DOMAIN] = UARTData() - return CORE.data[DOMAIN] - - -def request_wake_loop_on_rx() -> None: - """Request that the UART wake the main loop when data is received. - - Components that need low-latency notification of incoming UART data - should call this function during their code generation. - This enables the RX event task which wakes the main loop when data arrives. - """ - data = _get_data() - if not data.wake_loop_on_rx: - data.wake_loop_on_rx = True - - # UART RX event task uses wake_loop_threadsafe() to notify the main loop - # Automatically enable the socket wake infrastructure when RX wake is requested - from esphome.components import socket - - socket.require_wake_loop_threadsafe() - - def validate_raw_data(value): if isinstance(value, str): return value.encode("utf-8") @@ -542,7 +509,14 @@ async def uart_write_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): """Final code generation step to configure optional UART features.""" - if _get_data().wake_loop_on_rx: + if CORE.is_esp32 and CORE.has_networking: + # Wake-on-RX is essentially free on ESP32 (just an ISR function pointer + # registration) — enable by default to reduce RX buffer overflow risk + # by waking the main loop immediately when data arrives. + # Requires networking for the wake_loop_isrsafe() infrastructure. + from esphome.components import socket + + socket.require_wake_loop_threadsafe() cg.add_define("USE_UART_WAKE_LOOP_ON_RX") diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py index 5be05bb464..d88f9f7041 100644 --- a/esphome/components/zwave_proxy/__init__.py +++ b/esphome/components/zwave_proxy/__init__.py @@ -41,6 +41,3 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) cg.add_define("USE_ZWAVE_PROXY") - - # Request UART to wake the main loop when data arrives for low-latency processing - uart.request_wake_loop_on_rx()