diff --git a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h index 3d32798622..435e953655 100644 --- a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h +++ b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h @@ -93,6 +93,11 @@ class BK72xxBLETracker : public Component, // receiver merges per address (Home Assistant does). No GATT client either. return {.active_scan = false, .merges_scan_response = false, .gatt = false}; } + bool request_scan_mode(bool active) override { + // Passive-only controller: a passive request is already honored, an active + // one cannot be. + return !active; + } // The controller stores the address LSB-first (BLE convention); the contract // wants printable (MSB-first) order. void get_adapter_mac(uint8_t out[6]) override { diff --git a/esphome/components/ble_device_base/ble_hub.h b/esphome/components/ble_device_base/ble_hub.h index c02b491237..6f7e580975 100644 --- a/esphome/components/ble_device_base/ble_hub.h +++ b/esphome/components/ble_device_base/ble_hub.h @@ -79,6 +79,17 @@ class BLEHub { virtual bool scan_running() = 0; /// True when the current/configured scan mode is active (scan requests sent). virtual bool scan_active() = 0; + /// Request a scan-mode change (active = send scan requests). Returns false + /// when the hub cannot honor the request; the caller reports the real state + /// back to its subscriber. A hub that returns true applies the mode + /// immediately: a running scan is restarted with the new mode, an idle one + /// picks it up on its next start. The default cannot-change keeps hubs + /// without a mode switch (and out-of-tree trackers) building unchanged. + /// Independent of HubCapabilities::active_scan: that bit describes what the + /// CONTROLLER can do, this method describes whether the hub exposes a + /// runtime switch — a hub may support active scanning and still refuse + /// (esp32_ble_tracker drives its mode through its own tracker API). + virtual bool request_scan_mode(bool active) { return false; } }; } // namespace esphome::ble_device_base diff --git a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp index 28c5c927ea..08a954239f 100644 --- a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp +++ b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp @@ -152,6 +152,31 @@ void RP2BLETracker::start_scan() { this->start_scan_(); } +bool RP2BLETracker::request_scan_mode(bool active) { + if (this->scan_active_ == active) + return true; + this->scan_active_ = active; + ESP_LOGD(TAG, "Scan mode %s", active ? "active" : "passive"); + // Apply to a running scan by restarting the CONTROLLER scan with the new + // mode, bypassing the tracker's stop/start bookkeeping: no on_scan_end (the + // scan logically continues, only the request mode changes), no period reset. + // An idle scanner picks the mode up on its next start. + if (this->scan_running_) { + this->parent_->scan_stop(); + // Stamp the attempt so the SCAN_START_RETRY_MS floor covers this start + // like every other one. + this->last_scan_start_attempt_ = App.get_loop_component_start_time(); + if (!this->parent_->scan_start(static_cast(this->scan_interval_), + static_cast(this->scan_window_), this->scan_active_)) { + // The controller really stopped: behave exactly like loop()'s + // reconciliation branch - notify listeners and let its retry recover. + this->scan_running_ = false; + this->fire_scan_end_(); + } + } + return true; +} + void RP2BLETracker::stop_scan() { this->scan_continuous_ = false; this->stop_scan_(); diff --git a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h index bb2a6862af..dd000645c5 100644 --- a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h +++ b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h @@ -71,6 +71,7 @@ class RP2BLETracker : public Component, void get_adapter_mac(uint8_t out[6]) override { this->parent_->get_mac_msb_first(out); } bool scan_running() override { return this->scan_running_; } bool scan_active() override { return this->scan_active_; } + bool request_scan_mode(bool active) override; // ---- rp2040_ble::BLEScanListener ---- // Delivered by the controller's loop() on the ESPHome main loop — the diff --git a/tests/components/ble_device_base/test_scan_mode_request.cpp b/tests/components/ble_device_base/test_scan_mode_request.cpp new file mode 100644 index 0000000000..e98f25b10d --- /dev/null +++ b/tests/components/ble_device_base/test_scan_mode_request.cpp @@ -0,0 +1,59 @@ +#include + +#include + +#include "esphome/components/ble_device_base/ble_hub.h" + +namespace esphome::ble_device_base::testing { + +// Pins the request_scan_mode() contract: the base default refuses (so hubs +// without a mode switch — and out-of-tree trackers — keep building and +// callers report the real state), while an overriding hub both honors the +// request and applies it. +namespace { + +class DefaultHub : public BLEHub { + public: + void register_listener(ESPBTDeviceListener *listener) override {} + void set_raw_advertisement_callback(RawAdvertisementCallback callback) override {} + HubCapabilities get_capabilities() const override { return {false, false, false}; } + void get_adapter_mac(uint8_t out[6]) override {} + bool scan_running() override { return false; } + // Backed by real state so "changes nothing" is observable: a base default + // that silently mutated the hub would flip this and fail the assertion. + bool scan_active() override { return this->active_; } + + protected: + bool active_{true}; +}; + +class SwitchingHub : public DefaultHub { + public: + HubCapabilities get_capabilities() const override { return {true, false, false}; } + bool request_scan_mode(bool active) override { + this->active_ = active; + return true; + } +}; + +} // namespace + +TEST(BLEHubScanModeRequest, DefaultRefusesAndChangesNothing) { + DefaultHub hub; + EXPECT_TRUE(hub.scan_active()); + EXPECT_FALSE(hub.request_scan_mode(false)); + // Refused, not applied-and-reported-false: the state is untouched. + EXPECT_TRUE(hub.scan_active()); + EXPECT_FALSE(hub.request_scan_mode(true)); + EXPECT_TRUE(hub.scan_active()); +} + +TEST(BLEHubScanModeRequest, OverrideHonorsAndApplies) { + SwitchingHub hub; + EXPECT_TRUE(hub.request_scan_mode(true)); + EXPECT_TRUE(hub.scan_active()); + EXPECT_TRUE(hub.request_scan_mode(false)); + EXPECT_FALSE(hub.scan_active()); +} + +} // namespace esphome::ble_device_base::testing