[esp32_ble] Migrate the BLE count machinery to the shared slot counter factory (#18059)

This commit is contained in:
J. Nick Koston
2026-08-05 00:54:07 +00:00
committed by GitHub
parent 1bae91abaa
commit ad18bbc644
11 changed files with 149 additions and 99 deletions
@@ -87,6 +87,7 @@ async def to_code(config):
cg.add(var.set_active(config[CONF_ACTIVE]))
await esp32_ble_tracker.register_raw_ble_device(var, config)
await esp32_ble_tracker.register_scanner_state_listener(var, config)
# Define max connections for protobuf fixed array
connection_count = len(config.get(CONF_CONNECTIONS, []))
@@ -29,8 +29,6 @@ void BluetoothProxy::setup() {
// Capture the configured scan mode from YAML before any API changes
this->configured_scan_active_ = this->parent_->get_scan_active();
this->parent_->add_scanner_state_listener(this);
}
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
+26 -57
View File
@@ -1,5 +1,4 @@
from collections.abc import Callable, MutableMapping
from dataclasses import dataclass
from enum import Enum
import logging
from typing import Any
@@ -32,7 +31,7 @@ from esphome.const import (
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
)
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
from esphome.core import CORE, TimePeriod
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -134,18 +133,21 @@ def _get_required_loggers() -> set[BTLoggers]:
return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set())
# Dataclass for handler registration counts
@dataclass
class HandlerCounts:
gap_event: int = 0
gap_scan_event: int = 0
gattc_event: int = 0
gatts_event: int = 0
ble_status_event: int = 0
# Track handler registration counts for StaticVector sizing
_handler_counts = HandlerCounts()
# Handler slot counters sizing the StaticCallbackManager storage in ble.h;
# one request per register_* call below.
_request_gap_event_slot = cg.slot_counter("ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT")
_request_gap_scan_event_slot = cg.slot_counter(
"ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT"
)
_request_gattc_event_slot = cg.slot_counter(
"ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT"
)
_request_gatts_event_slot = cg.slot_counter(
"ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT"
)
_request_ble_status_event_slot = cg.slot_counter(
"ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT"
)
def _add_callback(
@@ -171,8 +173,8 @@ def _add_callback(
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
"""Register a GAP event handler and request a handler slot."""
_request_gap_event_slot()
_add_callback(
parent_var,
"add_gap_event_callback",
@@ -185,8 +187,8 @@ 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 and track the count."""
_handler_counts.gap_scan_event += 1
"""Register a GAP scan event handler and request a handler slot."""
_request_gap_scan_event_slot()
_add_callback(
parent_var,
"add_gap_scan_event_callback",
@@ -199,8 +201,8 @@ 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 and track the count."""
_handler_counts.gattc_event += 1
"""Register a GATTc event handler and request a handler slot."""
_request_gattc_event_slot()
_add_callback(
parent_var,
"add_gattc_event_callback",
@@ -213,8 +215,8 @@ def register_gattc_event_handler(
def register_gatts_event_handler(
parent_var: cg.MockObj, handler_var: cg.MockObj
) -> None:
"""Register a GATTs event handler and track the count."""
_handler_counts.gatts_event += 1
"""Register a GATTs event handler and request a handler slot."""
_request_gatts_event_slot()
_add_callback(
parent_var,
"add_gatts_event_callback",
@@ -227,8 +229,8 @@ 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 and track the count."""
_handler_counts.ble_status_event += 1
"""Register a BLE status event handler and request a handler slot."""
_request_ble_status_event_slot()
_add_callback(
parent_var,
"add_ble_status_event_callback",
@@ -518,36 +520,6 @@ def final_validation(config):
FINAL_VALIDATE_SCHEMA = final_validation
# This needs to be run as a job with CoroPriority.FINAL priority so that all components have
# 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
if _handler_counts.gap_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event
)
if _handler_counts.gap_scan_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT",
_handler_counts.gap_scan_event,
)
if _handler_counts.gattc_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT", _handler_counts.gattc_event
)
if _handler_counts.gatts_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT", _handler_counts.gatts_event
)
if _handler_counts.ble_status_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT",
_handler_counts.ble_status_event,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
@@ -633,9 +605,6 @@ async def to_code(config):
cg.add_define("USE_ESP32_BLE_ADVERTISING")
cg.add_define("USE_ESP32_BLE_UUID")
# Schedule the handler defines to be added after all components register
CORE.add_job(_add_ble_handler_defines)
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
async def ble_enabled_to_code(config, condition_id, template_arg, args):
@@ -1,6 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from esphome import automation
@@ -56,16 +55,8 @@ class BLEFeatures(StrEnum):
ESP_BT_DEVICE = "ESP_BT_DEVICE"
# Dataclass for registration counts
@dataclass
class RegistrationCounts:
listeners: int = 0
clients: int = 0
# CORE.data keys for state management
# CORE.data key for state management
ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY = "esp32_ble_tracker_required_features"
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY = "esp32_ble_tracker_registration_counts"
def _get_required_features() -> set[BLEFeatures]:
@@ -73,11 +64,13 @@ def _get_required_features() -> set[BLEFeatures]:
return CORE.data.setdefault(ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY, set())
def _get_registration_counts() -> RegistrationCounts:
"""Get the registration counts from CORE.data."""
return CORE.data.setdefault(
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY, RegistrationCounts()
)
# Slot counters sizing the tracker's StaticVector storage; one request per
# registered listener, client, or scanner state listener.
_request_listener_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT")
_request_client_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT")
_request_scanner_state_listener_slot = cg.slot_counter(
"ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT"
)
def register_ble_features(features: set[BLEFeatures]) -> None:
@@ -239,17 +232,15 @@ async def to_code(config):
):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
registration_counts = _get_registration_counts()
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
registration_counts.listeners += 1
_request_listener_slot()
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
registration_counts.listeners += 1
_request_listener_slot()
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
@@ -262,7 +253,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
registration_counts.listeners += 1
_request_listener_slot()
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
@@ -275,7 +266,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_SCAN_END, []):
registration_counts.listeners += 1
_request_listener_slot()
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -316,18 +307,6 @@ async def _add_ble_features():
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
# Add defines for StaticVector sizing based on registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
registration_counts = _get_registration_counts()
if registration_counts.listeners > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", registration_counts.listeners
)
if registration_counts.clients > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", registration_counts.clients
)
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
{
@@ -380,7 +359,7 @@ async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_get_registration_counts().listeners += 1
_request_listener_slot()
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -388,7 +367,7 @@ async def register_ble_device(
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_get_registration_counts().clients += 1
_request_client_slot()
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
@@ -402,7 +381,7 @@ async def register_raw_ble_device(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_get_registration_counts().listeners += 1
_request_listener_slot()
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -416,7 +395,21 @@ async def register_raw_client(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_get_registration_counts().clients += 1
_request_client_slot()
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
async def register_scanner_state_listener(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
"""Register a listener for scanner state changes.
The slot request here is what sizes the tracker's listener storage; a
build with no registrations compiles the storage out entirely.
"""
_request_scanner_state_listener_slot()
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.add_scanner_state_listener(var))
return var
@@ -3,6 +3,8 @@
#include "esphome/core/automation.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <vector>
#ifdef USE_ESP32
namespace esphome::esp32_ble_tracker {
@@ -443,9 +443,11 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state;
this->state_version_++;
#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT
for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state);
}
#endif
}
void ESP32BLETracker::dump_config() {
@@ -8,7 +8,6 @@
#include <array>
#include <span>
#include <string>
#include <vector>
#ifdef USE_ESP32
@@ -265,10 +264,15 @@ class ESP32BLETracker final : public Component,
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
/// Add a listener for scanner state changes
#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT
/// Add a listener for scanner state changes. Only compiled when a consumer
/// requested a slot in codegen: register through
/// esp32_ble_tracker.register_scanner_state_listener() in your component's
/// to_code, which requests the slot and emits this call.
void add_scanner_state_listener(BLEScannerStateListener *listener) {
this->scanner_state_listeners_.push_back(listener);
}
#endif
ScannerState get_scanner_state() const { return this->scanner_state_; }
protected:
@@ -335,7 +339,10 @@ class ESP32BLETracker final : public Component,
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
#endif
std::vector<BLEScannerStateListener *> scanner_state_listeners_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT
StaticVector<BLEScannerStateListener *, ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT>
scanner_state_listeners_;
#endif
// Parsed listeners registered through the neutral BLEHub contract (migrated
// sensors); dispatched alongside listeners_.
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
+1
View File
@@ -278,6 +278,7 @@
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT 1
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
#define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2
#define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1
@@ -0,0 +1,16 @@
esphome:
name: slotcount-esp32-proxy
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
password: password1
api:
bluetooth_proxy:
active: true
@@ -0,0 +1,9 @@
esphome:
name: slotcount-esp32-tracker
esp32:
board: esp32dev
framework:
type: esp-idf
esp32_ble_tracker:
@@ -81,3 +81,55 @@ def test_neutral_listener_count_emitted_when_requested() -> None:
)
CORE.flush_tasks()
assert get_define_value(ble_device_base.LISTENER_COUNT_DEFINE) == "1"
def test_esp32_tracker_handler_counts(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A bare tracker registers its four esp32_ble handlers and nothing else."""
generate_main(component_config_path("esp32_tracker_only.yaml"))
assert get_define_value("ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT") == "1"
assert get_define_value("ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT") == "1"
assert get_define_value("ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT") == "1"
assert get_define_value("ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT") == "1"
assert get_define_value("ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT") is None
# No consumer subscribed to scanner state, so the storage compiles out.
assert (
get_define_value("ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT")
is None
)
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") is None
def test_esp32_bluetooth_proxy_requests_scanner_state_slot(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The proxy requests one scanner state slot, one raw listener slot and a
client slot per connection (three by default with active: true)."""
generate_main(component_config_path("esp32_bluetooth_proxy.yaml"))
assert (
get_define_value("ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT")
== "1"
)
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") == "1"
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") == "3"
def test_counts_reset_between_compiles(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A second compile in the same process starts from zero.
The module level counters this change removes leaked across compiles in a
long lived host process (dashboard, device-builder), growing the handler
counts by one per compile and oversizing the StaticCallbackManager storage.
"""
generate_main(component_config_path("esp32_tracker_only.yaml"))
assert get_define_value("ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT") == "1"
CORE.reset()
generate_main(component_config_path("esp32_tracker_only.yaml"))
assert get_define_value("ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT") == "1"