[uart] Add apply_settings_live() for in-place ESP-IDF reconfiguration (#19087)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Keith Burzinski
2026-09-11 03:25:52 -05:00
committed by GitHub
co-authored by Claude Fable 5.1
parent f66ef23256
commit 380938177c
2 changed files with 134 additions and 31 deletions
@@ -160,6 +160,7 @@ void IDFUARTComponent::load_settings(bool dump_config) {
this->mark_failed();
return;
}
this->last_good_framing_ = this->framing_();
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
@@ -189,18 +190,9 @@ void IDFUARTComponent::load_settings(bool dump_config) {
setup_pin_if_needed(this->tx_pin_);
}
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;
}
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
invert |= UART_SIGNAL_RXD_INV;
}
if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) {
invert |= UART_SIGNAL_RTS_INV;
}
err = uart_set_line_inverse(this->uart_num_, invert);
// Must precede uart_set_pin() so an inverted TX line never shows the wrong idle
// level; apply_line_settings_() repeats it later for the reset registers.
err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_());
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
this->mark_failed();
@@ -214,25 +206,7 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
if (this->apply_line_settings_() != ESP_OK) {
this->mark_failed();
return;
}
@@ -250,6 +224,99 @@ void IDFUARTComponent::load_settings(bool dump_config) {
}
}
uint32_t IDFUARTComponent::line_inversion_mask_() {
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;
}
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
invert |= UART_SIGNAL_RXD_INV;
}
if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) {
invert |= UART_SIGNAL_RTS_INV;
}
return invert;
}
esp_err_t IDFUARTComponent::apply_line_settings_() {
// uart_param_config() resets these; call after every use of it.
esp_err_t err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_());
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
return err;
}
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
return err;
}
err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
return err;
}
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
void IDFUARTComponent::set_framing_(const Framing &framing) {
this->baud_rate_ = framing.baud_rate;
this->data_bits_ = framing.data_bits;
this->stop_bits_ = framing.stop_bits;
this->parity_ = framing.parity;
this->rx_full_threshold_ = framing.rx_full_threshold;
}
esp_err_t IDFUARTComponent::apply_settings_live() {
if (this->is_failed()) {
return ESP_ERR_INVALID_STATE;
}
// No driver yet: nothing to reconfigure in place.
if (!uart_is_driver_installed(this->uart_num_)) {
this->load_settings(false);
return this->is_failed() ? ESP_FAIL : ESP_OK;
}
// Keeps the driver ring buffers; flushes both hardware FIFOs (in-flight bytes lost).
uart_config_t uart_config = this->get_config_();
esp_err_t err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
// Failure leaves the registers reset; put back the last accepted framing so the
// getters still describe the hardware.
if (this->last_good_framing_.baud_rate == 0) {
ESP_LOGE(TAG, "uart_param_config (live) failed: %s; no previous framing to restore", esp_err_to_name(err));
this->mark_failed();
return err;
}
ESP_LOGW(TAG, "uart_param_config (live) failed: %s; restoring %" PRIu32 " baud", esp_err_to_name(err),
this->last_good_framing_.baud_rate);
this->set_framing_(this->last_good_framing_);
uart_config = this->get_config_();
esp_err_t restore_err = uart_param_config(this->uart_num_, &uart_config);
if (restore_err != ESP_OK) {
ESP_LOGE(TAG, "UART left unconfigured after failed live reconfigure: %s", esp_err_to_name(restore_err));
this->mark_failed();
return err;
}
// Previous framing is live again; report the refusal (line-setting errors log).
this->apply_line_settings_();
return err;
}
this->last_good_framing_ = this->framing_();
// The new framing is live; a line-setting failure here only logs.
this->apply_line_settings_();
return ESP_OK;
}
void IDFUARTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_);
LOG_PIN(" TX Pin: ", this->tx_pin_);
@@ -52,13 +52,49 @@ class IDFUARTComponent final : public UARTComponent, public Component {
void load_settings(bool dump_config) override;
using UARTComponent::load_settings; // also bring in the no-arg overload for convenience
/**
* Apply the current framing (baud rate, parity, data/stop bits) to the installed
* driver in place, without the delete/reinstall of load_settings(). Tasks blocked in
* the driver survive and the ring buffers are kept, but both hardware FIFOs are
* flushed: a frame in flight reaches the peer truncated and bytes not yet out of the
* RX FIFO are dropped. No lock is taken: quiesce writers first if that matters.
* rx_full_threshold is not rescaled (call set_rx_full_threshold_ms() first if it
* should follow the baud rate); a rollback restores the value from the last accepted
* configuration, undoing a standalone set_rx_full_threshold() made since. Without an
* installed driver this is a full load_settings(false) instead.
*
* @return ESP_OK once the new framing is live (a line-setting error after that only
* logs). On rejection (unreachable baud rate) the previous framing is restored and
* the driver's error returned; if the restore fails too the component is marked
* failed. ESP_ERR_INVALID_STATE if already failed; ESP_FAIL if the fallback
* load_settings() fails.
*/
esp_err_t apply_settings_live();
void on_shutdown() override;
protected:
void check_logger_conflict() override;
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;
uint8_t data_bits;
uint8_t stop_bits;
UARTParityOptions parity;
size_t rx_full_threshold; // sized for the baud rate, so rolled back with it
};
Framing framing_() const {
return {this->baud_rate_, this->data_bits_, this->stop_bits_, this->parity_, this->rx_full_threshold_};
}
void set_framing_(const Framing &framing);
// Last framing the driver accepted; baud_rate 0 means none yet.
Framing last_good_framing_{};
bool has_peek_{false};
uint8_t peek_byte_;
uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).