mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ble_device_base] Replace raw-advertisement std::function with a lightweight callback slot (#17902)
This commit is contained in:
@@ -123,8 +123,14 @@ void BK72xxBLETracker::dump_config() {
|
|||||||
|
|
||||||
void BK72xxBLETracker::on_scan_report(const bk72xx_ble::BLEScanReport &report) {
|
void BK72xxBLETracker::on_scan_report(const bk72xx_ble::BLEScanReport &report) {
|
||||||
// Raw callback (the raw-advertisement path).
|
// Raw callback (the raw-advertisement path).
|
||||||
if (this->raw_advertisement_callback_)
|
if (this->raw_advertisement_callback_.is_set()) {
|
||||||
this->raw_advertisement_callback_(report.mac, report.rssi, report.addr_type, report.data, report.data_len);
|
const ble_device_base::RawAdvertisement adv{.mac = report.mac,
|
||||||
|
.data = report.data,
|
||||||
|
.data_len = report.data_len,
|
||||||
|
.rssi = report.rssi,
|
||||||
|
.addr_type = report.addr_type};
|
||||||
|
this->raw_advertisement_callback_.invoke(adv);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||||
ble_device_base::ESPBTDevice device;
|
ble_device_base::ESPBTDevice device;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#ifdef USE_OTA_STATE_LISTENER
|
#ifdef USE_OTA_STATE_LISTENER
|
||||||
#include "esphome/components/ota/ota_backend.h"
|
#include "esphome/components/ota/ota_backend.h"
|
||||||
@@ -84,8 +83,8 @@ class BK72xxBLETracker : public Component,
|
|||||||
this->listeners_.push_back(listener);
|
this->listeners_.push_back(listener);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback cb) override {
|
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||||
this->raw_advertisement_callback_ = std::move(cb);
|
this->raw_advertisement_callback_ = callback;
|
||||||
}
|
}
|
||||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||||
// The Beken BDK exposes no active-scan path (passive scanning only), so the
|
// The Beken BDK exposes no active-scan path (passive scanning only), so the
|
||||||
@@ -131,7 +130,7 @@ class BK72xxBLETracker : public Component,
|
|||||||
uint32_t scan_period_start_{0}; // millis() at start of current scan period; used to rate-limit on_scan_end()
|
uint32_t scan_period_start_{0}; // millis() at start of current scan period; used to rate-limit on_scan_end()
|
||||||
bool scan_started_once_{false}; // true after first successful scan start; gates the period timer
|
bool scan_started_once_{false}; // true after first successful scan start; gates the period timer
|
||||||
|
|
||||||
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{nullptr};
|
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{};
|
||||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||||
// Parsed-advertisement consumers registered through ble_device_base.
|
// Parsed-advertisement consumers registered through ble_device_base.
|
||||||
// Codegen-sized: no heap allocation, no std::vector template instantiations.
|
// Codegen-sized: no heap allocation, no std::vector template instantiations.
|
||||||
|
|||||||
@@ -17,15 +17,36 @@
|
|||||||
#include "ble_device.h"
|
#include "ble_device.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace esphome::ble_device_base {
|
namespace esphome::ble_device_base {
|
||||||
|
|
||||||
/// Callback for raw advertisements (the bluetooth_proxy path).
|
/// One raw advertisement as delivered by the controller — a borrowed view,
|
||||||
/// mac[] is least-significant octet first (BLE controller convention);
|
/// valid only for the duration of the invoke() callback.
|
||||||
/// the hub delivers on the ESPHome main loop.
|
struct RawAdvertisement {
|
||||||
using RawAdvertisementCallback =
|
/// Least-significant octet first (BLE controller convention).
|
||||||
std::function<void(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len)>;
|
const uint8_t *mac;
|
||||||
|
const uint8_t *data;
|
||||||
|
uint16_t data_len;
|
||||||
|
int8_t rssi; // signed dBm
|
||||||
|
uint8_t addr_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Subscriber slot for the raw-advertisement stream (the bluetooth_proxy
|
||||||
|
/// path). The hub delivers on the ESPHome main loop. Same shape as
|
||||||
|
/// logger.h's LogCallback: an instance pointer plus a plain function
|
||||||
|
/// pointer — no virtuals, no std::function.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// hub->set_raw_advertisement_callback({this, [](void *self, const RawAdvertisement &adv) {
|
||||||
|
/// static_cast<MyComponent *>(self)->on_raw_advertisement(adv);
|
||||||
|
/// }});
|
||||||
|
struct RawAdvertisementCallback {
|
||||||
|
void *instance{nullptr};
|
||||||
|
void (*fn)(void *instance, const RawAdvertisement &adv){nullptr};
|
||||||
|
/// A default-constructed slot is "no subscriber"; hubs must guard on this.
|
||||||
|
bool is_set() const { return this->fn != nullptr; }
|
||||||
|
void invoke(const RawAdvertisement &adv) const { this->fn(this->instance, adv); }
|
||||||
|
};
|
||||||
|
|
||||||
/// What a tracker's controller/SDK can do — consumers branch on data, not #ifdefs.
|
/// What a tracker's controller/SDK can do — consumers branch on data, not #ifdefs.
|
||||||
struct HubCapabilities {
|
struct HubCapabilities {
|
||||||
@@ -48,7 +69,7 @@ class BLEHub {
|
|||||||
virtual void register_listener(ESPBTDeviceListener *listener) = 0;
|
virtual void register_listener(ESPBTDeviceListener *listener) = 0;
|
||||||
|
|
||||||
/// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time.
|
/// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time.
|
||||||
virtual void set_raw_advertisement_callback(RawAdvertisementCallback cb) = 0;
|
virtual void set_raw_advertisement_callback(RawAdvertisementCallback callback) = 0;
|
||||||
|
|
||||||
virtual HubCapabilities get_capabilities() const = 0;
|
virtual HubCapabilities get_capabilities() const = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -239,8 +239,8 @@ class ESP32BLETracker final : public Component,
|
|||||||
|
|
||||||
// ---- ble_device_base::BLEHub (the platform-neutral tracker contract) ----
|
// ---- ble_device_base::BLEHub (the platform-neutral tracker contract) ----
|
||||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override;
|
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override;
|
||||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback cb) override {
|
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||||
this->raw_advertisement_callback_ = std::move(cb);
|
this->raw_advertisement_callback_ = callback;
|
||||||
}
|
}
|
||||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||||
return {/* active_scan = */ true, /* merges_scan_response = */ true, /* gatt = */ true};
|
return {/* active_scan = */ true, /* merges_scan_response = */ true, /* gatt = */ true};
|
||||||
@@ -341,7 +341,7 @@ class ESP32BLETracker final : public Component,
|
|||||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||||
StaticVector<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> neutral_listeners_;
|
StaticVector<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> neutral_listeners_;
|
||||||
#endif
|
#endif
|
||||||
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{nullptr};
|
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{};
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
/// Per-period "Found device" DEBUG log with MAC dedup (shared ble_device_base impl)
|
/// Per-period "Found device" DEBUG log with MAC dedup (shared ble_device_base impl)
|
||||||
ble_device_base::DiscoveredDeviceLog discovered_log_;
|
ble_device_base::DiscoveredDeviceLog discovered_log_;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||||
|
|
||||||
|
namespace esphome::ble_device_base::testing {
|
||||||
|
|
||||||
|
// Exercises the hub contract around RawAdvertisementCallback, not just the
|
||||||
|
// struct: a hub stores one slot via set_raw_advertisement_callback(), fires it
|
||||||
|
// only when set ("no subscriber" is the default-constructed slot), and a new
|
||||||
|
// registration replaces the old ("one consumer at a time").
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class FakeHub : public BLEHub {
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/// The emit path every tracker implements: fire only when a subscriber is set.
|
||||||
|
void emit(const RawAdvertisement &adv) {
|
||||||
|
if (this->callback_.is_set())
|
||||||
|
this->callback_.invoke(adv);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
RawAdvertisementCallback callback_; // default-constructed: no subscriber
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CapturingSubscriber {
|
||||||
|
RawAdvertisement last{};
|
||||||
|
int calls{0};
|
||||||
|
|
||||||
|
static void trampoline(void *self, const RawAdvertisement &adv) {
|
||||||
|
auto *sub = static_cast<CapturingSubscriber *>(self);
|
||||||
|
sub->last = adv;
|
||||||
|
sub->calls++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first.
|
||||||
|
const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa};
|
||||||
|
const uint8_t ADV_DATA[4] = {0x02, 0x01, 0x06, 0x00};
|
||||||
|
|
||||||
|
RawAdvertisement make_test_adv() {
|
||||||
|
return RawAdvertisement{
|
||||||
|
.mac = MAC_LSB_FIRST, .data = ADV_DATA, .data_len = sizeof(ADV_DATA), .rssi = -63, .addr_type = 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(RawAdvertisementCallback, DefaultConstructedSlotIsNotSet) {
|
||||||
|
const RawAdvertisementCallback callback{};
|
||||||
|
EXPECT_FALSE(callback.is_set());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RawAdvertisementCallback, SubscriberSeesFieldsUnchanged) {
|
||||||
|
FakeHub hub;
|
||||||
|
CapturingSubscriber subscriber;
|
||||||
|
hub.set_raw_advertisement_callback({&subscriber, CapturingSubscriber::trampoline});
|
||||||
|
|
||||||
|
hub.emit(make_test_adv());
|
||||||
|
|
||||||
|
ASSERT_EQ(subscriber.calls, 1);
|
||||||
|
EXPECT_EQ(subscriber.last.mac, MAC_LSB_FIRST);
|
||||||
|
EXPECT_EQ(subscriber.last.data, ADV_DATA);
|
||||||
|
EXPECT_EQ(subscriber.last.data_len, sizeof(ADV_DATA));
|
||||||
|
EXPECT_EQ(subscriber.last.rssi, -63);
|
||||||
|
EXPECT_EQ(subscriber.last.addr_type, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RawAdvertisementCallback, NoSubscriberDoesNotFire) {
|
||||||
|
FakeHub hub;
|
||||||
|
// No set_raw_advertisement_callback(): emitting must be a guarded no-op,
|
||||||
|
// not a jump through a garbage pointer.
|
||||||
|
hub.emit(make_test_adv());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RawAdvertisementCallback, NewSubscriberReplacesOld) {
|
||||||
|
FakeHub hub;
|
||||||
|
CapturingSubscriber first;
|
||||||
|
CapturingSubscriber second;
|
||||||
|
hub.set_raw_advertisement_callback({&first, CapturingSubscriber::trampoline});
|
||||||
|
hub.set_raw_advertisement_callback({&second, CapturingSubscriber::trampoline});
|
||||||
|
|
||||||
|
hub.emit(make_test_adv());
|
||||||
|
|
||||||
|
EXPECT_EQ(first.calls, 0); // one consumer at a time
|
||||||
|
ASSERT_EQ(second.calls, 1);
|
||||||
|
EXPECT_EQ(second.last.rssi, -63);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::ble_device_base::testing
|
||||||
Reference in New Issue
Block a user