From ab458fffab6643e1a17e503ff8b3d871457d3cc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Aug 2026 01:18:21 -0500 Subject: [PATCH] Bind BLEHub to the build's tracker at compile time --- .../components/bk72xx_ble_tracker/__init__.py | 3 + .../bk72xx_ble_tracker/bk72xx_ble_tracker.h | 15 ++- .../components/ble_device_base/__init__.py | 13 ++- .../components/ble_device_base/automation.h | 12 ++- esphome/components/ble_device_base/ble_hub.h | 95 +++++++++---------- .../components/ble_device_base/ble_hub_impl.h | 36 +++++++ .../components/bluetooth_proxy/__init__.py | 10 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 14 ++- .../bluetooth_proxy/bluetooth_proxy.h | 24 +---- .../components/esp32_ble_tracker/__init__.py | 3 + .../esp32_ble_tracker/esp32_ble_tracker.h | 24 +++-- .../components/ln882h_ble_tracker/__init__.py | 3 + .../ln882h_ble_tracker/ln882h_ble_tracker.h | 15 ++- .../components/rp2_ble_tracker/__init__.py | 3 + .../rp2_ble_tracker/rp2_ble_tracker.h | 15 ++- esphome/core/defines.h | 4 + .../ble_device_base/test_raw_callback.cpp | 13 +-- .../test_scan_mode_request.cpp | 42 ++++---- 18 files changed, 193 insertions(+), 151 deletions(-) create mode 100644 esphome/components/ble_device_base/ble_hub_impl.h diff --git a/esphome/components/bk72xx_ble_tracker/__init__.py b/esphome/components/bk72xx_ble_tracker/__init__.py index e7f8ed92ba..7fefb310cd 100644 --- a/esphome/components/bk72xx_ble_tracker/__init__.py +++ b/esphome/components/bk72xx_ble_tracker/__init__.py @@ -144,6 +144,9 @@ async def stop_scan_action_to_code( async def to_code(config: ConfigType) -> None: + # Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h. + cg.add_define("USE_BK72XX_BLE_TRACKER") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h index 67e4467c77..cc51918da5 100644 --- a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h +++ b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h @@ -45,7 +45,6 @@ namespace esphome::bk72xx_ble_tracker { // --------------------------------------------------------------------------- class BK72xxBLETracker : public Component, - public ble_device_base::BLEHub, public bk72xx_ble::BLEScanListener, public Parented #ifdef USE_OTA_STATE_LISTENER @@ -93,15 +92,15 @@ class BK72xxBLETracker : public Component, void stop_scan(); // ---- ble_device_base::BLEHub contract ---- - void register_listener(ble_device_base::ESPBTDeviceListener *listener) override { + void register_listener(ble_device_base::ESPBTDeviceListener *listener) { #ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT this->listeners_.push_back(listener); #endif } - void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) { this->raw_advertisement_callback_ = callback; } - ble_device_base::HubCapabilities get_capabilities() const override { + static constexpr ble_device_base::HubCapabilities get_capabilities() { // The Beken BDK exposes no active-scan path (passive scanning only), so the // controller never solicits scan responses and never merges them; consumers // relying on scan-response fields (device names) get them only where the @@ -110,21 +109,21 @@ class BK72xxBLETracker : public Component, // path there is no mode to switch to. return {.active_scan = false, .merges_scan_response = false, .gatt = false, .scan_mode_switch = false}; } - bool request_scan_mode(bool active) override { + bool request_scan_mode(bool active) { // 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 { + void get_adapter_mac(uint8_t out[6]) { uint8_t mac[6]; this->parent_->get_mac_lsb_first(mac); for (int i = 0; i < 6; i++) out[i] = mac[5 - i]; } - bool scan_running() override { return this->scan_running_; } - bool scan_active() override { return false; } // BK72xx scan is passive-only + bool scan_running() { return this->scan_running_; } + bool scan_active() { return false; } // BK72xx scan is passive-only // ---- bk72xx_ble::BLEScanListener ---- // Delivered by the controller's loop() on the ESPHome main task — the diff --git a/esphome/components/ble_device_base/__init__.py b/esphome/components/ble_device_base/__init__.py index a52286f456..bc97c973ea 100644 --- a/esphome/components/ble_device_base/__init__.py +++ b/esphome/components/ble_device_base/__init__.py @@ -3,15 +3,16 @@ ble_device_base — the platform-neutral BLE layer. Owns the shared advertisement types (ESPBTUUID / ESPBTDevice / ServiceData / ESPBLEiBeacon / ESPBTDeviceListener, in ble_device.h) and the tracker contract -(BLEHub, in ble_hub.h) on every platform. +(BLEHub, in ble_hub.h; C++-side a per-platform alias bound in ble_hub_impl.h) +on every platform. BLE consumers (sensor components, bluetooth_proxy) bind to whichever tracker the configuration declares via `cv.use_id(BLEHub)` — ESPHome resolves any declared subclass, so there is no platform table here and no dependency in either direction. A sensor extends BLE_DEVICE_SCHEMA in its CONFIG_SCHEMA (so an explicit ble_hub_id: is a declared key even on strict schemas) and calls -register_ble_device() in to_code; a tracker component subclasses BLEHub (C++ -and codegen class) and MUST call register_hub_provider() at import time — +register_ble_device() in to_code; a tracker component declares BLEHub as its +codegen-class parent and MUST call register_hub_provider() at import time — without it _require_hub rejects configs that bind through the generated id (an explicit ble_hub_id: bypasses the registry). Adding a new BLE chip requires only a new in-tree tracker component; out-of-tree BLE hubs are @@ -48,8 +49,10 @@ LISTENER_COUNT_DEFINE = "ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT" ble_device_base_ns = cg.esphome_ns.namespace("ble_device_base") -# The neutral tracker contract. Every tracker's codegen class declares this as a -# parent, which is what lets cv.use_id(BLEHub) resolve any of them. +# The neutral tracker contract. Every tracker's codegen class declares this as +# a parent, which is what lets cv.use_id(BLEHub) resolve any of them. Python +# only: the C++ name is a per-platform alias for the build's tracker class +# (ble_hub_impl.h), so generated code never names ble_device_base::BLEHub. BLEHub = ble_device_base_ns.class_("BLEHub") # The neutral listener base (C++: ble_device_base::ESPBTDeviceListener). diff --git a/esphome/components/ble_device_base/automation.h b/esphome/components/ble_device_base/automation.h index 507b3278d9..cb933dd690 100644 --- a/esphome/components/ble_device_base/automation.h +++ b/esphome/components/ble_device_base/automation.h @@ -1,11 +1,13 @@ // Platform-neutral BLE advertisement triggers: ESPBTDeviceListener subclasses // registered on a BLEHub, exposed by each tracker under its own automation // names. parse_device()'s return feeds the "Found device" suppression. +// The constructors are templated on the hub type instead of naming BLEHub: +// the contract is duck-typed and this header must also build with no tracker +// present (host unit tests). #pragma once #include "ble_device.h" -#include "ble_hub.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" @@ -18,7 +20,7 @@ namespace esphome::ble_device_base { // on_ble_advertise: fires on every BLE advertisement, optionally filtered to one or more MACs. class ESPBTAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { public: - explicit ESPBTAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); } + template explicit ESPBTAdvertiseTrigger(Hub *parent) { parent->register_listener(this); } void set_addresses(std::initializer_list addresses) { this->addresses_ = addresses; } @@ -39,7 +41,7 @@ class ESPBTAdvertiseTrigger final : public Trigger, public // data for the given UUID. Optional single-MAC filter. class BLEServiceDataAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { public: - explicit BLEServiceDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); } + template explicit BLEServiceDataAdvertiseTrigger(Hub *parent) { parent->register_listener(this); } void set_service_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast(uuid)); } void set_service_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast(uuid)); } @@ -73,7 +75,7 @@ class BLEServiceDataAdvertiseTrigger final : public Trigger, // manufacturer data for the given ID. Optional single-MAC filter. class BLEManufacturerDataAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { public: - explicit BLEManufacturerDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); } + template explicit BLEManufacturerDataAdvertiseTrigger(Hub *parent) { parent->register_listener(this); } void set_manufacturer_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast(uuid)); } void set_manufacturer_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast(uuid)); } @@ -108,7 +110,7 @@ class BLEManufacturerDataAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { public: - explicit BLEEndOfScanTrigger(BLEHub *parent) { parent->register_listener(this); } + template explicit BLEEndOfScanTrigger(Hub *parent) { parent->register_listener(this); } bool parse_device(const ESPBTDevice &device) override { return false; } void on_scan_end() override { this->trigger(); } diff --git a/esphome/components/ble_device_base/ble_hub.h b/esphome/components/ble_device_base/ble_hub.h index 72b6aba086..7069093110 100644 --- a/esphome/components/ble_device_base/ble_hub.h +++ b/esphome/components/ble_device_base/ble_hub.h @@ -1,13 +1,17 @@ // ble_hub.h // -// BLEHub — the platform-neutral BLE tracker contract. +// The platform-neutral BLE tracker contract: the types every tracker and +// consumer share, plus (below) the method surface every tracker provides. // -// Every BLE tracker component (esp32_ble_tracker, bk72xx_ble_tracker, -// ln882h_ble_tracker, future chips) implements this interface; every BLE -// consumer (sensor components, bluetooth_proxy) binds to it — in YAML via -// `cv.use_id(BLEHub)`, which resolves whichever tracker the config declares. -// Adding a new BLE chip therefore requires only a new tracker component that -// implements BLEHub: no consumer, registry, or base changes. +// Exactly one tracker component exists per build (one chip, one controller), +// so BLEHub is not an abstract interface: ble_hub_impl.h binds the name to +// the build's tracker with a `using` alias, and every hub call is a direct, +// inlinable member call — no vtable, no virtual dispatch. Trackers include +// this header and implement the documented surface; consumers include +// ble_hub_impl.h and bind in YAML via `cv.use_id(BLEHub)` (the Python-side +// class, which resolves whichever tracker the config declares). Adding a new +// BLE chip requires only a new tracker component that provides the surface +// and an alias arm in ble_hub_impl.h. // // Chip differences are expressed as data (HubCapabilities), never as // platform conditionals in consumers. @@ -90,48 +94,39 @@ struct HubCapabilities { bool scan_mode_switch; }; -class BLEHub { - public: - virtual ~BLEHub() = default; - - /// Register a parsed-advertisement consumer (BLE sensors, automation triggers). - virtual void register_listener(ESPBTDeviceListener *listener) = 0; - - /// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time. - virtual void set_raw_advertisement_callback(RawAdvertisementCallback callback) = 0; - -#ifdef USE_BLE_SCANNER_STATE_CALLBACK - /// Push subscriber for scanner-state transitions; hubs that can push - /// invoke scanner_state_callback_ where their state changes. Compiled only - /// when a subscriber exists (bluetooth_proxy emits the define), so - /// subscriber-less builds carry no storage. - void set_scanner_state_callback(ScannerStateCallback callback) { this->scanner_state_callback_ = callback; } - - protected: - ScannerStateCallback scanner_state_callback_{}; - - public: -#endif // USE_BLE_SCANNER_STATE_CALLBACK - - virtual HubCapabilities get_capabilities() const = 0; - - /// Adapter MAC in printable (MSB-first) order, out[0] = MSB. - virtual void get_adapter_mac(uint8_t out[6]) = 0; - - 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; whether this method honors requests is advertised by - /// HubCapabilities::scan_mode_switch, so consumers can gate features on the - /// switch without probing. - virtual bool request_scan_mode(bool active) { return false; } -}; +// The BLEHub method surface. Duck-typed: a tracker that misses one of these +// fails to compile at the consumer's call site, not at a class definition. +// +// /// Register a parsed-advertisement consumer (BLE sensors, automation triggers). +// void register_listener(ESPBTDeviceListener *listener); +// +// /// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time. +// void set_raw_advertisement_callback(RawAdvertisementCallback callback); +// +// /// Push subscriber for scanner-state transitions, invoked where the hub's +// /// state changes. Provided only by hubs that push (today: esp32), gated on +// /// USE_BLE_SCANNER_STATE_CALLBACK (bluetooth_proxy emits the define), so +// /// subscriber-less builds carry no storage. Consumers of a hub without it +// /// fall back to polling scan_running(). +// void set_scanner_state_callback(ScannerStateCallback callback); +// +// static constexpr HubCapabilities get_capabilities(); +// +// /// Adapter MAC in printable (MSB-first) order, out[0] = MSB. +// void get_adapter_mac(uint8_t out[6]); +// +// bool scan_running(); +// /// True when the current/configured scan mode is active (scan requests sent). +// bool scan_active(); +// /// 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. A hub without a mode switch refuses +// /// without changing any state. Independent of HubCapabilities::active_scan: +// /// that bit describes what the CONTROLLER can do; whether this method +// /// honors requests is advertised by HubCapabilities::scan_mode_switch, so +// /// consumers can gate features on the switch without probing. +// bool request_scan_mode(bool active); } // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_hub_impl.h b/esphome/components/ble_device_base/ble_hub_impl.h new file mode 100644 index 0000000000..29cec01f58 --- /dev/null +++ b/esphome/components/ble_device_base/ble_hub_impl.h @@ -0,0 +1,36 @@ +// ble_hub_impl.h +// +// Binds ble_device_base::BLEHub to the build's tracker. Exactly one tracker +// component exists per build, so the hub is a compile-time alias rather than +// an abstract interface: every hub call is a direct, inlinable member call +// and trackers carry no vtable for the contract. Consumers include this +// header; trackers include ble_hub.h (the contract types and the documented +// method surface). Each tracker's codegen emits its USE_*_BLE_TRACKER define. + +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32_BLE_TRACKER) +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#elif defined(USE_RP2_BLE_TRACKER) +#include "esphome/components/rp2_ble_tracker/rp2_ble_tracker.h" +#elif defined(USE_BK72XX_BLE_TRACKER) +#include "esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h" +#elif defined(USE_LN882H_BLE_TRACKER) +#include "esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h" +#endif + +namespace esphome::ble_device_base { + +#if defined(USE_ESP32_BLE_TRACKER) +using BLEHub = esp32_ble_tracker::ESP32BLETracker; +#elif defined(USE_RP2_BLE_TRACKER) +using BLEHub = rp2_ble_tracker::RP2BLETracker; +#elif defined(USE_BK72XX_BLE_TRACKER) +using BLEHub = bk72xx_ble_tracker::BK72xxBLETracker; +#elif defined(USE_LN882H_BLE_TRACKER) +using BLEHub = ln882h_ble_tracker::LN882HBLETracker; +#endif + +} // namespace esphome::ble_device_base diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 8d9aa88bd8..a28c8abc71 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -374,10 +374,12 @@ async def _to_code_esp32(config: ConfigType) -> None: await cg.register_component(var, config) cg.add(var.set_active(config[CONF_ACTIVE])) - # Advertisements and scanner state arrive through the hub callbacks - # (installed in setup()); the tracker stays typed for scan-mode calls. tracker = await cg.get_variable(config[esp32_ble_tracker.CONF_ESP32_BLE_ID]) - cg.add(var.set_parent(tracker)) + cg.add(var.set_ble_hub(tracker)) + + # Compiles the scanner-state push slot into the tracker and the matching + # registration into the proxy; the other hubs are polled instead. + cg.add_define("USE_BLE_SCANNER_STATE_CALLBACK") # Define max connections for protobuf fixed array connection_count = len(config.get(CONF_CONNECTIONS, [])) @@ -426,5 +428,3 @@ async def to_code(config: ConfigType) -> None: cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) cg.add_define("USE_BLUETOOTH_PROXY") - # Compiles the scanner-state push slot into the hub (see ble_hub.h). - cg.add_define("USE_BLE_SCANNER_STATE_CALLBACK") diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 9f481b47f8..0b76e450cb 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -75,9 +75,13 @@ void BluetoothProxy::setup() { this->hub_->set_raw_advertisement_callback({this, [](void *self, const ble_device_base::RawAdvertisement &adv) { static_cast(self)->on_raw_advertisement_(adv); }}); +#ifdef USE_BLE_SCANNER_STATE_CALLBACK + // Only hubs that push scanner-state transitions compile the slot (today: + // esp32); elsewhere loop() polls scan_running() instead. this->hub_->set_scanner_state_callback({this, [](void *self, ble_device_base::ScannerState state) { static_cast(self)->send_bluetooth_scanner_state_(state); }}); +#endif } // The hub delivers raw advertisements on the ESPHome main loop. @@ -465,13 +469,13 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn #ifdef USE_ESP32 void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { - if (this->parent_()->get_scan_active() == active) { + if (this->hub_->get_scan_active() == active) { return; } ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive"); - this->parent_()->set_scan_active(active); - this->parent_()->stop_scan(); - this->parent_()->set_scan_continuous( + this->hub_->set_scan_active(active); + this->hub_->stop_scan(); + this->hub_->set_scan_continuous( true); // Set this to true to automatically start scanning again when it has cleaned up. } @@ -619,7 +623,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection } this->api_connection_ = api_connection; #ifdef USE_ESP32 - this->send_bluetooth_scanner_state_(this->parent_()->get_scanner_state()); + this->send_bluetooth_scanner_state_(this->hub_->get_scanner_state()); #else this->send_polled_scanner_state_(); #endif diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 68a43d8651..77ed888ff8 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -15,17 +15,13 @@ #include "esphome/components/bluetooth_connection/bluetooth_connection.h" +#include "esphome/components/ble_device_base/ble_hub_impl.h" + #ifdef USE_ESP32 -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" - #include "esphome/components/bluetooth_connection/bluetooth_connection_esp32.h" - -#else -#include "esphome/components/ble_device_base/ble_hub.h" -#ifdef USE_BLE_GATT_CLIENT +#elif defined(USE_BLE_GATT_CLIENT) #include "esphome/components/bluetooth_connection/bluetooth_connection_hub.h" #endif -#endif // USE_ESP32 namespace esphome::bluetooth_proxy { @@ -75,11 +71,7 @@ class BluetoothProxy final : public Component { #endif public: BluetoothProxy(); -#ifdef USE_ESP32 - // Advertisements arrive through the hub's raw callback; parent_() below - // recovers the tracker type for the esp32-only scan-mode calls. - void set_parent(esp32_ble_tracker::ESP32BLETracker *parent) { this->hub_ = parent; } -#endif // USE_ESP32 + void set_ble_hub(ble_device_base::BLEHub *hub) { this->hub_ = hub; } void dump_config() override; void setup() override; void loop() override; @@ -88,7 +80,6 @@ class BluetoothProxy final : public Component { void register_connection(BluetoothConnection *connection); #endif // BLUETOOTH_CONNECTION_HAS_GATT #ifndef USE_ESP32 - void set_ble_hub(ble_device_base::BLEHub *hub) { this->hub_ = hub; } // Run after the hub's setup() (the trackers use AFTER_WIFI): setup() below // snapshots scan_active()/scan_running() and installs the raw callback, and // the BLEHub contract does not promise those are settled any earlier than @@ -262,13 +253,6 @@ class BluetoothProxy final : public Component { std::array connections_{}; #endif ble_device_base::BLEHub *hub_{nullptr}; -#ifdef USE_ESP32 - // set_parent() is the only writer of hub_ on esp32, so the downcast is - // exact; ESP32BLETracker derives from BLEHub non-virtually. - esp32_ble_tracker::ESP32BLETracker *parent_() { - return static_cast(this->hub_); - } -#endif // BLE advertisement batching api::BluetoothLERawAdvertisementsResponse response_; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 0549a5f218..f5b4f232c7 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -205,6 +205,9 @@ async def to_code(config): # available on esp32 (sensors with irk: worked without opting in). ble_device_base.request_irk_support() + # Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h. + cg.add_define("USE_ESP32_BLE_TRACKER") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index ed8082a14d..2211f88b7e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -161,7 +161,6 @@ class ESPBTClient : public ESPBTDeviceListener { }; class ESP32BLETracker final : public Component, - public ble_device_base::BLEHub, #ifdef USE_OTA_STATE_LISTENER public ota::OTAGlobalStateListener, #endif @@ -186,19 +185,27 @@ class ESP32BLETracker final : public Component, void register_client(ESPBTClient *client); // ---- ble_device_base::BLEHub (the platform-neutral tracker contract) ---- - void register_listener(ble_device_base::ESPBTDeviceListener *listener) override; - void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + void register_listener(ble_device_base::ESPBTDeviceListener *listener); + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) { this->raw_advertisement_callback_ = callback; } - ble_device_base::HubCapabilities get_capabilities() const override { +#ifdef USE_BLE_SCANNER_STATE_CALLBACK + void set_scanner_state_callback(ble_device_base::ScannerStateCallback callback) { + this->scanner_state_callback_ = callback; + } +#endif + static constexpr ble_device_base::HubCapabilities get_capabilities() { // scan_mode_switch is false: the mode is driven through this tracker's own // API (set_scan_active + restart), not the neutral request_scan_mode(). return {/* active_scan = */ true, /* merges_scan_response = */ true, /* gatt = */ true, /* scan_mode_switch = */ false}; } - void get_adapter_mac(uint8_t out[6]) override; - bool scan_running() override { return this->scanner_state_ == ScannerState::RUNNING; } - bool scan_active() override { return this->scan_active_; } + void get_adapter_mac(uint8_t out[6]); + bool scan_running() { return this->scanner_state_ == ScannerState::RUNNING; } + bool scan_active() { return this->scan_active_; } + // The mode is driven through this tracker's own API (see get_capabilities); + // the neutral request refuses without changing any state. + bool request_scan_mode(bool active) { return false; } #ifdef USE_ESP32_BLE_DEVICE void print_bt_device_info(const ESPBTDevice &device); @@ -288,6 +295,9 @@ class ESP32BLETracker final : public Component, StaticVector neutral_listeners_; #endif ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{}; +#ifdef USE_BLE_SCANNER_STATE_CALLBACK + ble_device_base::ScannerStateCallback scanner_state_callback_{}; +#endif #ifdef USE_ESP32_BLE_DEVICE /// Per-period "Found device" DEBUG log with MAC dedup (shared ble_device_base impl) ble_device_base::DiscoveredDeviceLog discovered_log_; diff --git a/esphome/components/ln882h_ble_tracker/__init__.py b/esphome/components/ln882h_ble_tracker/__init__.py index ceb2aeffec..45f1b95164 100644 --- a/esphome/components/ln882h_ble_tracker/__init__.py +++ b/esphome/components/ln882h_ble_tracker/__init__.py @@ -127,6 +127,9 @@ async def stop_scan_action_to_code( async def to_code(config: ConfigType) -> None: + # Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h. + cg.add_define("USE_LN882H_BLE_TRACKER") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h b/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h index 1ad36c40d4..9c0e0b2f1a 100644 --- a/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h +++ b/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h @@ -26,7 +26,6 @@ namespace esphome::ln882h_ble_tracker { // --------------------------------------------------------------------------- class LN882HBLETracker : public Component, - public ble_device_base::BLEHub, public Parented, public ln882h_ble::BLEScanListener #ifdef USE_OTA_STATE_LISTENER @@ -75,15 +74,15 @@ class LN882HBLETracker : public Component, void stop_scan(); // ---- ble_device_base::BLEHub contract ---- - void register_listener(ble_device_base::ESPBTDeviceListener *listener) override { + void register_listener(ble_device_base::ESPBTDeviceListener *listener) { #ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT this->listeners_.push_back(listener); #endif } - void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) { this->raw_advertisement_callback_ = callback; } - ble_device_base::HubCapabilities get_capabilities() const override { + static constexpr ble_device_base::HubCapabilities get_capabilities() { // The LN882H controller supports active scanning; adv + scan response arrive // as separate reports and are merged by this tracker (Bluedroid semantics). // The SDK's GATT client is not exposed. @@ -92,15 +91,15 @@ class LN882HBLETracker : public Component, } // 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 { + void get_adapter_mac(uint8_t out[6]) { uint8_t mac[6]; this->parent_->get_mac_lsb_first(mac); for (int i = 0; i < 6; i++) out[i] = mac[5 - i]; } - bool scan_running() override { return this->scan_running_; } - bool scan_active() override { return this->scan_active_; } - bool request_scan_mode(bool active) override; + bool scan_running() { return this->scan_running_; } + bool scan_active() { return this->scan_active_; } + bool request_scan_mode(bool active); // ---- ln882h_ble::BLEScanListener ---- // Delivered by the controller's loop() on the ESPHome main task — the diff --git a/esphome/components/rp2_ble_tracker/__init__.py b/esphome/components/rp2_ble_tracker/__init__.py index 5840185768..15c1229a85 100644 --- a/esphome/components/rp2_ble_tracker/__init__.py +++ b/esphome/components/rp2_ble_tracker/__init__.py @@ -55,6 +55,9 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config: ConfigType) -> None: + # Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h. + cg.add_define("USE_RP2_BLE_TRACKER") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h index 8106763489..054f6a65d2 100644 --- a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h +++ b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h @@ -17,7 +17,6 @@ namespace esphome::rp2_ble_tracker { class RP2BLETracker : public Component, - public ble_device_base::BLEHub, public rp2040_ble::BLEScanListener, public Parented #ifdef USE_OTA_STATE_LISTENER @@ -51,15 +50,15 @@ class RP2BLETracker : public Component, void stop_scan(); // ---- ble_device_base::BLEHub contract ---- - void register_listener(ble_device_base::ESPBTDeviceListener *listener) override { + void register_listener(ble_device_base::ESPBTDeviceListener *listener) { #ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT this->listeners_.push_back(listener); #endif } - void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) { this->raw_advertisement_callback_ = callback; } - ble_device_base::HubCapabilities get_capabilities() const override { + static constexpr ble_device_base::HubCapabilities get_capabilities() { // BTstack delivers scan responses as separate advertisement reports rather // than merging them into the advertisement — consumers relying on // scan-response fields (device names) get them only where the receiver @@ -74,10 +73,10 @@ class RP2BLETracker : public Component, } // The controller stores the address in printable (MSB-first) order, which is // exactly what the contract wants. - 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; + void get_adapter_mac(uint8_t out[6]) { this->parent_->get_mac_msb_first(out); } + bool scan_running() { return this->scan_running_; } + bool scan_active() { return this->scan_active_; } + bool request_scan_mode(bool active); // ---- rp2040_ble::BLEScanListener ---- // Delivered by the controller's loop() on the ESPHome main loop — the diff --git a/esphome/core/defines.h b/esphome/core/defines.h index fd351356df..ed5bbd495c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -305,6 +305,7 @@ #define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE #define USE_ESP32_BLE_SERVER_ON_CONNECT #define USE_ESP32_BLE_SERVER_ON_DISCONNECT +#define USE_ESP32_BLE_TRACKER #define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 @@ -465,6 +466,7 @@ #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP #define USE_RP2040_BLE +#define USE_RP2_BLE_TRACKER #define RP2040_BLE_SCAN_LISTENER_COUNT 1 #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define USE_BLE_GATT_CLIENT @@ -486,8 +488,10 @@ #ifdef USE_LIBRETINY #define USE_BK72XX_BLE +#define USE_BK72XX_BLE_TRACKER #define BK72XX_BLE_SCAN_LISTENER_COUNT 1 #define USE_LN882H_BLE +#define USE_LN882H_BLE_TRACKER #define LN882H_BLE_SCAN_LISTENER_COUNT 1 #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define USE_CAPTIVE_PORTAL diff --git a/tests/components/ble_device_base/test_raw_callback.cpp b/tests/components/ble_device_base/test_raw_callback.cpp index 4d72c8fb18..6ee9e38852 100644 --- a/tests/components/ble_device_base/test_raw_callback.cpp +++ b/tests/components/ble_device_base/test_raw_callback.cpp @@ -13,17 +13,14 @@ namespace esphome::ble_device_base::testing { // // The in-tree emit site (BK72xxBLETracker::on_scan_report) compiles against // the Beken SDK and cannot run host-side, so the guard-and-fire semantics are -// pinned here through a minimal host BLEHub implementation instead. +// pinned here through a minimal host hub instead. The contract is duck-typed +// (BLEHub is a per-platform alias, not a base class), so the fake carries +// only the slot surface under test. namespace { -class FakeHub : public BLEHub { +class FakeHub { public: - void register_listener(ESPBTDeviceListener *listener) override {} - void set_raw_advertisement_callback(RawAdvertisementCallback callback) override { this->callback_ = callback; } - HubCapabilities get_capabilities() const override { return {false, false, false}; } - void get_adapter_mac(uint8_t out[6]) override {} - bool scan_running() override { return false; } - bool scan_active() override { return false; } + void set_raw_advertisement_callback(RawAdvertisementCallback callback) { this->callback_ = callback; } /// The emit path every tracker implements: fire only when a subscriber is set. void emit(const RawAdvertisement &adv) { diff --git a/tests/components/ble_device_base/test_scan_mode_request.cpp b/tests/components/ble_device_base/test_scan_mode_request.cpp index 9125eb6f2f..3948532f41 100644 --- a/tests/components/ble_device_base/test_scan_mode_request.cpp +++ b/tests/components/ble_device_base/test_scan_mode_request.cpp @@ -6,47 +6,45 @@ 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. +// Pins the request_scan_mode() contract documented in ble_hub.h: a hub +// without a mode switch refuses without changing any state (so callers report +// the real state back), while a switching hub both honors the request and +// applies it. The contract is duck-typed (BLEHub is a per-platform alias), +// so the shapes are pinned through minimal host hubs mirroring the in-tree +// tracker stubs. namespace { -class DefaultHub : public BLEHub { +class RefusingHub { 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_; } + static constexpr HubCapabilities get_capabilities() { return {false, false, false}; } + bool scan_active() { return this->active_; } + // The refusing shape every non-switching tracker provides: no state touched. + bool request_scan_mode(bool active) { return false; } protected: bool active_{true}; }; -class SwitchingHub : public DefaultHub { +class SwitchingHub : public RefusingHub { public: - HubCapabilities get_capabilities() const override { return {true, false, false, /* scan_mode_switch = */ true}; } - bool request_scan_mode(bool active) override { + static constexpr HubCapabilities get_capabilities() { return {true, false, false, /* scan_mode_switch = */ true}; } + bool request_scan_mode(bool active) { this->active_ = active; return true; } }; // The esp32 shape: the controller supports active scanning but the hub keeps -// the refusing default (mode is driven through its own tracker API). -class CapableRefusingHub : public DefaultHub { +// the refusing stub (mode is driven through its own tracker API). +class CapableRefusingHub : public RefusingHub { public: - HubCapabilities get_capabilities() const override { return {true, false, false}; } + static constexpr HubCapabilities get_capabilities() { return {true, false, false}; } }; } // namespace -TEST(BLEHubScanModeRequest, DefaultRefusesAndChangesNothing) { - DefaultHub hub; +TEST(BLEHubScanModeRequest, RefusingHubChangesNothing) { + RefusingHub hub; EXPECT_TRUE(hub.scan_active()); EXPECT_FALSE(hub.request_scan_mode(false)); // Refused, not applied-and-reported-false: the state is untouched. @@ -55,7 +53,7 @@ TEST(BLEHubScanModeRequest, DefaultRefusesAndChangesNothing) { EXPECT_TRUE(hub.scan_active()); } -TEST(BLEHubScanModeRequest, OverrideHonorsAndApplies) { +TEST(BLEHubScanModeRequest, SwitchingHubHonorsAndApplies) { SwitchingHub hub; EXPECT_TRUE(hub.get_capabilities().scan_mode_switch); EXPECT_TRUE(hub.request_scan_mode(true));