From 5e842a8b207230be2fd8db09baf5098f62767a21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 7 Mar 2026 23:23:13 -0600 Subject: [PATCH 1/2] [uart] Return `flush` result, expose timeout via config (#14608) Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ble_nus/ble_nus.cpp | 5 +++-- esphome/components/ble_nus/ble_nus.h | 2 +- esphome/components/uart/__init__.py | 6 ++++++ esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.h | 16 +++++++++++++++- .../components/uart/uart_component_esp8266.cpp | 3 ++- esphome/components/uart/uart_component_esp8266.h | 2 +- .../components/uart/uart_component_esp_idf.cpp | 13 +++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 5 ++++- esphome/components/uart/uart_component_host.cpp | 5 +++-- esphome/components/uart/uart_component_host.h | 2 +- .../components/uart/uart_component_libretiny.cpp | 3 ++- .../components/uart/uart_component_libretiny.h | 2 +- .../components/uart/uart_component_rp2040.cpp | 3 ++- esphome/components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 11 ++++++++--- esphome/components/usb_uart/usb_uart.cpp | 5 ++++- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 5 +++-- esphome/components/weikai/weikai.h | 2 +- tests/components/ld2450/common.h | 2 +- tests/components/uart/common.h | 2 +- .../external_components/uart_mock/uart_mock.cpp | 3 ++- .../external_components/uart_mock/uart_mock.h | 2 +- 25 files changed, 77 insertions(+), 30 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d1710100a09..e38dc998028 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -100,16 +100,17 @@ size_t BLENUS::available() { #endif } -void BLENUS::flush() { +uart::FlushResult BLENUS::flush() { constexpr uint32_t timeout_5sec = 5000; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_5sec) { ESP_LOGW(TAG, "Flush timeout"); - return; + return uart::FlushResult::TIMEOUT; } delay(1); } + return uart::FlushResult::SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index 67e9ae9f97a..b482c240e53 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 1b976b73a90..2cb6eac0509 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -183,6 +183,7 @@ UART_PARITY_OPTIONS = { "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, } +CONF_FLUSH_TIMEOUT = "flush_timeout" CONF_RX_FULL_THRESHOLD = "rx_full_threshold" CONF_RX_TIMEOUT = "rx_timeout" @@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_RX_TIMEOUT, esp32=2): cv.All( cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=0, max=92) ), + cv.Optional(CONF_FLUSH_TIMEOUT): cv.All( + cv.only_on_esp32, cv.positive_time_period_milliseconds + ), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -345,6 +349,8 @@ async def to_code(config): ) cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) + if CONF_FLUSH_TIMEOUT in config: + cg.add(var.set_flush_timeout(config[CONF_FLUSH_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index bb91e5cd7c0..2c4fb34c9a8 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - void flush() { this->parent_->flush(); } + FlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 078ce64b30f..3d7e71b89fd 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -29,6 +29,14 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); +/// Result of a flush() call. +enum class FlushResult { + SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + TIMEOUT, ///< Confirmed: timed out before TX completed. + FAILED, ///< Confirmed: driver or hardware error. + ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +}; + class UARTComponent { public: // Writes an array of bytes to the UART bus. @@ -72,7 +80,13 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - virtual void flush() = 0; + // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual FlushResult flush() = 0; + + // Sets the maximum time to wait for TX to drain during flush(). + // Only meaningful on ESP32 (IDF). Other platforms ignore this value. + // @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely. + virtual void set_flush_timeout(uint32_t flush_timeout_ms) {} // Sets the TX (transmit) pin for the UART bus. // @param tx_pin Pointer to the internal GPIO pin used for transmission. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 3ebf381c846..91218c43006 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,13 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -void ESP8266UartComponent::flush() { +FlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } + return FlushResult::ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index e84cbe386d5..ca90dc59646 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 544404448b9..47ddf1a38d7 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -230,6 +230,9 @@ void IDFUARTComponent::dump_config() { " RX Timeout: %u", this->rx_buffer_size_, this->rx_full_threshold_, this->rx_timeout_); } + if (this->flush_timeout_ms_ > 0) { + ESP_LOGCONFIG(TAG, " Flush Timeout: %" PRIu32 " ms", this->flush_timeout_ms_); + } ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" @@ -332,9 +335,15 @@ size_t IDFUARTComponent::available() { return available; } -void IDFUARTComponent::flush() { +FlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - uart_wait_tx_done(this->uart_num_, portMAX_DELAY); + 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); + if (err == ESP_OK) + return FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return FlushResult::TIMEOUT; + return FlushResult::FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 631bf54cd56..9fa2013cfd1 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,9 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; + + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } uint8_t get_hw_serial_number() { return this->uart_num_; } @@ -57,6 +59,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool has_peek_{false}; uint8_t peek_byte_; + uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY). #ifdef USE_UART_WAKE_LOOP_ON_RX // ISR callback for UART RX data notification — wakes the main loop directly. diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 9dce25c500e..66026f3ccdd 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,12 +274,13 @@ size_t HostUartComponent::available() { return result; }; -void HostUartComponent::flush() { +FlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return; + return FlushResult::ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); + return FlushResult::ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index 89b951093b6..c22efdcb92d 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 83d2acb332d..6a550f296a1 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,9 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -void LibreTinyUARTComponent::flush() { +FlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 31f082d31e1..77df8080671 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index faf8f4d90f1..858f1a02ddf 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -187,9 +187,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -void RP2040UartComponent::flush() { +FlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 4ca58e8dc60..891212ca74c 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index ddcc65232d4..624f41cf8c6 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parentedhas_peek_ ? 1 : 0); } -void USBCDCACMInstance::flush() { +uart::FlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return; + return uart::FlushResult::ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -341,7 +341,12 @@ void USBCDCACMInstance::flush() { } // Also wait for USB to finish transmitting - tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); + esp_err_t err = tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); + if (err == ESP_OK) + return uart::FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a1f87384914..a0101a55463 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -166,7 +166,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -void USBUartChannel::flush() { +uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The 100 ms timeout guards against a device that stops responding mid-flush; @@ -177,6 +177,9 @@ void USBUartChannel::flush() { this->parent_->start_output(this); yield(); } + if (!this->output_queue_.empty() || this->output_started_.load()) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7290f8a958a..671f0cab8c0 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -110,7 +110,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 3f5d6c787cb..f01d164e9fd 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,15 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast(buffer), length); } -void WeikaiChannel::flush() { +uart::FlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return; + return uart::FlushResult::TIMEOUT; } yield(); // reschedule our thread to avoid blocking } + return uart::FlushResult::SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 43c3a1e4f4c..715b82bfc71 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - void flush() override; + uart::FlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index d5ffbe1295b..9f9e7b3e9f6 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(uart::FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index 1f9bfa15e7f..f7e2d8a3f76 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index e8c07d632de..1edeb97cf16 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -144,8 +144,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -void MockUartComponent::flush() { +uart::FlushResult MockUartComponent::flush() { // Nothing to flush in mock + return uart::FlushResult::ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 901e371dec4..c9afb963579 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; From f7aeaad929c65ba459436e7eef860811ffb3a160 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 19:45:07 -1000 Subject: [PATCH 2/2] [api] Replace std::vector with APIBuffer to skip zero-fill Replace std::vector with a minimal APIBuffer class for the shared protobuf write buffer, frame helper receive buffer (rx_buf_), and noise handshake prologue buffer. std::vector::resize() zero-fills new bytes via memset. Every byte is immediately overwritten by the protobuf encoder or socket reads, making the zero-fill pure waste. APIBuffer skips zero-initialization on resize() and uses make_unique_for_overwrite where available. Also removes a dead write_raw_ template overload from api_frame_helper.h. --- esphome/components/api/api_buffer.cpp | 13 ++++ esphome/components/api/api_buffer.h | 62 +++++++++++++++++++ esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 8 +-- esphome/components/api/api_frame_helper.h | 9 +-- .../components/api/api_frame_helper_noise.cpp | 3 +- .../components/api/api_frame_helper_noise.h | 4 +- esphome/components/api/api_server.h | 4 +- esphome/components/api/proto.h | 10 +-- tests/components/api/test.ln882x-ard.yaml | 5 ++ 10 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 esphome/components/api/api_buffer.cpp create mode 100644 esphome/components/api/api_buffer.h create mode 100644 tests/components/api/test.ln882x-ard.yaml diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp new file mode 100644 index 00000000000..6db18b0365e --- /dev/null +++ b/esphome/components/api/api_buffer.cpp @@ -0,0 +1,13 @@ +#include "api_buffer.h" + +namespace esphome::api { + +void APIBuffer::grow_(size_t n) { + auto new_data = make_buffer(n); + if (this->size_) + std::memcpy(new_data.get(), this->data_.get(), this->size_); + this->data_ = std::move(new_data); + this->capacity_ = n; +} + +} // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h new file mode 100644 index 00000000000..13dd4e56108 --- /dev/null +++ b/esphome/components/api/api_buffer.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Helper to use make_unique_for_overwrite where available (skips zero-fill), +/// falling back to make_unique on older GCC (ESP8266, BK72xx, LN882x). +inline std::unique_ptr make_buffer(size_t n) { +#if defined(USE_ESP8266) || defined(USE_BK72XX) || defined(USE_LN882X) + return std::make_unique(n); +#else + return std::make_unique_for_overwrite(n); +#endif +} + +/// Byte buffer that skips zero-initialization on resize(). +/// +/// std::vector::resize() zero-fills new bytes via memset. For the +/// shared protobuf write buffer, every byte is overwritten by the encoder, +/// making the zero-fill pure waste. For the receive buffer, bytes are +/// overwritten by socket reads. +/// +/// Safe because: callers always write exactly the number of bytes they +/// resize for, and debug_check_bounds_ validates writes in debug builds. +class APIBuffer { + public: + void clear() { this->size_ = 0; } + inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE { + if (n > this->capacity_) + this->grow_(n); + } + inline void resize(size_t n) ESPHOME_ALWAYS_INLINE { + this->reserve(n); + this->size_ = n; // no zero-fill + } + uint8_t *data() { return this->data_.get(); } + const uint8_t *data() const { return this->data_.get(); } + size_t size() const { return this->size_; } + bool empty() const { return this->size_ == 0; } + uint8_t &operator[](size_t i) { return this->data_[i]; } + const uint8_t &operator[](size_t i) const { return this->data_[i]; } + /// Release all memory (equivalent to std::vector swap trick). + void release() { + this->data_.reset(); + this->size_ = 0; + this->capacity_ = 0; + } + + protected: + void grow_(size_t n); + std::unique_ptr data_; + size_t size_{0}; + size_t capacity_{0}; +}; + +} // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b9b33ddcc22..2f746b73539 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1915,7 +1915,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode if (total_calculated_size > remaining_size) return 0; // Doesn't fit - std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); if (conn->flags_.batch_first_message) { // First message - buffer already prepared by caller, just clear flag @@ -2083,7 +2083,7 @@ void APIConnection::process_batch_() { // Separated from process_batch_() so the single-message fast path gets a minimal // stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. -void APIConnection::process_batch_multi_(std::vector &shared_buf, size_t num_items, uint8_t header_padding, +void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) { // Ensure MessageInfo remains trivially destructible for our placement new approach static_assert(std::is_trivially_destructible::value, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b075bc83ab2..3d864a7cf23 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -278,7 +278,7 @@ class APIConnection final : public APIServerConnectionBase { } } - void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) @@ -289,7 +289,7 @@ class APIConnection final : public APIServerConnectionBase { } // Convenience overload - computes frame overhead internally - void prepare_first_message_buffer(std::vector &shared_buf, size_t payload_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) { const uint8_t header_padding = this->helper_->frame_header_padding(); const uint8_t footer_size = this->helper_->frame_footer_size(); this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); @@ -669,8 +669,8 @@ class APIConnection final : public APIServerConnectionBase { bool schedule_batch_(); void process_batch_(); - void process_batch_multi_(std::vector &shared_buf, size_t num_items, uint8_t header_padding, - uint8_t footer_size) __attribute__((noinline)); + void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) + __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 2b4e9ea3cdb..2ec2b72c930 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,10 +5,10 @@ #include #include #include -#include #include "esphome/core/defines.h" #ifdef USE_API +#include "esphome/components/api/api_buffer.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -179,7 +179,7 @@ class APIFrameHelper { // and clearing would lose partially received data. if (this->rx_buf_len_ == 0) { // Use swap trick since shrink_to_fit() is non-binding and may be ignored - std::vector().swap(this->rx_buf_); + this->rx_buf_.release(); } } @@ -206,9 +206,6 @@ class APIFrameHelper { // Common socket write error handling APIError handle_socket_write_error_(); - template - APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, - const std::string &info, StateEnum &state, StateEnum failed_state); // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_; @@ -234,7 +231,7 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::array, API_MAX_SEND_QUEUE> tx_buf_; - std::vector rx_buf_; + APIBuffer rx_buf_; // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index ba4f2f0642d..22a477aa59d 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -579,8 +579,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now - // Use swap idiom to actually release memory (= {} only clears size, not capacity) - std::vector().swap(prologue_); + prologue_.release(); err = noise_handshakestate_start(handshake_); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 183b8c8a51d..83410febb26 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Reference to noise context (4 bytes on 32-bit) APINoiseContext &ctx_; - // Vector (12 bytes on 32-bit) - std::vector prologue_; + // Buffer for noise handshake prologue (released after handshake) + APIBuffer prologue_; // NoiseProtocolId (size depends on implementation) NoiseProtocolId nid_; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e5f371d8a13..aabea3485f3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -65,7 +65,7 @@ class APIServer : public Component, void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections - std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } + APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); @@ -276,7 +276,7 @@ class APIServer : public Component, // Not pre-allocated: all send paths call prepare_first_message_buffer() which // reserves the exact needed size. Pre-allocating here would cause heap fragmentation // since the buffer would almost always reallocate on first use. - std::vector shared_write_buffer_; + APIBuffer shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index bbdd11b29d0..8468b31bcd4 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,6 +1,7 @@ #pragma once #include "api_pb2_defines.h" +#include "api_buffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -237,9 +238,8 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} - ProtoWriteBuffer(std::vector *buffer, size_t write_pos) - : buffer_(buffer), pos_(buffer->data() + write_pos) {} + ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} void encode_varint_raw(uint32_t value) { while (value > 0x7F) { this->debug_check_bounds_(1); @@ -375,7 +375,7 @@ class ProtoWriteBuffer { // Non-template core for encode_optional_sub_message. void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); - std::vector *get_buffer() const { return buffer_; } + APIBuffer *get_buffer() const { return buffer_; } protected: #ifdef ESPHOME_DEBUG_API @@ -385,7 +385,7 @@ class ProtoWriteBuffer { void debug_check_bounds_([[maybe_unused]] size_t bytes) {} #endif - std::vector *buffer_; + APIBuffer *buffer_; uint8_t *pos_; }; diff --git a/tests/components/api/test.ln882x-ard.yaml b/tests/components/api/test.ln882x-ard.yaml new file mode 100644 index 00000000000..46c01d926f2 --- /dev/null +++ b/tests/components/api/test.ln882x-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1