From 6b8da2f0ca1dcc94a88aa553445fee21fe7ccd3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 16:09:37 -1000 Subject: [PATCH 1/7] preen --- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index f4b63f3a5d..33ea847497 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -199,6 +199,11 @@ void BluetoothProxy::loop() { return; } + // Early return if no advertisements pending + if (this->advertisement_count_ == 0) { + return; + } + // Flush any pending BLE advertisements that have been accumulated but not yet sent uint32_t now = App.get_loop_component_start_time(); From c7884253d24642f58471542eef2e5d320888c838 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 16:11:16 -1000 Subject: [PATCH 2/7] cannot always need to update timestamp --- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 33ea847497..f4b63f3a5d 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -199,11 +199,6 @@ void BluetoothProxy::loop() { return; } - // Early return if no advertisements pending - if (this->advertisement_count_ == 0) { - return; - } - // Flush any pending BLE advertisements that have been accumulated but not yet sent uint32_t now = App.get_loop_component_start_time(); From ffbadc09296e42893f34709b168f87bb9f2e9ecc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 16:30:56 -1000 Subject: [PATCH 3/7] [esp32_ble_tracker] Batch BLE advertisement processing to reduce overhead --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 44577afbbd..96003073d7 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -128,46 +128,53 @@ void ESP32BLETracker::loop() { uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); while (read_idx != write_idx) { - // Process one result at a time directly from ring buffer - BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; + // Calculate how many contiguous results we can process in one batch + // If write > read: process all results from read to write + // If write <= read (wraparound): process from read to end of buffer first + size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx); + // Process the batch for raw advertisements if (this->raw_advertisements_) { for (auto *listener : this->listeners_) { - listener->parse_devices(&scan_result, 1); + listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); } for (auto *client : this->clients_) { - client->parse_devices(&scan_result, 1); + client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); } } + // Process individual results for parsed advertisements if (this->parse_advertisements_) { #ifdef USE_ESP32_BLE_DEVICE - ESPBTDevice device; - device.parse_scan_rst(scan_result); + for (size_t i = 0; i < batch_size; i++) { + BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i]; + ESPBTDevice device; + device.parse_scan_rst(scan_result); - bool found = false; - for (auto *listener : this->listeners_) { - if (listener->parse_device(device)) - found = true; - } + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } - for (auto *client : this->clients_) { - if (client->parse_device(device)) { - found = true; - if (!connecting && client->state() == ClientState::DISCOVERED) { - promote_to_connecting = true; + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + if (!connecting && client->state() == ClientState::DISCOVERED) { + promote_to_connecting = true; + } } } - } - if (!found && !this->scan_continuous_) { - this->print_bt_device_info(device); + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } } #endif // USE_ESP32_BLE_DEVICE } - // Move to next entry in ring buffer - read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + // Update read index for entire batch + read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE; // Store with release to ensure reads complete before index update this->ring_read_index_.store(read_idx, std::memory_order_release); From 65cbb0d7413d0396ac98679404a8c1b86b561fd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 19:31:53 -1000 Subject: [PATCH 4/7] [gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) --- .../components/gpio/binary_sensor/__init__.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 59f54520fa..8372bc7e08 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -4,7 +4,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN +from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_ID, + CONF_NAME, + CONF_NUMBER, + CONF_PIN, +) from esphome.core import CORE from .. import gpio_ns @@ -76,6 +82,18 @@ async def to_code(config): ) use_interrupt = False + # Check if pin is shared with other components (allow_other_uses) + # When a pin is shared, interrupts can interfere with other components + # (e.g., duty_cycle sensor) that need to monitor the pin's state changes + if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): + _LOGGER.info( + "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " + "The sensor will use polling mode for compatibility with other pin uses.", + config.get(CONF_NAME, config[CONF_ID]), + config[CONF_PIN][CONF_NUMBER], + ) + use_interrupt = False + cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) From e2524c9764f21fd23978c2d91229b7b99827e35b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 21:14:25 -1000 Subject: [PATCH 5/7] [api] Eliminate heap allocation in process_batch_ using stack-allocated PacketInfo array --- esphome/components/api/api_connection.cpp | 21 +++++++++++++-------- esphome/components/api/api_connection.h | 10 +++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2ac3303691..c829d25c83 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1708,9 +1708,12 @@ void APIConnection::process_batch_() { return; } - // Pre-allocate storage for packet info - std::vector packet_info; - packet_info.reserve(num_items); + size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH); + + // Stack-allocated array for packet info + alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)]; + PacketInfo *packet_info = reinterpret_cast(packet_info_storage); + size_t packet_count = 0; // Cache these values to avoid repeated virtual calls const uint8_t header_padding = this->helper_->frame_header_padding(); @@ -1742,8 +1745,8 @@ void APIConnection::process_batch_() { // The actual message data follows after the header padding uint32_t current_offset = 0; - // Process items and encode directly to buffer - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + // Process items and encode directly to buffer (up to our limit) + for (size_t i = 0; i < packets_to_process; i++) { const auto &item = this->deferred_batch_[i]; // Try to encode message // The creator will calculate overhead to determine if the message fits @@ -1757,7 +1760,9 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size uint16_t proto_payload_size = payload_size - header_padding - footer_size; - packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); + // Use placement new to construct PacketInfo in pre-allocated stack array + // This avoids default-constructing all MAX_PACKETS_PER_BATCH elements + new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size); // Update tracking variables items_processed++; @@ -1783,8 +1788,8 @@ void APIConnection::process_batch_() { } // Send all collected packets - APIError err = - this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); + APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, + std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3873c7fcac..ba5a2678e5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -19,7 +19,15 @@ namespace api { // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending -static constexpr size_t MAX_INITIAL_PER_BATCH = 20; +static constexpr size_t MAX_INITIAL_PER_BATCH = 24; +// Maximum number of packets to process in a single batch (platform-dependent) +// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ +// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes +#if defined(USE_ESP32) || defined(USE_HOST) +static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty +#else +static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks +#endif class APIConnection : public APIServerConnection { public: From 8223db761d31cbc36518473e57a19605874f09fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 22:05:55 -1000 Subject: [PATCH 6/7] document --- esphome/components/api/api_connection.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index ba5a2678e5..bf5aca25c7 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -19,6 +19,8 @@ namespace api { // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending +// This was increased from 20 to 24 after removing the unique_id field from entity info messages, +// which reduced message sizes allowing more entities per batch without exceeding packet limits static constexpr size_t MAX_INITIAL_PER_BATCH = 24; // Maximum number of packets to process in a single batch (platform-dependent) // This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ From 09705ca5269a80cee7ffc4d16b16733b1a9c17e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 22:11:13 -1000 Subject: [PATCH 7/7] guard --- esphome/components/api/api_connection.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c829d25c83..53f1b2632f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1671,6 +1671,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { } void APIConnection::process_batch_() { + // Ensure PacketInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible::value, + "PacketInfo must remain trivially destructible with this placement-new approach"); + if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; return;