Bind BLEHub to the build's tracker at compile time

This commit is contained in:
J. Nick Koston
2026-08-08 01:18:21 -05:00
parent c0c3afe6b1
commit ab458fffab
18 changed files with 193 additions and 151 deletions
@@ -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)
@@ -45,7 +45,6 @@ namespace esphome::bk72xx_ble_tracker {
// ---------------------------------------------------------------------------
class BK72xxBLETracker : public Component,
public ble_device_base::BLEHub,
public bk72xx_ble::BLEScanListener,
public Parented<bk72xx_ble::BK72xxBLE>
#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
@@ -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).
@@ -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<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
template<typename Hub> explicit ESPBTAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
void set_addresses(std::initializer_list<uint64_t> addresses) { this->addresses_ = addresses; }
@@ -39,7 +41,7 @@ class ESPBTAdvertiseTrigger final : public Trigger<const ESPBTDevice &>, public
// data for the given UUID. Optional single-MAC filter.
class BLEServiceDataAdvertiseTrigger final : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {
public:
explicit BLEServiceDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
template<typename Hub> explicit BLEServiceDataAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
void set_service_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast<uint16_t>(uuid)); }
void set_service_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast<uint32_t>(uuid)); }
@@ -73,7 +75,7 @@ class BLEServiceDataAdvertiseTrigger final : public Trigger<const adv_data_t &>,
// manufacturer data for the given ID. Optional single-MAC filter.
class BLEManufacturerDataAdvertiseTrigger final : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {
public:
explicit BLEManufacturerDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
template<typename Hub> explicit BLEManufacturerDataAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
void set_manufacturer_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast<uint16_t>(uuid)); }
void set_manufacturer_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast<uint32_t>(uuid)); }
@@ -108,7 +110,7 @@ class BLEManufacturerDataAdvertiseTrigger final : public Trigger<const adv_data_
// claims devices (parse_device always returns false).
class BLEEndOfScanTrigger final : public Trigger<>, public ESPBTDeviceListener {
public:
explicit BLEEndOfScanTrigger(BLEHub *parent) { parent->register_listener(this); }
template<typename Hub> explicit BLEEndOfScanTrigger(Hub *parent) { parent->register_listener(this); }
bool parse_device(const ESPBTDevice &device) override { return false; }
void on_scan_end() override { this->trigger(); }
+45 -50
View File
@@ -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
@@ -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
@@ -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")
@@ -75,9 +75,13 @@ void BluetoothProxy::setup() {
this->hub_->set_raw_advertisement_callback({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
static_cast<BluetoothProxy *>(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<BluetoothProxy *>(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
@@ -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<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> 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<esp32_ble_tracker::ESP32BLETracker *>(this->hub_);
}
#endif
// BLE advertisement batching
api::BluetoothLERawAdvertisementsResponse response_;
@@ -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)
@@ -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<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> 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_;
@@ -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)
@@ -26,7 +26,6 @@ namespace esphome::ln882h_ble_tracker {
// ---------------------------------------------------------------------------
class LN882HBLETracker : public Component,
public ble_device_base::BLEHub,
public Parented<ln882h_ble::LN882HBLE>,
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
@@ -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)
@@ -17,7 +17,6 @@
namespace esphome::rp2_ble_tracker {
class RP2BLETracker : public Component,
public ble_device_base::BLEHub,
public rp2040_ble::BLEScanListener,
public Parented<rp2040_ble::RP2040BLE>
#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
+4
View File
@@ -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
@@ -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) {
@@ -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));