diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py index 8c218c0954..191f833505 100644 --- a/esphome/components/bluetooth_connection/__init__.py +++ b/esphome/components/bluetooth_connection/__init__.py @@ -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) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp index dc77d448a5..ebf579033b 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp @@ -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(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; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h index df43ebd66d..492c1e071a 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h @@ -104,9 +104,10 @@ class RP2GattClient final : public Component, public Parented 1) + +#include + +#include + +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(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(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 diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index b1c684fcc3..3a1ce7ff9d 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -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]) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bfb019d7ae..fdcfa2aa8a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -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 diff --git a/tests/component_tests/bluetooth_proxy/config/rp2_proxy_default.yaml b/tests/component_tests/bluetooth_proxy/config/rp2_proxy_default.yaml new file mode 100644 index 0000000000..93c769283f --- /dev/null +++ b/tests/component_tests/bluetooth_proxy/config/rp2_proxy_default.yaml @@ -0,0 +1,15 @@ +esphome: + name: poolwrap-rp2-default + +rp2: + board: rpipicow + +wifi: + ssid: MySSID + password: password1 + +api: + +rp2_ble_tracker: + +bluetooth_proxy: diff --git a/tests/component_tests/bluetooth_proxy/config/rp2_proxy_single_slot.yaml b/tests/component_tests/bluetooth_proxy/config/rp2_proxy_single_slot.yaml new file mode 100644 index 0000000000..4e9c94df59 --- /dev/null +++ b/tests/component_tests/bluetooth_proxy/config/rp2_proxy_single_slot.yaml @@ -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 diff --git a/tests/component_tests/bluetooth_proxy/test_platform_gates.py b/tests/component_tests/bluetooth_proxy/test_platform_gates.py index a47dfd53fa..a9e33496c3 100644 --- a/tests/component_tests/bluetooth_proxy/test_platform_gates.py +++ b/tests/component_tests/bluetooth_proxy/test_platform_gates.py @@ -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. diff --git a/tests/component_tests/bluetooth_proxy/test_rp2_pool_wrap.py b/tests/component_tests/bluetooth_proxy/test_rp2_pool_wrap.py new file mode 100644 index 0000000000..694277c952 --- /dev/null +++ b/tests/component_tests/bluetooth_proxy/test_rp2_pool_wrap.py @@ -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" diff --git a/tests/components/bluetooth_connection/validate.rp2040-ard.yaml b/tests/components/bluetooth_connection/validate.rp2040-ard.yaml index 620aaa177b..7c523b3fba 100644 --- a/tests/components/bluetooth_connection/validate.rp2040-ard.yaml +++ b/tests/components/bluetooth_connection/validate.rp2040-ard.yaml @@ -8,4 +8,4 @@ rp2_ble_tracker: bluetooth_proxy: active: true - connection_slots: 1 + connection_slots: 3 diff --git a/tests/components/bluetooth_proxy/test.rp2040-ard.yaml b/tests/components/bluetooth_proxy/test.rp2040-ard.yaml index e219c7542d..f39142a7c3 100644 --- a/tests/components/bluetooth_proxy/test.rp2040-ard.yaml +++ b/tests/components/bluetooth_proxy/test.rp2040-ard.yaml @@ -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