[bluetooth_connection] Support 3 connection slots on rp2

This commit is contained in:
J. Nick Koston
2026-08-10 15:12:30 -05:00
parent f3d1fc0d64
commit ff56ba16bd
12 changed files with 320 additions and 52 deletions
@@ -36,9 +36,12 @@ 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;
# raising this needs an upstream change (the layer itself supports N).
RP2_MAX_CONNECTIONS = 1
# arduino-pico's prebuilt BTstack is compiled with MAX_NR_GATT_CLIENTS 1 and
# MAX_NR_HCI_CONNECTIONS 2; for more than one slot, btstack_memory_rp2.cpp
# replaces those pools via linker --wrap (add_btstack_pool_overrides), 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
# Slot limits for the hub platforms running the connection-capable proxy;
# the backend registry itself is _PLATFORM_BACKENDS below.
@@ -53,6 +56,27 @@ BluedroidGattClient = bluetooth_connection_ns.class_(
CONF_BACKEND_ID = "backend_id"
# 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",
)
def add_btstack_pool_overrides(slot_count: int) -> None:
"""Emit the --wrap flags that swap the prebuilt BTstack pools for the
ESPHOME_BLE_GATT_CLIENT_COUNT-sized ones in btstack_memory_rp2.cpp.
No-op off rp2 and for single-slot builds, which fit the prebuilt pools
(and stay byte-identical to previous releases)."""
if not CORE.is_rp2 or slot_count <= 1:
return
for symbol in _RP2_BTSTACK_POOL_SYMBOLS:
cg.add_build_flag(f"-Wl,--wrap={symbol}")
def _esp32_schema_fragment() -> cv.Schema:
from esphome.components import esp32_ble_tracker
@@ -168,6 +192,7 @@ 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)
@@ -53,6 +53,7 @@ RP2GattClient *RP2GattClient::instances[ESPHOME_BLE_GATT_CLIENT_COUNT] = {};
uint8_t RP2GattClient::instance_count = 0;
btstack_packet_callback_registration_t RP2GattClient::hci_event_registration = {};
btstack_packet_callback_registration_t RP2GattClient::sm_event_registration = {};
RP2GattClient *RP2GattClient::connect_owner = nullptr;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static ESPBTUUID uuid_from_btstack(uint16_t uuid16, const uint8_t uuid128[16]) {
@@ -127,10 +128,19 @@ void RP2GattClient::hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *
gap_subevent_le_connection_complete_get_peer_address(packet, peer);
uint8_t status = gap_subevent_le_connection_complete_get_status(packet);
hci_con_handle_t con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
// The stack-wide create-connection resolved either way (success, failure
// or cancel completion); a pending engine's loop() may issue the next.
connect_owner = nullptr;
// Route to the engine that is waiting for this peer.
for (uint8_t i = 0; i < instance_count; i++) {
RP2GattClient *inst = instances[i];
if (inst->state_ == EngineState::CONNECTING && memcmp(inst->peer_addr_, peer, sizeof(bd_addr_t)) == 0) {
if (status == 0) {
// Stamp the handle here in the BTstack context: a disconnection
// racing the queued CONNECTED event arrives in this same context
// and must route by handle (it carries no address).
inst->con_handle_ = con_handle;
}
inst->enqueue_event_irq_(RP2GattEvent::CONNECTED, status, con_handle);
break;
}
@@ -138,19 +148,9 @@ void RP2GattClient::hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *
break;
}
case HCI_EVENT_DISCONNECTION_COMPLETE: {
hci_con_handle_t con_handle = hci_event_disconnection_complete_get_connection_handle(packet);
RP2GattClient *inst = instance_for_con_handle(con_handle);
if (inst == nullptr && instance_count == 1) {
// The main loop may not have recorded the handle yet (the CONNECTED
// event is still queued); with a single engine the connecting
// instance is unambiguous, so route there to close the
// accept-then-drop window. With multiple engines the event has no
// address to match on, so it must be dropped instead of guessed.
RP2GattClient *candidate = instances[0];
if (candidate->con_handle_ == HCI_CON_HANDLE_INVALID && candidate->state_ != EngineState::IDLE) {
inst = candidate;
}
}
// Routable even against a still-queued CONNECTED event: the handle is
// stamped in this context at connection-complete time.
RP2GattClient *inst = instance_for_con_handle(hci_event_disconnection_complete_get_connection_handle(packet));
if (inst != nullptr) {
inst->enqueue_event_irq_(RP2GattEvent::DISCONNECTED, hci_event_disconnection_complete_get_reason(packet), 0);
}
@@ -400,30 +400,48 @@ void RP2GattClient::loop() {
ESP_LOGW(TAG, "Dropped %u GATT notifications (queue full)", notify_dropped);
}
if (this->state_ == EngineState::CONNECTING || this->state_ == EngineState::MTU_EXCHANGE) {
if (this->state_ == EngineState::CONNECT_PENDING) {
if (millis() - this->connect_started_ > CONNECT_TIMEOUT_MS) {
// Never reached the radio; nothing stack-side to cancel.
ESP_LOGW(TAG, "Connect timeout (queued)");
this->fail_connection_(HCI_REASON_CONNECTION_TIMEOUT);
} else if (int err = this->try_gap_connect_(); err != 0) {
ESP_LOGW(TAG, "gap_connect failed, status=0x%02x", err);
this->fail_connection_(static_cast<uint8_t>(err));
}
} else if (this->state_ == EngineState::CONNECTING || this->state_ == EngineState::MTU_EXCHANGE) {
uint32_t now = millis();
if (now - this->connect_started_ > CONNECT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Connect timeout");
if (this->state_ == EngineState::CONNECTING && this->con_handle_ == HCI_CON_HANDLE_INVALID) {
bool cancel_sent = false;
if (!this->connect_cancel_attempted_) {
this->connect_cancel_attempted_ = true;
BluetoothLock lock;
gap_connect_cancel();
// gap_connect_cancel is stack-global; only the engine whose
// create-connection is in flight may issue it.
if (connect_owner == this) {
gap_connect_cancel();
cancel_sent = true;
}
}
if (cancel_sent) {
// The cancel produces a connection-complete event with a failure
// status, which drives the normal failure path; restart the timer
// so a lost event escalates below instead of wedging here.
this->connect_started_ = now;
} else {
// The cancel's completion never arrived: reclaim the slot and the
// scan rather than cancelling forever.
// Not the owner (the completion resolved but its event was
// dropped), or the cancel's completion never arrived: reclaim the
// slot and the scan rather than cancelling forever.
this->fail_connection_(HCI_REASON_CONNECTION_TIMEOUT);
}
} else {
// The link is up (MTU exchange stalled): tear it down properly so the
// controller frees its side; the DISCONNECTING safety net below
// reclaims state if the disconnection event is lost. Dropping engine
// state without gap_disconnect would leak the live link and the
// single GATT slot for the rest of the boot.
// state without gap_disconnect would leak the live link and this
// engine's GATT slot for the rest of the boot.
this->gatt_disconnect();
}
}
@@ -563,6 +581,15 @@ void RP2GattClient::release_scan_inhibit_() {
}
void RP2GattClient::fail_connection_(uint8_t reason) {
{
// Timeout escalation can fire with the completion event lost; release the
// stack-wide connect slot so pending engines can proceed (a still-busy
// stack answers them with DISALLOWED and they keep pending).
BluetoothLock lock;
if (connect_owner == this) {
connect_owner = nullptr;
}
}
this->cleanup_link_state_();
this->release_scan_inhibit_();
this->state_ = EngineState::IDLE;
@@ -838,22 +865,48 @@ int RP2GattClient::connect(uint64_t address, uint8_t addr_type) {
this->parent_->inhibit_scan();
this->connect_cancel_attempted_ = false;
this->cancel_requested_ = false;
// Bounds the queued wait; restarted when gap_connect is accepted so the
// radio attempt gets its full budget (HA's own ~20 s timeout arbitrates the
// sum via a disconnect request).
this->connect_started_ = millis();
if (int err = this->try_gap_connect_(); err != 0) {
ESP_LOGW(TAG, "gap_connect failed, status=0x%02x", err);
this->release_scan_inhibit_();
return err;
}
this->enable_loop();
return 0;
}
// One outgoing LE create-connection exists stack-wide: issue it if no other
// engine owns it, otherwise park in CONNECT_PENDING for loop() to retry.
// Returns nonzero only for hard failures (state untouched; caller cleans up).
int RP2GattClient::try_gap_connect_() {
uint8_t status;
{
BluetoothLock lock;
gap_set_connection_parameters(CONN_SCAN_INTERVAL, CONN_SCAN_WINDOW, FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL,
0, FAST_CONN_TIMEOUT, CONN_CE_MIN, CONN_CE_MAX);
status = gap_connect(this->peer_addr_, this->peer_addr_type_);
if (connect_owner != nullptr) {
status = ERROR_CODE_COMMAND_DISALLOWED;
} else {
gap_set_connection_parameters(CONN_SCAN_INTERVAL, CONN_SCAN_WINDOW, FAST_MIN_CONN_INTERVAL,
FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, CONN_CE_MIN, CONN_CE_MAX);
status = gap_connect(this->peer_addr_, this->peer_addr_type_);
if (status == 0) {
connect_owner = this;
}
}
}
if (status != 0) {
ESP_LOGW(TAG, "gap_connect failed, status=0x%02x", status);
this->release_scan_inhibit_();
return status;
if (status == 0) {
this->state_ = EngineState::CONNECTING;
this->connect_started_ = millis();
return 0;
}
this->state_ = EngineState::CONNECTING;
this->connect_started_ = millis();
this->enable_loop();
return 0;
if (status == ERROR_CODE_COMMAND_DISALLOWED) {
// Radio busy with another engine's connect; resolved from loop().
this->state_ = EngineState::CONNECT_PENDING;
return 0;
}
return status;
}
int RP2GattClient::gatt_disconnect() {
@@ -862,6 +915,19 @@ int RP2GattClient::gatt_disconnect() {
return GATT_ERR_NOT_CONNECTED;
case EngineState::DISCONNECTING:
return 0; // already on its way down
case EngineState::CONNECT_PENDING: {
// Nothing issued stack-side; complete via the queue like a refused
// gap_disconnect so the listener cannot re-enter disconnect mid-call.
{
BluetoothLock lock;
this->enqueue_event_irq_(RP2GattEvent::DISCONNECTED, HCI_REASON_CONNECTION_TIMEOUT, 0);
}
this->state_ = EngineState::DISCONNECTING;
this->disconnecting_started_ = millis();
this->release_scan_inhibit_();
this->enable_loop();
return 0;
}
case EngineState::CONNECTING: {
if (this->con_handle_ == HCI_CON_HANDLE_INVALID) {
// The cancel can lose the race against a successful connection
@@ -871,8 +937,12 @@ int RP2GattClient::gatt_disconnect() {
this->cancel_requested_ = true;
this->connect_cancel_attempted_ = true;
BluetoothLock lock;
gap_connect_cancel();
// Completion arrives as a failed connection-complete event.
// Owner: the cancel completes as a failed connection-complete. Not
// the owner (completion already resolved in the BTstack context): the
// queued event drives the same teardown, nothing to cancel.
if (connect_owner == this) {
gap_connect_cancel();
}
return 0;
}
break;
@@ -104,9 +104,10 @@ class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040
// the link stays READY while they run.
enum class EngineState : uint8_t {
IDLE,
CONNECTING, // gap_connect issued, waiting for connection complete
MTU_EXCHANGE, // link up, waiting for GATT_EVENT_MTU
READY, // on_connection_state(true) delivered
CONNECT_PENDING, // queued: another engine owns the stack-wide create-connection
CONNECTING, // gap_connect issued, waiting for connection complete
MTU_EXCHANGE, // link up, waiting for GATT_EVENT_MTU
READY, // on_connection_state(true) delivered
DISCONNECTING,
};
@@ -143,6 +144,7 @@ class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040
int issue_descriptor_query_(uint16_t char_index);
void finish_discovery_(int error);
void fail_connection_(uint8_t reason);
int try_gap_connect_();
void cleanup_link_state_();
bool notify_subscribed_(uint16_t handle) const;
static void can_write_no_rsp_trampoline(void *context);
@@ -214,6 +216,12 @@ class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040
static btstack_packet_callback_registration_t hci_event_registration;
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static btstack_packet_callback_registration_t sm_event_registration;
// The engine whose gap_connect is in flight: BTstack allows one outgoing LE
// create-connection stack-wide, and gap_connect_cancel is global, so only
// the owner may cancel. Written under BluetoothLock from the main loop,
// cleared in the BTstack context when the procedure resolves.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static RP2GattClient *connect_owner;
};
} // namespace esphome::bluetooth_connection
@@ -0,0 +1,89 @@
// 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 connection slot is configured; single-slot 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.
#include "esphome/core/defines.h"
#if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT) && (ESPHOME_BLE_GATT_CLIENT_COUNT > 1)
#include <btstack.h>
#include <cstring>
namespace esphome::bluetooth_connection {
namespace {
// One gatt_client_t per configured connection slot.
constexpr int GATT_CLIENT_POOL_SIZE = ESPHOME_BLE_GATT_CLIENT_COUNT;
// An hci_connection_t is held from gap_connect() to DISCONNECTION_COMPLETE
// (scanning holds none); +1 mirrors the prebuilt library's own headroom
// (2 connections for 1 GATT client) so a teardown/re-connect overlap can
// never starve a slot.
constexpr int HCI_CONNECTION_POOL_SIZE = ESPHOME_BLE_GATT_CLIENT_COUNT + 1;
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
gatt_client_t gatt_client_storage[GATT_CLIENT_POOL_SIZE];
btstack_memory_pool_t gatt_client_pool;
hci_connection_t hci_connection_storage[HCI_CONNECTION_POOL_SIZE];
btstack_memory_pool_t hci_connection_pool;
bool pools_ready = false;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
// Lazy one-time init instead of a global constructor: every caller (hci.c /
// gatt_client.c inside the prebuilt archive) runs in BTstack's single
// serialized context, so a plain flag is race-free and no static-init-order
// hazard exists.
void ensure_pools() {
if (pools_ready)
return;
btstack_memory_pool_create(&gatt_client_pool, gatt_client_storage, GATT_CLIENT_POOL_SIZE, sizeof(gatt_client_t));
btstack_memory_pool_create(&hci_connection_pool, hci_connection_storage, HCI_CONNECTION_POOL_SIZE,
sizeof(hci_connection_t));
pools_ready = true;
}
} // namespace
// Exact semantics of btstack_memory.c's static-pool arm: zeroed block on
// success, NULL when exhausted; free returns the block to the pool.
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
gatt_client_t *__wrap_btstack_memory_gatt_client_get(void) {
ensure_pools();
void *buffer = btstack_memory_pool_get(&gatt_client_pool);
if (buffer != nullptr) {
memset(buffer, 0, sizeof(gatt_client_t));
}
return static_cast<gatt_client_t *>(buffer);
}
void __wrap_btstack_memory_gatt_client_free(gatt_client_t *gatt_client) {
btstack_memory_pool_free(&gatt_client_pool, gatt_client);
}
hci_connection_t *__wrap_btstack_memory_hci_connection_get(void) {
ensure_pools();
void *buffer = btstack_memory_pool_get(&hci_connection_pool);
if (buffer != nullptr) {
memset(buffer, 0, sizeof(hci_connection_t));
}
return static_cast<hci_connection_t *>(buffer);
}
void __wrap_btstack_memory_hci_connection_free(hci_connection_t *hci_connection) {
btstack_memory_pool_free(&hci_connection_pool, hci_connection);
}
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
} // namespace esphome::bluetooth_connection
#endif // USE_RP2040_BLE && USE_BLE_GATT_CLIENT && ESPHOME_BLE_GATT_CLIENT_COUNT > 1
@@ -152,8 +152,9 @@ def _validate_no_active(config: ConfigType) -> ConfigType:
@functools.cache
def _rp2_config_schema() -> cv.All:
"""Full proxy on the rp2 BLE hub: active connections through the BTstack
GATT client backend in bluetooth_connection. The slot limit comes from the
prebuilt BTstack library (one connection today); the code is built for N."""
GATT client backend in bluetooth_connection. Multi-slot builds replace the
prebuilt library's one-client BTstack pools via linker --wrap
(bluetooth_connection.add_btstack_pool_overrides)."""
connection_schema = bluetooth_connection.hub_connection_schema(PLATFORM_RP2)
def populate_connections(config: ConfigType) -> ConfigType:
@@ -183,8 +184,8 @@ def _rp2_config_schema() -> cv.All:
min=1,
max=max_conn,
msg=f"rp2 supports at most {max_conn} connection slot(s); "
"the framework's BTstack library is built with "
f"MAX_NR_GATT_CLIENTS {max_conn}",
"the BTstack pool overrides in bluetooth_connection "
f"are sized for {max_conn}",
),
),
}
@@ -206,6 +207,8 @@ async def _connections_to_code(var: cg.MockObj, config: ConfigType) -> None:
# this define whenever a proxy is present (zero on advertisement-only
# hubs); sized here so it can never diverge from the loop below.
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", len(connections))
# Multi-slot rp2 builds outgrow the prebuilt BTstack pools; no-op elsewhere.
bluetooth_connection.add_btstack_pool_overrides(len(connections))
for connection_conf in connections:
backend = await bluetooth_connection.new_gatt_backend(connection_conf)
connection = cg.new_Pvariable(connection_conf[CONF_ID])
+3 -3
View File
@@ -254,13 +254,13 @@
#define USE_BLUETOOTH_PROXY
// Mirror the codegen values per platform: _to_code_esp32() emits the connection
// count (default 3) and the scanner-state push slot, _to_code_ble_hub() emits
// the slot count (1 on rp2, 0 on advertisement-only hubs) — so static analysis
// the slot count (3 on rp2, 0 on advertisement-only hubs) — so static analysis
// checks the same instantiations a real build produces.
#ifdef USE_ESP32
#define USE_BLE_SCANNER_STATE_CALLBACK
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
#elif defined(USE_RP2)
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 1
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
#else
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 0
#endif
@@ -473,7 +473,7 @@
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
#define USE_BLE_SCAN_RESPONSE_MERGER
#define USE_BLE_GATT_CLIENT
#define ESPHOME_BLE_GATT_CLIENT_COUNT 1
#define ESPHOME_BLE_GATT_CLIENT_COUNT 3
#define USE_RP2040_VARIANT_RP2040
#define USE_SPI
#ifndef USE_ETHERNET
@@ -0,0 +1,15 @@
esphome:
name: poolwrap-rp2-default
rp2:
board: rpipicow
wifi:
ssid: MySSID
password: password1
api:
rp2_ble_tracker:
bluetooth_proxy:
@@ -0,0 +1,16 @@
esphome:
name: poolwrap-rp2-single
rp2:
board: rpipicow
wifi:
ssid: MySSID
password: password1
api:
rp2_ble_tracker:
bluetooth_proxy:
connection_slots: 1
@@ -139,8 +139,8 @@ def test_rp2_defaults_to_the_full_proxy(
_register_tracker(PLATFORM_RP2)
validated = bluetooth_proxy.CONFIG_SCHEMA({})
assert validated[CONF_ACTIVE] is True
assert validated[bluetooth_proxy.CONF_CONNECTION_SLOTS] == 1
assert len(validated[bluetooth_proxy.CONF_CONNECTIONS]) == 1
assert validated[bluetooth_proxy.CONF_CONNECTION_SLOTS] == 3
assert len(validated[bluetooth_proxy.CONF_CONNECTIONS]) == 3
def test_rp2_accepts_explicit_passive(
@@ -156,11 +156,15 @@ def test_rp2_accepts_explicit_passive(
def test_rp2_rejects_slots_beyond_the_btstack_limit(
set_core_config: SetCoreConfigCallable,
) -> None:
# The prebuilt BTstack library allows exactly one GATT client connection.
# The BTstack pool overrides are sized for RP2_MAX_CONNECTIONS slots.
set_core_config(PlatformFramework.RP2_ARDUINO)
_register_tracker(PLATFORM_RP2)
with pytest.raises(cv.Invalid, match="at most 1 connection slot"):
bluetooth_proxy.CONFIG_SCHEMA({"connection_slots": 2})
with pytest.raises(cv.Invalid, match="at most 3 connection slot"):
bluetooth_proxy.CONFIG_SCHEMA({"connection_slots": 4})
# Fewer slots than the cap stay accepted (the prebuilt single-client pool
# path for 1, the wrap path for 2).
validated = bluetooth_proxy.CONFIG_SCHEMA({"connection_slots": 1})
assert len(validated[bluetooth_proxy.CONF_CONNECTIONS]) == 1
# Values past even the loosest platform cap stop at the outer walkable
# schema, which stays bounded for range walkers (device-builder sync);
# in-range values get the platform message above.
@@ -0,0 +1,37 @@
"""The rp2 BTstack pool overrides: multi-slot builds emit the --wrap flags
that swap the prebuilt single-client pools for the codegen-sized ones;
single-slot builds emit none and stay byte-identical to previous releases."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from esphome.components import bluetooth_connection
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
)
def test_default_slots_emit_the_pool_wrap(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
generate_main(component_config_path("rp2_proxy_default.yaml"))
assert all(flag in CORE.build_flags for flag in WRAP_FLAGS)
assert get_define_value("ESPHOME_BLE_GATT_CLIENT_COUNT") == "3"
assert get_define_value("BLUETOOTH_PROXY_MAX_CONNECTIONS") == "3"
def test_single_slot_keeps_the_prebuilt_pools(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
generate_main(component_config_path("rp2_proxy_single_slot.yaml"))
assert not any(flag in CORE.build_flags for flag in WRAP_FLAGS)
assert get_define_value("ESPHOME_BLE_GATT_CLIENT_COUNT") == "1"
assert get_define_value("BLUETOOTH_PROXY_MAX_CONNECTIONS") == "1"
@@ -8,4 +8,4 @@ rp2_ble_tracker:
bluetooth_proxy:
active: true
connection_slots: 1
connection_slots: 3
@@ -1,5 +1,6 @@
# Full proxy on the rp2 BLE hub: active defaults to true here (esp32 parity),
# so this compiles the BTstack GATT client backend and one connection slot.
# so this compiles the BTstack GATT client backend with the default three
# connection slots, exercising the btstack_memory_rp2.cpp pool --wrap link.
# No explicit ble_hub_id: the generated binding resolves the single declared
# hub, and an inline id here would collide with rp2_ble_tracker's own fixture
# once CI merges both components into one grouped rp2040-ard build (grouped