Compare commits

...
Author SHA1 Message Date
J. Nick Koston 06668c742d Merge remote-tracking branch 'origin/dev' into uart-idf-write-before-setup-guard
# Conflicts:
#	esphome/components/uart/uart_component_esp_idf.h
2026-09-14 10:52:21 -05:00
J. Nick Koston 9a7ab80c43 [uart] Clear the driver installed flag only after a successful delete
A failed uart_driver_delete leaves the old driver installed and
working; clearing the flag beforehand would gate off a live driver.
2026-08-16 21:16:53 -05:00
J. Nick Koston 575311f0e8 [uart] Re-arm the dropped write warning and unify the readiness predicate
Reset the one-shot warning when the driver is reinstalled so a later
not-installed episode is loud again, and use the driver installed flag
in the rx threshold and timeout setters so the file carries a single
readiness predicate.
2026-08-16 20:27:20 -05:00
J. Nick Koston 11b37e1861 [uart] Order members largest to smallest to reduce padding 2026-08-16 19:30:59 -05:00
J. Nick Koston 752f36458b [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.
2026-08-16 19:27:16 -05:00
J. Nick Koston d98484c283 [uart] Report no data available while the bus is not ready
A stale peeked byte was still counted by available() while the read
paths refused to deliver it, so a caller looping on available() would
spin forever.
2026-08-16 18:39:23 -05:00
J. Nick Koston 7f9636b7f2 [uart] Guard ESP-IDF UART operations before the driver is installed
A component at the same setup priority could write to the bus before
uart_driver_install() ran; the failed write marked the UART component
failed, its setup was then skipped, and the bus never came up. Guard
the driver calls on is_ready() so early use is dropped instead.
2026-08-16 17:32:55 -05:00
2 changed files with 57 additions and 9 deletions
@@ -132,6 +132,9 @@ void IDFUARTComponent::load_settings(bool dump_config) {
this->mark_failed();
return;
}
// Only mark the driver gone once the delete actually succeeded; a failed
// delete leaves the old driver installed and working
this->driver_installed_ = false;
}
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
@@ -146,6 +149,10 @@ void IDFUARTComponent::load_settings(bool dump_config) {
this->mark_failed();
return;
}
this->driver_installed_ = true;
// Re-arm the dropped-write warning so a later not-installed episode
// (a failed reinstall through load_settings) is loud again
this->warned_not_ready_ = false;
// uart_param_config must be called after uart_driver_install and before any
// other uart_set_*() calls. The driver installation resets the UART peripheral
@@ -282,7 +289,7 @@ esp_err_t IDFUARTComponent::apply_settings_live() {
return ESP_ERR_INVALID_STATE;
}
// No driver yet: nothing to reconfigure in place.
if (!uart_is_driver_installed(this->uart_num_)) {
if (!this->driver_installed_) {
this->load_settings(false);
return this->is_failed() ? ESP_FAIL : ESP_OK;
}
@@ -346,7 +353,7 @@ void IDFUARTComponent::dump_config() {
}
void IDFUARTComponent::set_rx_full_threshold(size_t rx_full_threshold) {
if (this->is_ready()) {
if (this->driver_installed_) {
esp_err_t err = uart_set_rx_full_threshold(this->uart_num_, rx_full_threshold);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
@@ -357,7 +364,7 @@ void IDFUARTComponent::set_rx_full_threshold(size_t rx_full_threshold) {
}
void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
if (this->is_ready()) {
if (this->driver_installed_) {
esp_err_t err = uart_set_rx_timeout(this->uart_num_, rx_timeout);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
@@ -368,6 +375,21 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
}
void IDFUARTComponent::write_array(const uint8_t *data, size_t 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);
if (write_len != (int32_t) len) {
ESP_LOGW(TAG, "uart_write_bytes failed: %" PRId32 " != %zu", write_len, len);
@@ -381,6 +403,9 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
}
bool IDFUARTComponent::peek_byte(uint8_t *data) {
if (!this->driver_installed_) {
return false;
}
if (!this->check_read_timeout_())
return false;
if (this->has_peek_) {
@@ -398,7 +423,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
}
bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
if (len == 0) {
if (len == 0 || !this->driver_installed_) {
return false;
}
size_t length_to_read = len;
@@ -424,6 +449,15 @@ size_t IDFUARTComponent::available() {
size_t available = 0;
esp_err_t err;
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;
}
err = uart_get_buffered_data_len(this->uart_num_, &available);
if (err != ESP_OK) {
@@ -437,6 +471,10 @@ size_t IDFUARTComponent::available() {
}
UARTFlushResult IDFUARTComponent::flush() {
if (!this->driver_installed_) {
// Nothing can be pending before the driver is installed
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
ESP_LOGVV(TAG, " Flushing");
TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_);
esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks);
@@ -468,7 +506,9 @@ void IDFUARTComponent::on_shutdown() {
esp_err_t err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
return;
}
this->driver_installed_ = false;
}
} // namespace esphome::uart
@@ -75,11 +75,10 @@ class IDFUARTComponent final : public UARTComponent, public Component {
protected:
void check_logger_conflict() override;
uart_config_t get_config_();
uint32_t line_inversion_mask_();
// Re-applies what uart_param_config() resets: inversion, RX threshold/timeout, mode.
esp_err_t apply_line_settings_();
uart_port_t uart_num_{UART_NUM_MAX};
uart_config_t get_config_();
struct Framing {
uint32_t baud_rate;
@@ -92,12 +91,21 @@ class IDFUARTComponent final : public UARTComponent, public Component {
return {this->baud_rate_, this->data_bits_, this->stop_bits_, this->parity_, this->rx_full_threshold_};
}
void set_framing_(const Framing &framing);
// Members ordered largest to smallest to minimize padding
// Last framing the driver accepted; baud_rate 0 means none yet.
Framing last_good_framing_{};
bool has_peek_{false};
uint8_t peek_byte_;
uart_port_t uart_num_{UART_NUM_MAX};
uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).
uint8_t peek_byte_;
bool has_peek_{false};
/// True while the driver this component installed for uart_num_ is in place. Gates all
/// driver-touching I/O: uart_is_driver_installed() cannot be used as the predicate since
/// it also reports a driver another component installed on the same 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};
#ifdef USE_UART_WAKE_LOOP_ON_RX
// ISR callback for UART RX data notification — wakes the main loop directly.