From 5850931d2793834c60217a5b365b21f00a544841 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Mar 2026 22:45:25 -1000 Subject: [PATCH 1/4] [esp32_ble] Devirtualize BLE event handler dispatch Replace virtual handler interfaces (GAPEventHandler, GAPScanEventHandler, GATTcEventHandler, GATTsEventHandler, BLEStatusEventHandler) with StaticCallbackManager-based dispatch using lambda callbacks. This eliminates vtable lookups and MI this-pointer adjustment thunks on every BLE event dispatch in the hot path. Each lambda captures a single pointer (fits in Callback inline storage, no heap allocation) and the StaticCallbackManager avoids std::vector template bloat. Breaking change: external components inheriting from the virtual handler classes need to switch to add_*_callback() registration instead. --- esphome/components/esp32_ble/__init__.py | 68 +++++++++++++----- esphome/components/esp32_ble/ble.cpp | 21 ++---- esphome/components/esp32_ble/ble.h | 69 +++++++------------ .../components/esp32_ble_beacon/__init__.py | 1 - .../esp32_ble_beacon/esp32_ble_beacon.h | 4 +- .../components/esp32_ble_server/__init__.py | 1 - .../components/esp32_ble_server/ble_server.h | 7 +- .../components/esp32_ble_tracker/__init__.py | 2 - .../esp32_ble_tracker/esp32_ble_tracker.h | 13 ++-- esphome/core/helpers.h | 32 +++++++++ 10 files changed, 121 insertions(+), 97 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 43208eb87e..21bdff8c7c 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -120,7 +120,7 @@ def _get_required_loggers() -> set[BTLoggers]: return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set()) -# Dataclass for handler registration counts +# Dataclass for handler registration counts (used for StaticCallbackManager sizing) @dataclass class HandlerCounts: gap_event: int = 0 @@ -130,46 +130,83 @@ class HandlerCounts: ble_status_event: int = 0 -# Track handler registration counts for StaticVector sizing +# Track handler registration counts for StaticCallbackManager sizing _handler_counts = HandlerCounts() def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: - """Register a GAP event handler and track the count.""" + """Register a GAP event handler callback.""" _handler_counts.gap_event += 1 - cg.add(parent_var.register_gap_event_handler(handler_var)) + cg.add( + parent_var.add_gap_event_callback( + cg.RawExpression( + f"[{handler_var}](esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {{ " + f"{handler_var}->gap_event_handler(event, param); }}" + ) + ) + ) def register_gap_scan_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a GAP scan event handler and track the count.""" + """Register a GAP scan event handler callback.""" _handler_counts.gap_scan_event += 1 - cg.add(parent_var.register_gap_scan_event_handler(handler_var)) + cg.add( + parent_var.add_gap_scan_event_callback( + cg.RawExpression( + f"[{handler_var}](const esphome::esp32_ble::BLEScanResult &scan_result) {{ " + f"{handler_var}->gap_scan_event_handler(scan_result); }}" + ) + ) + ) def register_gattc_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a GATTc event handler and track the count.""" + """Register a GATTc event handler callback.""" _handler_counts.gattc_event += 1 - cg.add(parent_var.register_gattc_event_handler(handler_var)) + cg.add( + parent_var.add_gattc_event_callback( + cg.RawExpression( + f"[{handler_var}](esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, " + f"esp_ble_gattc_cb_param_t *param) {{ " + f"{handler_var}->gattc_event_handler(event, gattc_if, param); }}" + ) + ) + ) def register_gatts_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a GATTs event handler and track the count.""" + """Register a GATTs event handler callback.""" _handler_counts.gatts_event += 1 - cg.add(parent_var.register_gatts_event_handler(handler_var)) + cg.add( + parent_var.add_gatts_event_callback( + cg.RawExpression( + f"[{handler_var}](esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, " + f"esp_ble_gatts_cb_param_t *param) {{ " + f"{handler_var}->gatts_event_handler(event, gatts_if, param); }}" + ) + ) + ) def register_ble_status_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a BLE status event handler and track the count.""" + """Register a BLE status event handler callback.""" _handler_counts.ble_status_event += 1 - cg.add(parent_var.register_ble_status_event_handler(handler_var)) + cg.add( + parent_var.add_ble_status_event_callback( + cg.RawExpression( + f"[{handler_var}]() {{ " + f"{handler_var}->ble_before_disabled_event_handler(); }}" + ) + ) + ) def register_bt_logger(*loggers: BTLoggers) -> None: @@ -225,10 +262,6 @@ NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) -GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") -GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") -GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") - BLEEnabledCondition = esp32_ble_ns.class_("BLEEnabledCondition", automation.Condition) BLEEnableAction = esp32_ble_ns.class_("BLEEnableAction", automation.Action) BLEDisableAction = esp32_ble_ns.class_("BLEDisableAction", automation.Action) @@ -494,8 +527,7 @@ FINAL_VALIDATE_SCHEMA = final_validation # a chance to register their handlers before the counts are added to defines. @coroutine_with_priority(CoroPriority.FINAL) async def _add_ble_handler_defines(): - # Add defines for StaticVector sizing based on handler registration counts - # Only define if count > 0 to avoid allocating unnecessary memory + # Add defines for StaticCallbackManager sizing based on handler registration counts if _handler_counts.gap_event > 0: cg.add_define( "ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 317f8fd11b..2cd2ec67f7 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -408,9 +408,7 @@ void ESP32BLE::loop() { esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param; ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); - for (auto *gatts_handler : this->gatts_event_handlers_) { - gatts_handler->gatts_event_handler(event, gatts_if, param); - } + this->gatts_event_callbacks_.call(event, gatts_if, param); break; } #endif @@ -420,9 +418,7 @@ void ESP32BLE::loop() { esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param; ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); - for (auto *gattc_handler : this->gattc_event_handlers_) { - gattc_handler->gattc_event_handler(event, gattc_if, param); - } + this->gattc_event_callbacks_.call(event, gattc_if, param); break; } #endif @@ -431,10 +427,7 @@ void ESP32BLE::loop() { switch (gap_event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - // Use the new scan event handler - no memcpy! - for (auto *scan_handler : this->gap_scan_event_handlers_) { - scan_handler->gap_scan_event_handler(ble_event->scan_result()); - } + this->gap_scan_event_callbacks_.call(ble_event->scan_result()); #endif break; @@ -478,9 +471,7 @@ void ESP32BLE::loop() { } // clang-format on // Dispatch to all registered handlers - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler(gap_event, param); - } + this->gap_event_callbacks_.call(gap_event, param); } #endif break; @@ -518,9 +509,7 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { ESP_LOGD(TAG, "Disabling"); #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - for (auto *ble_event_handler : this->ble_status_event_handlers_) { - ble_event_handler->ble_before_disabled_event_handler(); - } + this->ble_status_event_callbacks_.call(); #endif if (!ble_dismantle_()) { diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 82b2789461..de8c8c2343 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -87,37 +87,6 @@ enum BLEComponentState : uint8_t { BLE_COMPONENT_STATE_ACTIVE, }; -class GAPEventHandler { - public: - virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; -}; - -class GAPScanEventHandler { - public: - virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; -}; - -#ifdef USE_ESP32_BLE_CLIENT -class GATTcEventHandler { - public: - virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) = 0; -}; -#endif - -#ifdef USE_ESP32_BLE_SERVER -class GATTsEventHandler { - public: - virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) = 0; -}; -#endif - -class BLEStatusEventHandler { - public: - virtual void ble_before_disabled_event_handler() = 0; -}; - class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } @@ -154,22 +123,28 @@ class ESP32BLE : public Component { #endif #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + template void add_gap_event_callback(F &&callback) { + this->gap_event_callbacks_.add(std::forward(callback)); + } #endif #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - void register_gap_scan_event_handler(GAPScanEventHandler *handler) { - this->gap_scan_event_handlers_.push_back(handler); + template void add_gap_scan_event_callback(F &&callback) { + this->gap_scan_event_callbacks_.add(std::forward(callback)); } #endif #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) - void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } + template void add_gattc_event_callback(F &&callback) { + this->gattc_event_callbacks_.add(std::forward(callback)); + } #endif #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) - void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + template void add_gatts_event_callback(F &&callback) { + this->gatts_event_callbacks_.add(std::forward(callback)); + } #endif #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - void register_ble_status_event_handler(BLEStatusEventHandler *handler) { - this->ble_status_event_handlers_.push_back(handler); + template void add_ble_status_event_callback(F &&callback) { + this->ble_status_event_callbacks_.add(std::forward(callback)); } #endif void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } @@ -202,21 +177,27 @@ class ESP32BLE : public Component { private: template friend void enqueue_ble_event(Args... args); - // Handler vectors - use StaticVector when counts are known at compile time #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - StaticVector gap_event_handlers_; + StaticCallbackManager + gap_event_callbacks_; #endif #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - StaticVector gap_scan_event_handlers_; + StaticCallbackManager + gap_scan_event_callbacks_; #endif #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) - StaticVector gattc_event_handlers_; + StaticCallbackManager + gattc_event_callbacks_; #endif #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) - StaticVector gatts_event_handlers_; + StaticCallbackManager + gatts_event_callbacks_; #endif #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - StaticVector ble_status_event_handlers_; + StaticCallbackManager ble_status_event_callbacks_; #endif // Large objects (size depends on template parameters, but typically aligned to 4 bytes) diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 04c783980d..e2e790164e 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -13,7 +13,6 @@ esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") ESP32BLEBeacon = esp32_ble_beacon_ns.class_( "ESP32BLEBeacon", cg.Component, - esp32_ble.GAPEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) CONF_MAJOR = "major" diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 7a0424f3aa..e16c413179 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -35,7 +35,7 @@ using esp_ble_ibeacon_t = struct { using namespace esp32_ble; -class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented { +class ESP32BLEBeacon : public Component, public Parented { public: explicit ESP32BLEBeacon(const std::array &uuid) : uuid_(uuid) {} @@ -51,7 +51,7 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } #endif - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); protected: void on_advertise_(); diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 827ddba955..57106cd93b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -72,7 +72,6 @@ BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic") BLEServer = esp32_ble_server_ns.class_( "BLEServer", cg.Component, - esp32_ble.GATTsEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace( diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 1b419d2ee4..9708ed40c8 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -24,7 +24,7 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { +class BLEServer : public Component, public Parented { public: void setup() override; void loop() override; @@ -53,10 +53,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv const uint16_t *get_clients() const { return this->clients_; } uint8_t get_client_count() const { return this->client_count_; } - void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) override; + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void ble_before_disabled_event_handler() override; + void ble_before_disabled_event_handler(); // Direct callback registration - supports multiple callbacks void on_connect(std::function &&callback) { diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index c5e8f3178d..b9c4c28ccf 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -90,8 +90,6 @@ esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", cg.Component, - esp32_ble.GAPEventHandler, - esp32_ble.GATTcEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f50ed107b6..ff69a4dcd2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -291,10 +291,6 @@ class ESPBTClient : public ESPBTDeviceListener { }; class ESP32BLETracker : public Component, - public GAPEventHandler, - public GAPScanEventHandler, - public GATTcEventHandler, - public BLEStatusEventHandler, #ifdef USE_OTA_STATE_LISTENER public ota::OTAGlobalStateListener, #endif @@ -325,11 +321,10 @@ class ESP32BLETracker : public Component, void start_scan(); void stop_scan(); - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; - void gap_scan_event_handler(const BLEScanResult &scan_result) override; - void ble_before_disabled_event_handler() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void gap_scan_event_handler(const BLEScanResult &scan_result); + void ble_before_disabled_event_handler(); #ifdef USE_OTA_STATE_LISTENER void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 66ba166445..d1477ca1e1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1830,6 +1830,38 @@ template class CallbackManager { std::vector> callbacks_; }; +/** CallbackManager backed by StaticVector for compile-time-known callback counts. + * + * Drop-in replacement for CallbackManager that avoids std::vector template bloat + * (_M_realloc_insert, etc.) when the maximum number of callbacks is known at compile time. + * + * @tparam N Maximum number of callbacks (compile-time constant, typically from cg.add_define()) + * @tparam Ts The arguments for the callbacks, wrapped in void(). + */ +template class StaticCallbackManager; + +template class StaticCallbackManager { + public: + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline without heap allocation. + template void add(F &&callback) { + this->callbacks_.push_back(Callback::create(std::forward(callback))); + } + + /// Call all callbacks in this manager. + void call(Ts... args) { + for (auto &cb : this->callbacks_) + cb.call(args...); + } + size_t size() const { return this->callbacks_.size(); } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { call(args...); } + + protected: + StaticVector, N> callbacks_; +}; + template class LazyCallbackManager; /** Lazy-allocating callback manager that only allocates memory when callbacks are registered. From d05ac6eae470ca4869e4a471cf0a08ba4a2ac0be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Mar 2026 22:47:28 -1000 Subject: [PATCH 2/4] Revert unnecessary comment/docstring changes --- esphome/components/esp32_ble/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 21bdff8c7c..070ddeeb2b 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -120,7 +120,7 @@ def _get_required_loggers() -> set[BTLoggers]: return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set()) -# Dataclass for handler registration counts (used for StaticCallbackManager sizing) +# Dataclass for handler registration counts @dataclass class HandlerCounts: gap_event: int = 0 @@ -130,12 +130,12 @@ class HandlerCounts: ble_status_event: int = 0 -# Track handler registration counts for StaticCallbackManager sizing +# Track handler registration counts for StaticVector sizing _handler_counts = HandlerCounts() def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: - """Register a GAP event handler callback.""" + """Register a GAP event handler and track the count.""" _handler_counts.gap_event += 1 cg.add( parent_var.add_gap_event_callback( @@ -150,7 +150,7 @@ def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) def register_gap_scan_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a GAP scan event handler callback.""" + """Register a GAP scan event handler and track the count.""" _handler_counts.gap_scan_event += 1 cg.add( parent_var.add_gap_scan_event_callback( @@ -165,7 +165,7 @@ def register_gap_scan_event_handler( def register_gattc_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a GATTc event handler callback.""" + """Register a GATTc event handler and track the count.""" _handler_counts.gattc_event += 1 cg.add( parent_var.add_gattc_event_callback( @@ -181,7 +181,7 @@ def register_gattc_event_handler( def register_gatts_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a GATTs event handler callback.""" + """Register a GATTs event handler and track the count.""" _handler_counts.gatts_event += 1 cg.add( parent_var.add_gatts_event_callback( @@ -197,7 +197,7 @@ def register_gatts_event_handler( def register_ble_status_event_handler( parent_var: cg.MockObj, handler_var: cg.MockObj ) -> None: - """Register a BLE status event handler callback.""" + """Register a BLE status event handler and track the count.""" _handler_counts.ble_status_event += 1 cg.add( parent_var.add_ble_status_event_callback( @@ -527,7 +527,8 @@ FINAL_VALIDATE_SCHEMA = final_validation # a chance to register their handlers before the counts are added to defines. @coroutine_with_priority(CoroPriority.FINAL) async def _add_ble_handler_defines(): - # Add defines for StaticCallbackManager sizing based on handler registration counts + # Add defines for StaticVector sizing based on handler registration counts + # Only define if count > 0 to avoid allocating unnecessary memory if _handler_counts.gap_event > 0: cg.add_define( "ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event From 888405790f518d1a127ccd92273b62d5c2e2d557 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Mar 2026 22:51:09 -1000 Subject: [PATCH 3/4] Fix static storage duration capture warnings in lambdas Use braced scope with local variable to avoid capturing static storage duration variables directly in lambda captures. --- esphome/components/esp32_ble/__init__.py | 82 +++++++++++++----------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 070ddeeb2b..96e6eaa765 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -134,16 +134,30 @@ class HandlerCounts: _handler_counts = HandlerCounts() +def _add_callback(parent_var, method, handler_var, params, call_args): + """Generate a lambda callback that forwards to a handler method. + + Uses a braced scope with a local variable to avoid capturing variables + with static storage duration. + """ + cg.add( + cg.RawStatement( + f"{{ auto *h = {handler_var}; " + f"{parent_var}->{method}(" + f"[h]({params}) {{ h->{call_args}; }}); }}" + ) + ) + + def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: """Register a GAP event handler and track the count.""" _handler_counts.gap_event += 1 - cg.add( - parent_var.add_gap_event_callback( - cg.RawExpression( - f"[{handler_var}](esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {{ " - f"{handler_var}->gap_event_handler(event, param); }}" - ) - ) + _add_callback( + parent_var, + "add_gap_event_callback", + handler_var, + "esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param", + "gap_event_handler(event, param)", ) @@ -152,13 +166,12 @@ def register_gap_scan_event_handler( ) -> None: """Register a GAP scan event handler and track the count.""" _handler_counts.gap_scan_event += 1 - cg.add( - parent_var.add_gap_scan_event_callback( - cg.RawExpression( - f"[{handler_var}](const esphome::esp32_ble::BLEScanResult &scan_result) {{ " - f"{handler_var}->gap_scan_event_handler(scan_result); }}" - ) - ) + _add_callback( + parent_var, + "add_gap_scan_event_callback", + handler_var, + "const esphome::esp32_ble::BLEScanResult &scan_result", + "gap_scan_event_handler(scan_result)", ) @@ -167,14 +180,12 @@ def register_gattc_event_handler( ) -> None: """Register a GATTc event handler and track the count.""" _handler_counts.gattc_event += 1 - cg.add( - parent_var.add_gattc_event_callback( - cg.RawExpression( - f"[{handler_var}](esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, " - f"esp_ble_gattc_cb_param_t *param) {{ " - f"{handler_var}->gattc_event_handler(event, gattc_if, param); }}" - ) - ) + _add_callback( + parent_var, + "add_gattc_event_callback", + handler_var, + "esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param", + "gattc_event_handler(event, gattc_if, param)", ) @@ -183,14 +194,12 @@ def register_gatts_event_handler( ) -> None: """Register a GATTs event handler and track the count.""" _handler_counts.gatts_event += 1 - cg.add( - parent_var.add_gatts_event_callback( - cg.RawExpression( - f"[{handler_var}](esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, " - f"esp_ble_gatts_cb_param_t *param) {{ " - f"{handler_var}->gatts_event_handler(event, gatts_if, param); }}" - ) - ) + _add_callback( + parent_var, + "add_gatts_event_callback", + handler_var, + "esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param", + "gatts_event_handler(event, gatts_if, param)", ) @@ -199,13 +208,12 @@ def register_ble_status_event_handler( ) -> None: """Register a BLE status event handler and track the count.""" _handler_counts.ble_status_event += 1 - cg.add( - parent_var.add_ble_status_event_callback( - cg.RawExpression( - f"[{handler_var}]() {{ " - f"{handler_var}->ble_before_disabled_event_handler(); }}" - ) - ) + _add_callback( + parent_var, + "add_ble_status_event_callback", + handler_var, + "", + "ble_before_disabled_event_handler()", ) From 33230e1c94d56c1d38d289664f89427813c77c38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Mar 2026 22:51:23 -1000 Subject: [PATCH 4/4] Add type annotations to _add_callback --- esphome/components/esp32_ble/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 96e6eaa765..3a38811648 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -134,7 +134,13 @@ class HandlerCounts: _handler_counts = HandlerCounts() -def _add_callback(parent_var, method, handler_var, params, call_args): +def _add_callback( + parent_var: cg.MockObj, + method: str, + handler_var: cg.MockObj, + params: str, + call_args: str, +) -> None: """Generate a lambda callback that forwards to a handler method. Uses a braced scope with a local variable to avoid capturing variables