mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
[bluetooth_connection] Support 3 connection slots on rp2
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user