From c0e70d9beb09f09c283c38db10c6478a9139047e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Aug 2026 11:50:18 -0500 Subject: [PATCH] [rp2040_ble] Add scan arbitration and GATT client hooks (#18155) --- esphome/components/rp2040_ble/rp2040_ble.cpp | 52 ++++++++++++++++++++ esphome/components/rp2040_ble/rp2040_ble.h | 28 +++++++++-- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/esphome/components/rp2040_ble/rp2040_ble.cpp b/esphome/components/rp2040_ble/rp2040_ble.cpp index 8e7c7d6be58..7dd84d9c316 100644 --- a/esphome/components/rp2040_ble/rp2040_ble.cpp +++ b/esphome/components/rp2040_ble/rp2040_ble.cpp @@ -57,6 +57,17 @@ void RP2040BLE::enable() { l2cap_init(); sm_init(); +#ifdef USE_BLE_GATT_CLIENT + gatt_client_init(); + // The GATT engine kicks the MTU exchange explicitly right after a + // connection completes (auto negotiation would only run on the first + // query, which a with-cache connection never issues). + gatt_client_mtu_enable_auto_negotiation(0); + // Just-works security for peripheral-initiated pairing. + sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); + sm_set_authentication_requirements(SM_AUTHREQ_BONDING); +#endif + this->hci_event_callback_registration_.callback = &RP2040BLE::packet_handler; hci_add_event_handler(&this->hci_event_callback_registration_); @@ -215,6 +226,18 @@ bool RP2040BLE::scan_start(uint16_t interval, uint16_t window, bool active) { // the moment a tracker retries. Callers retry until the stack is up. return false; } +#ifdef USE_BLE_GATT_CLIENT + this->scan_interval_ = interval; + this->scan_window_ = window; + this->scan_active_mode_ = active; + this->scan_desired_ = true; + if (this->scan_inhibit_count_ > 0) { + // A connect attempt owns the radio; the scan starts physically when the + // inhibit is released. Report success — the controller will run it. + ESP_LOGV(TAG, "Scan start deferred (connect in progress)"); + return true; + } +#endif // Serialize with the BTstack background worker (arduino-pico's BluetoothHCI // takes the same lock around its gap_* calls). BluetoothLock lock; @@ -224,6 +247,9 @@ bool RP2040BLE::scan_start(uint16_t interval, uint16_t window, bool active) { } void RP2040BLE::scan_stop() { +#ifdef USE_BLE_GATT_CLIENT + this->scan_desired_ = false; +#endif if (!this->is_active()) { return; // nothing can be scanning on a stack that is not up } @@ -231,6 +257,32 @@ void RP2040BLE::scan_stop() { gap_stop_scan(); } +#ifdef USE_BLE_GATT_CLIENT +void RP2040BLE::inhibit_scan() { + if (this->scan_inhibit_count_++ != 0) { + return; // another connect attempt already owns the radio + } + if (this->scan_desired_ && this->is_active()) { + BluetoothLock lock; + gap_stop_scan(); + } +} + +void RP2040BLE::release_scan_inhibit() { + if (this->scan_inhibit_count_ == 0) { + return; + } + this->scan_inhibit_count_--; + if (this->scan_inhibit_count_ != 0) { + return; + } + if (this->scan_desired_) { + // One physical-start path: scan_start re-applies the remembered params. + this->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_mode_); + } +} +#endif // USE_BLE_GATT_CLIENT + } // namespace esphome::rp2040_ble #endif // USE_RP2040_BLE diff --git a/esphome/components/rp2040_ble/rp2040_ble.h b/esphome/components/rp2040_ble/rp2040_ble.h index cc015c05030..99eb8cd88ad 100644 --- a/esphome/components/rp2040_ble/rp2040_ble.h +++ b/esphome/components/rp2040_ble/rp2040_ble.h @@ -91,13 +91,25 @@ class RP2040BLE final : public Component { /// (0.625 ms). Returns false until the stack is ACTIVE (callers retry — the /// tracker's rate-limited retry loop); powering the stack on stays with the /// user (enable_on_boot or an explicit enable() call). The controller keeps - /// no scan state: a disable()/enable() power cycle ends the scan, and the - /// caller must call scan_start() again once the stack is back to ACTIVE - /// (the tracker's loop() reconciliation does exactly that). + /// no scan state across power cycles: a disable()/enable() cycle ends the + /// scan, and the caller must call scan_start() again once the stack is back + /// to ACTIVE (the tracker's loop() reconciliation does exactly that). + /// While a GATT connect attempt has the scan inhibited, the desired scan is + /// remembered and started physically when the inhibit is released. bool scan_start(uint16_t interval, uint16_t window, bool active); /// Stop the controller scan (no-op when not scanning). void scan_stop(); +#ifdef USE_BLE_GATT_CLIENT + /// Pause the physical scan for the duration of a GATT connect attempt + /// (initiating and scanning contend for the radio). The desired scan state + /// set through scan_start()/scan_stop() is remembered and reconciled by + /// release_scan_inhibit(). Holders must guarantee the release on every + /// abort path (the GATT engine reclaims via its connect timeout). + void inhibit_scan(); + void release_scan_inhibit(); +#endif + protected: static void packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size); @@ -128,6 +140,16 @@ class RP2040BLE final : public Component { bool enable_on_boot_{true}; bool btstack_initialized_{false}; bool active_logged_{false}; +#ifdef USE_BLE_GATT_CLIENT + // Remembered scan intent, so connect attempts can pause the physical scan + // and restore it afterwards without involving the tracker. Counted so + // overlapping connect attempts compose once multiple slots exist. + uint16_t scan_interval_{0}; + uint16_t scan_window_{0}; + uint8_t scan_inhibit_count_{0}; + bool scan_active_mode_{false}; + bool scan_desired_{false}; +#endif }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)