Move BTstack pool ownership into rp2040_ble and count connection slots like esp32_ble

This commit is contained in:
J. Nick Koston
2026-08-10 16:56:57 -05:00
parent f90027f7c5
commit 0429d05859
10 changed files with 88 additions and 49 deletions
@@ -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)
@@ -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]
+61 -1
View File
@@ -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])
@@ -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 <cstring>
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
@@ -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
)
@@ -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: