From 752f36458b1c807b69ac9e68f16a0e3317b839bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Aug 2026 19:27:16 -0500 Subject: [PATCH] [uart] Gate I/O on driver installation instead of component state Track driver installation with a dedicated flag. Component state was the wrong predicate on both ends: before setup uart_num_ is not yet assigned so uart_is_driver_installed() could alias another bus, and after a runtime failure the installed driver kept working, so gating on is_ready() turned mark_failed() into a permanent bus shutdown that load_settings() could not revive. Also throttle the dropped write warning to a single line since consumers writing from loop() can hit it on every iteration of the setup phase wait loops. --- .../uart/uart_component_esp_idf.cpp | 39 ++++++++++++------- .../components/uart/uart_component_esp_idf.h | 8 ++++ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 4e88295bf4..78e28d78d3 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -126,6 +126,7 @@ void IDFUARTComponent::load_settings(bool dump_config) { esp_err_t err; if (uart_is_driver_installed(this->uart_num_)) { + this->driver_installed_ = false; err = uart_driver_delete(this->uart_num_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); @@ -146,6 +147,7 @@ void IDFUARTComponent::load_settings(bool dump_config) { this->mark_failed(); return; } + this->driver_installed_ = true; // uart_param_config must be called after uart_driver_install and before any // other uart_set_*() calls. The driver installation resets the UART peripheral @@ -301,12 +303,19 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { } void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { - if (!this->is_ready()) { - // Another component used the bus before setup() installed the driver, or - // after the bus failed. Calling the driver would fail and mark this - // component failed, which would then skip the driver installation entirely - // and permanently disable the bus, so drop the data instead. - ESP_LOGW(TAG, "write_array called while the bus is not ready; dropping %zu bytes", len); + if (!this->driver_installed_) { + // Another component used the bus before setup() installed the driver. + // Calling the driver would fail and mark this component failed, which + // would then skip the driver installation entirely and permanently + // disable the bus, so drop the data instead. Warn only once: consumers + // writing from loop() can hit this on every iteration of the setup + // phase's wait loops, which would flood the log. + if (!this->warned_not_ready_) { + this->warned_not_ready_ = true; + ESP_LOGW(TAG, "write_array called before the driver was installed; dropping %zu bytes", len); + } else { + ESP_LOGV(TAG, "write_array called before the driver was installed; dropping %zu bytes", len); + } return; } int32_t write_len = uart_write_bytes(this->uart_num_, data, len); @@ -322,7 +331,7 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { } bool IDFUARTComponent::peek_byte(uint8_t *data) { - if (!this->is_ready()) { + if (!this->driver_installed_) { return false; } if (!this->check_read_timeout_()) @@ -342,7 +351,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { } bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { - if (len == 0 || !this->is_ready()) { + if (len == 0 || !this->driver_installed_) { return false; } size_t length_to_read = len; @@ -368,12 +377,12 @@ size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; - if (!this->is_ready()) { - // The driver is not installed yet (or the bus failed); asking the driver - // would fail and mark the whole bus failed, so report no data instead. - // A stale peeked byte must not be counted either: the read paths refuse - // to deliver it while the bus is not ready, so advertising it would make - // the common `while (available()) read()` pattern spin forever. + if (!this->driver_installed_) { + // The driver is not installed yet; asking the driver would fail and mark + // the whole bus failed, so report no data instead. A stale peeked byte + // must not be counted either: the read paths refuse to deliver it while + // the driver is missing, so advertising it would make the common + // `while (available()) read()` pattern spin forever. return 0; } @@ -390,7 +399,7 @@ size_t IDFUARTComponent::available() { } UARTFlushResult IDFUARTComponent::flush() { - if (!this->is_ready()) { + if (!this->driver_installed_) { // Nothing can be pending before the driver is installed return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 649dd3aa46..eec1e92b60 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -58,6 +58,14 @@ class IDFUARTComponent final : public UARTComponent, public Component { uart_config_t get_config_(); bool has_peek_{false}; + /// True once uart_driver_install() succeeded for uart_num_. Gates all + /// driver-touching I/O: before setup uart_num_ is not even assigned, so + /// uart_is_driver_installed() cannot be used as the predicate (it could + /// alias another component's port). Deliberately not tied to the component + /// state so a bus marked failed after a successful install keeps serving + /// I/O like it always did, and load_settings() can revive it. + bool driver_installed_{false}; + bool warned_not_ready_{false}; uint8_t peek_byte_; uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).