From 0429d05859cdb546e48323b40885b6f2fe2037d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Aug 2026 16:56:57 -0500 Subject: [PATCH] Move BTstack pool ownership into rp2040_ble and count connection slots like esp32_ble --- .../bluetooth_connection/__init__.py | 37 ++++------- .../components/bluetooth_proxy/__init__.py | 8 ++- esphome/components/rp2040_ble/__init__.py | 62 ++++++++++++++++++- .../btstack_memory.cpp} | 17 ++--- tests/component_tests/rp2040_ble/__init__.py | 0 .../config/rp2_proxy_default.yaml | 0 .../config/rp2_proxy_single_slot.yaml | 0 .../config/rp2_proxy_two_slots.yaml | 0 .../test_pool_wrap.py} | 4 +- .../bluetooth_proxy/test.rp2350-ard.yaml | 9 --- 10 files changed, 88 insertions(+), 49 deletions(-) rename esphome/components/{bluetooth_connection/btstack_memory_rp2.cpp => rp2040_ble/btstack_memory.cpp} (89%) create mode 100644 tests/component_tests/rp2040_ble/__init__.py rename tests/component_tests/{bluetooth_proxy => rp2040_ble}/config/rp2_proxy_default.yaml (100%) rename tests/component_tests/{bluetooth_proxy => rp2040_ble}/config/rp2_proxy_single_slot.yaml (100%) rename tests/component_tests/{bluetooth_proxy => rp2040_ble}/config/rp2_proxy_two_slots.yaml (100%) rename tests/component_tests/{bluetooth_proxy/test_rp2_pool_wrap.py => rp2040_ble/test_pool_wrap.py} (92%) delete mode 100644 tests/components/bluetooth_proxy/test.rp2350-ard.yaml diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py index 38c9649aa1..02c4a3989c 100644 --- a/esphome/components/bluetooth_connection/__init__.py +++ b/esphome/components/bluetooth_connection/__init__.py @@ -9,6 +9,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass import esphome.codegen as cg +from esphome.components import rp2040_ble from esphome.config_helpers import ( filter_source_files_from_platform, frameworks_for_platforms, @@ -37,11 +38,11 @@ CODEOWNERS = ["@bdraco", "@jesserockz"] bluetooth_connection_ns = cg.esphome_ns.namespace("bluetooth_connection") # arduino-pico's prebuilt BTstack is compiled with MAX_NR_GATT_CLIENTS 1 and -# MAX_NR_HCI_CONNECTIONS 2; for more than one backend, btstack_memory_rp2.cpp -# replaces those pools via linker --wrap (emitted by _rp2_register), sized -# from ESPHOME_BLE_GATT_CLIENT_COUNT. 3 matches the esp32 default and stays -# within the controller's resources (MAX_NR_CONTROLLER_ACL_BUFFERS 3). -RP2_MAX_CONNECTIONS = 3 +# MAX_NR_HCI_CONNECTIONS 2; for more than one backend, rp2040_ble's +# btstack_memory.cpp replaces those pools via linker --wrap (requested by +# _rp2_register), sized from ESPHOME_BLE_GATT_CLIENT_COUNT. The cap itself +# belongs to the platform stack that owns the pools. +RP2_MAX_CONNECTIONS = rp2040_ble.MAX_CONNECTIONS # Slot limits for the hub platforms running the connection-capable proxy; # the backend registry itself is _PLATFORM_BACKENDS below. @@ -58,16 +59,6 @@ CONF_BACKEND_ID = "backend_id" DOMAIN = "bluetooth_connection" -# The four btstack_memory accessors whose static pools are baked into the -# prebuilt liblwip-bt.a; every internal use crosses an object boundary in the -# archive, so --wrap intercepts them all (see btstack_memory_rp2.cpp). -_RP2_BTSTACK_POOL_SYMBOLS = ( - "btstack_memory_gatt_client_get", - "btstack_memory_gatt_client_free", - "btstack_memory_hci_connection_get", - "btstack_memory_hci_connection_free", -) - @dataclass class _ConnectionData: @@ -87,8 +78,6 @@ def _esp32_schema_fragment() -> cv.Schema: def _rp2_schema_fragment() -> cv.Schema: - from esphome.components import rp2040_ble - return cv.Schema( {cv.GenerateID(rp2040_ble.CONF_RP2040_BLE_ID): cv.use_id(rp2040_ble.RP2040BLE)} ) @@ -103,18 +92,15 @@ async def _esp32_register(backend: cg.MockObj, config: ConfigType) -> None: async def _rp2_register(backend: cg.MockObj, config: ConfigType) -> None: - from esphome.components import rp2040_ble - # More than one backend outgrows the prebuilt BTstack pools: swap them for - # the ESPHOME_BLE_GATT_CLIENT_COUNT-sized ones in btstack_memory_rp2.cpp. - # Keyed to backend registrations (the same event that grows the count that - # sizes the pools), so single-backend builds emit no flags and stay - # byte-identical to previous releases. + # the ESPHOME_BLE_GATT_CLIENT_COUNT-sized ones in rp2040_ble's + # btstack_memory.cpp. Keyed to backend registrations (the same event that + # grows the count that sizes the pools), so single-backend builds emit no + # flags and stay byte-identical to previous releases. data = _get_data() data.rp2_backend_count += 1 if data.rp2_backend_count == 2: - for symbol in _RP2_BTSTACK_POOL_SYMBOLS: - cg.add_build_flag(f"-Wl,--wrap={symbol}") + rp2040_ble.add_btstack_pool_overrides() await cg.register_parented(backend, config[rp2040_ble.CONF_RP2040_BLE_ID]) @@ -205,7 +191,6 @@ SOURCE_FILE_FRAMEWORKS: dict[str, set[PlatformFramework]] = { PlatformFramework.ESP32_IDF, }, "bluetooth_connection_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, - "btstack_memory_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, } FILTER_SOURCE_FILES = filter_source_files_from_platform(SOURCE_FILE_FRAMEWORKS) diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 6a1c874170..3d0d706a24 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -157,15 +157,17 @@ def _rp2_config_schema() -> cv.All: connection_schema = bluetooth_connection.hub_connection_schema(PLATFORM_RP2) def populate_connections(config: ConfigType) -> ConfigType: + from esphome.components import rp2040_ble + # One wrapper + backend pair per slot, declared during validation so # their ids exist for codegen (the esp32 arm's `connections` pattern). if not config[CONF_ACTIVE]: return config + connection_slots: int = config[CONF_CONNECTION_SLOTS] + rp2040_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config) return { **config, - CONF_CONNECTIONS: [ - connection_schema({}) for _ in range(config[CONF_CONNECTION_SLOTS]) - ], + CONF_CONNECTIONS: [connection_schema({}) for _ in range(connection_slots)], } max_conn = bluetooth_connection.HUB_MAX_CONNECTIONS[PLATFORM_RP2] diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index e49dceb000..9ab3da554c 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -1,6 +1,9 @@ +from collections.abc import Callable, MutableMapping + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID +from esphome.core import CORE from esphome.types import ConfigType DEPENDENCIES = ["rp2"] @@ -8,6 +11,15 @@ CODEOWNERS = ["@bdraco"] CONF_RP2040_BLE_ID = "rp2040_ble_id" +KEY_RP2040_BLE = "rp2040_ble" +KEY_USED_CONNECTION_SLOTS = "used_connection_slots" + +# Hard platform cap on concurrent GATT connections: the BTstack pool overrides +# in btstack_memory.cpp are sized from ESPHOME_BLE_GATT_CLIENT_COUNT with this +# as the ceiling. 3 matches the esp32 default and stays within the +# controller's resources (MAX_NR_CONTROLLER_ACL_BUFFERS 3). +MAX_CONNECTIONS = 3 + rp2040_ble_ns = cg.esphome_ns.namespace("rp2040_ble") RP2040BLE = rp2040_ble_ns.class_("RP2040BLE", cg.Component) @@ -30,13 +42,61 @@ def _validate_board(config: ConfigType) -> ConfigType: return config -FINAL_VALIDATE_SCHEMA = _validate_board +def consume_connection_slots( + value: int, consumer: str +) -> Callable[[MutableMapping], MutableMapping]: + """Reserve BLE connection slots for a component (the esp32_ble pattern); + the total is checked against MAX_CONNECTIONS in final validation.""" + + def _consume_connection_slots(config: MutableMapping) -> MutableMapping: + data: dict = CORE.data.setdefault(KEY_RP2040_BLE, {}) + slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) + slots.extend([consumer] * value) + return config + + return _consume_connection_slots + + +def _final_validate(config: ConfigType) -> ConfigType: + _validate_board(config) + # Skip in testing mode to allow component grouping (esp32_ble parity). + if not CORE.testing_mode: + used = CORE.data.get(KEY_RP2040_BLE, {}).get(KEY_USED_CONNECTION_SLOTS, []) + if len(used) > MAX_CONNECTIONS: + raise cv.Invalid( + f"BLE components require {len(used)} connection slots but the " + f"rp2 maximum is {MAX_CONNECTIONS}. " + f"Components: {', '.join(used)}" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate # Once per registered scan listener; sizes the controller's StaticVector # listener storage. request_scan_listener_slot = cg.slot_counter("RP2040_BLE_SCAN_LISTENER_COUNT") +# The four btstack_memory accessors whose static pools are baked into the +# prebuilt liblwip-bt.a; every internal use crosses an object boundary in the +# archive, so --wrap intercepts them all (see btstack_memory.cpp). +_BTSTACK_POOL_SYMBOLS = ( + "btstack_memory_gatt_client_get", + "btstack_memory_gatt_client_free", + "btstack_memory_hci_connection_get", + "btstack_memory_hci_connection_free", +) + + +def add_btstack_pool_overrides() -> None: + """Emit the --wrap flags that swap the prebuilt BTstack pools for the + ESPHOME_BLE_GATT_CLIENT_COUNT-sized ones in btstack_memory.cpp. Called by + bluetooth_connection when a second GATT backend registers; idempotent + (build flags are a set).""" + for symbol in _BTSTACK_POOL_SYMBOLS: + cg.add_build_flag(f"-Wl,--wrap={symbol}") + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/bluetooth_connection/btstack_memory_rp2.cpp b/esphome/components/rp2040_ble/btstack_memory.cpp similarity index 89% rename from esphome/components/bluetooth_connection/btstack_memory_rp2.cpp rename to esphome/components/rp2040_ble/btstack_memory.cpp index cc6c633222..8af57924a2 100644 --- a/esphome/components/bluetooth_connection/btstack_memory_rp2.cpp +++ b/esphome/components/rp2040_ble/btstack_memory.cpp @@ -1,12 +1,13 @@ // Replaces the gatt_client / hci_connection static pools baked into // arduino-pico's prebuilt liblwip-bt.a (built with MAX_NR_GATT_CLIENTS 1, // MAX_NR_HCI_CONNECTIONS 2) with pools sized from ESPHOME_BLE_GATT_CLIENT_COUNT. -// bluetooth_connection/__init__.py emits the matching -Wl,--wrap flags only -// when more than one backend registers; single-backend builds emit no flags -// and this file compiles to nothing, leaving the prebuilt pools in charge. -// Layout safety: the framework defines ENABLE_CLASSIC / ENABLE_BLE for -// every user TU whenever PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH is set -// (rp2040_ble always sets it), so sizeof() here matches the archive. +// add_btstack_pool_overrides() in this component's codegen emits the matching +// -Wl,--wrap flags, requested by bluetooth_connection when more than one GATT +// backend registers; single-backend builds emit no flags and this file +// compiles to nothing, leaving the prebuilt pools in charge. Layout safety: +// the framework defines ENABLE_CLASSIC / ENABLE_BLE for every user TU +// whenever PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH is set (this component +// always sets it), so sizeof() here matches the archive. #include "esphome/core/defines.h" @@ -16,7 +17,7 @@ #include -namespace esphome::bluetooth_connection { +namespace esphome::rp2040_ble { namespace { // Pinned against arduino-pico 6.0.0's prebuilt archives: a framework bump (or @@ -112,6 +113,6 @@ void __wrap_btstack_memory_hci_connection_free(hci_connection_t *hci_connection) } // extern "C" // NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) -} // namespace esphome::bluetooth_connection +} // namespace esphome::rp2040_ble #endif // USE_RP2040_BLE && USE_BLE_GATT_CLIENT && ESPHOME_BLE_GATT_CLIENT_COUNT > 1 diff --git a/tests/component_tests/rp2040_ble/__init__.py b/tests/component_tests/rp2040_ble/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/bluetooth_proxy/config/rp2_proxy_default.yaml b/tests/component_tests/rp2040_ble/config/rp2_proxy_default.yaml similarity index 100% rename from tests/component_tests/bluetooth_proxy/config/rp2_proxy_default.yaml rename to tests/component_tests/rp2040_ble/config/rp2_proxy_default.yaml diff --git a/tests/component_tests/bluetooth_proxy/config/rp2_proxy_single_slot.yaml b/tests/component_tests/rp2040_ble/config/rp2_proxy_single_slot.yaml similarity index 100% rename from tests/component_tests/bluetooth_proxy/config/rp2_proxy_single_slot.yaml rename to tests/component_tests/rp2040_ble/config/rp2_proxy_single_slot.yaml diff --git a/tests/component_tests/bluetooth_proxy/config/rp2_proxy_two_slots.yaml b/tests/component_tests/rp2040_ble/config/rp2_proxy_two_slots.yaml similarity index 100% rename from tests/component_tests/bluetooth_proxy/config/rp2_proxy_two_slots.yaml rename to tests/component_tests/rp2040_ble/config/rp2_proxy_two_slots.yaml diff --git a/tests/component_tests/bluetooth_proxy/test_rp2_pool_wrap.py b/tests/component_tests/rp2040_ble/test_pool_wrap.py similarity index 92% rename from tests/component_tests/bluetooth_proxy/test_rp2_pool_wrap.py rename to tests/component_tests/rp2040_ble/test_pool_wrap.py index 7ccf3c82a3..6cbcc03e3f 100644 --- a/tests/component_tests/bluetooth_proxy/test_rp2_pool_wrap.py +++ b/tests/component_tests/rp2040_ble/test_pool_wrap.py @@ -7,13 +7,13 @@ from __future__ import annotations from collections.abc import Callable from pathlib import Path -from esphome.components import bluetooth_connection +from esphome.components import rp2040_ble from esphome.core import CORE from ..helpers import get_define_value WRAP_FLAGS = tuple( - f"-Wl,--wrap={symbol}" for symbol in bluetooth_connection._RP2_BTSTACK_POOL_SYMBOLS + f"-Wl,--wrap={symbol}" for symbol in rp2040_ble._BTSTACK_POOL_SYMBOLS ) diff --git a/tests/components/bluetooth_proxy/test.rp2350-ard.yaml b/tests/components/bluetooth_proxy/test.rp2350-ard.yaml deleted file mode 100644 index 1abc62cedb..0000000000 --- a/tests/components/bluetooth_proxy/test.rp2350-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Pico 2 W build of the full proxy: links the rp2350 framework archive, so -# the pool --wrap overrides and their per-architecture layout asserts are -# exercised for this chip too (see test.rp2040-ard.yaml for the slot shape). -packages: - common: !include common.yaml - -rp2_ble_tracker: - -bluetooth_proxy: