mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
[bluetooth_proxy] Deliver scanner state through the hub callback (#18175)
This commit is contained in:
@@ -49,6 +49,28 @@ struct RawAdvertisementCallback {
|
||||
void invoke(const RawAdvertisement &adv) const { this->fn(this->instance, adv); }
|
||||
};
|
||||
|
||||
/// Scanner lifecycle, wire-value aligned with the api enum so consumers cast
|
||||
/// directly (pinned by static_asserts at the cast sites).
|
||||
enum class ScannerState : uint8_t {
|
||||
IDLE = 0,
|
||||
STARTING = 1,
|
||||
RUNNING = 2,
|
||||
FAILED = 3,
|
||||
STOPPING = 4,
|
||||
STOPPED = 5,
|
||||
};
|
||||
|
||||
/// Subscriber slot for scanner-state transitions; same shape as
|
||||
/// RawAdvertisementCallback, delivered on the ESPHome main loop. Hubs that
|
||||
/// cannot push drop the registration and the consumer falls back to polling
|
||||
/// scan_running().
|
||||
struct ScannerStateCallback {
|
||||
void *instance{nullptr};
|
||||
void (*fn)(void *instance, ScannerState state){nullptr};
|
||||
bool is_set() const { return this->fn != nullptr; }
|
||||
void invoke(ScannerState state) const { this->fn(this->instance, state); }
|
||||
};
|
||||
|
||||
/// What a tracker's controller/SDK can do — consumers branch on data, not #ifdefs.
|
||||
struct HubCapabilities {
|
||||
/// Controller can send scan requests (active scanning).
|
||||
@@ -79,6 +101,19 @@ class BLEHub {
|
||||
/// 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.
|
||||
|
||||
@@ -374,12 +374,10 @@ async def _to_code_esp32(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_active(config[CONF_ACTIVE]))
|
||||
# Advertisements arrive through the hub raw callback (installed in
|
||||
# setup()); only the scanner-state listener still registers with the
|
||||
# tracker directly.
|
||||
# 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))
|
||||
await esp32_ble_tracker.register_scanner_state_listener(var, config)
|
||||
|
||||
# Define max connections for protobuf fixed array
|
||||
connection_count = len(config.get(CONF_CONNECTIONS, []))
|
||||
@@ -428,3 +426,5 @@ 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")
|
||||
|
||||
@@ -25,15 +25,22 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62
|
||||
|
||||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// The neutral enum's values are the wire values.
|
||||
static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::IDLE) == api::enums::BLUETOOTH_SCANNER_STATE_IDLE);
|
||||
static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STARTING) ==
|
||||
api::enums::BLUETOOTH_SCANNER_STATE_STARTING);
|
||||
static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::RUNNING) ==
|
||||
api::enums::BLUETOOTH_SCANNER_STATE_RUNNING);
|
||||
static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::FAILED) ==
|
||||
api::enums::BLUETOOTH_SCANNER_STATE_FAILED);
|
||||
static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STOPPING) ==
|
||||
api::enums::BLUETOOTH_SCANNER_STATE_STOPPING);
|
||||
static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STOPPED) ==
|
||||
api::enums::BLUETOOTH_SCANNER_STATE_STOPPED);
|
||||
|
||||
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
|
||||
bool BluetoothProxy::send_bluetooth_scanner_state_(ble_device_base::ScannerState state) {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return false;
|
||||
api::BluetoothScannerStateResponse resp;
|
||||
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
|
||||
resp.mode = this->hub_->scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
@@ -41,30 +48,21 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
|
||||
resp.configured_mode = this->configured_scan_active_
|
||||
? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
|
||||
this->api_connection_->send_message(resp);
|
||||
return this->api_connection_->send_message(resp);
|
||||
}
|
||||
|
||||
#else // !USE_ESP32
|
||||
|
||||
void BluetoothProxy::send_bluetooth_scanner_state_() {
|
||||
#ifndef USE_ESP32
|
||||
void BluetoothProxy::send_polled_scanner_state_() {
|
||||
// One read feeds both the frame and the change detector; the detector only
|
||||
// advances if the frame was accepted, so a dropped send (WOULD_BLOCK on a
|
||||
// full TX buffer) is retried from loop() instead of leaving a stale state.
|
||||
const bool running = this->hub_->scan_running();
|
||||
api::BluetoothScannerStateResponse resp;
|
||||
resp.state = running ? api::enums::BluetoothScannerState::BLUETOOTH_SCANNER_STATE_RUNNING
|
||||
: api::enums::BluetoothScannerState::BLUETOOTH_SCANNER_STATE_IDLE;
|
||||
resp.mode = this->hub_->scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
|
||||
resp.configured_mode = this->configured_scan_active_
|
||||
? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
|
||||
if (this->api_connection_->send_message(resp)) {
|
||||
if (this->send_bluetooth_scanner_state_(running ? ble_device_base::ScannerState::RUNNING
|
||||
: ble_device_base::ScannerState::IDLE)) {
|
||||
this->last_scan_running_ = running;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // !USE_ESP32
|
||||
|
||||
void BluetoothProxy::setup() {
|
||||
// BLUETOOTH_PROXY_MAX_CONNECTIONS is 0 on an advertisement-only proxy.
|
||||
@@ -77,6 +75,9 @@ 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);
|
||||
}});
|
||||
this->hub_->set_scanner_state_callback({this, [](void *self, ble_device_base::ScannerState state) {
|
||||
static_cast<BluetoothProxy *>(self)->send_bluetooth_scanner_state_(state);
|
||||
}});
|
||||
}
|
||||
|
||||
// The hub delivers raw advertisements on the ESPHome main loop.
|
||||
@@ -510,9 +511,10 @@ void BluetoothProxy::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The hub has no scanner-state listener interface; poll and report on change.
|
||||
// This hub doesn't push scanner-state transitions; poll and report on
|
||||
// change. A hub gaining push must also refresh last_scan_running_ here.
|
||||
if (this->hub_->scan_running() != this->last_scan_running_) {
|
||||
this->send_bluetooth_scanner_state_();
|
||||
this->send_polled_scanner_state_();
|
||||
}
|
||||
|
||||
this->flush_pending_advertisements_();
|
||||
@@ -600,7 +602,7 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
|
||||
// Reports the mode change; the sender also refreshes last_scan_running_, so
|
||||
// a failed restart (scan_running_ dropped by the tracker) is not reported
|
||||
// again by loop() on the next tick.
|
||||
this->send_bluetooth_scanner_state_();
|
||||
this->send_polled_scanner_state_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +625,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
|
||||
#ifdef USE_ESP32
|
||||
this->send_bluetooth_scanner_state_(this->parent_()->get_scanner_state());
|
||||
#else
|
||||
this->send_bluetooth_scanner_state_();
|
||||
this->send_polled_scanner_state_();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
|
||||
};
|
||||
|
||||
#ifdef USE_ESP32
|
||||
class BluetoothProxy final : public esp32_ble_tracker::BLEScannerStateListener, public Component {
|
||||
#else
|
||||
class BluetoothProxy final : public Component {
|
||||
#endif
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
// Allow the connection to update connections_free_response_
|
||||
friend bluetooth_connection::BluetoothConnection;
|
||||
@@ -135,11 +131,6 @@ class BluetoothProxy final : public Component {
|
||||
void set_active(bool active) { this->active_ = active; }
|
||||
bool has_active() { return this->active_; }
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// BLEScannerStateListener interface
|
||||
void on_scanner_state(esp32_ble_tracker::ScannerState state) override;
|
||||
#endif
|
||||
|
||||
uint32_t get_legacy_version() const {
|
||||
if (!this->active_) {
|
||||
return LEGACY_PASSIVE_ONLY_VERSION;
|
||||
@@ -213,10 +204,9 @@ class BluetoothProxy final : public Component {
|
||||
}
|
||||
|
||||
protected:
|
||||
#ifdef USE_ESP32
|
||||
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
|
||||
#else
|
||||
void send_bluetooth_scanner_state_();
|
||||
bool send_bluetooth_scanner_state_(ble_device_base::ScannerState state);
|
||||
#ifndef USE_ESP32
|
||||
void send_polled_scanner_state_();
|
||||
#endif
|
||||
void on_raw_advertisement_(const ble_device_base::RawAdvertisement &raw);
|
||||
|
||||
|
||||
@@ -422,6 +422,11 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
|
||||
this->scanner_state_ = state;
|
||||
this->state_version_++;
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
if (this->scanner_state_callback_.is_set()) {
|
||||
this->scanner_state_callback_.invoke(state);
|
||||
}
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT
|
||||
for (auto *listener : this->scanner_state_listeners_) {
|
||||
listener->on_scanner_state(state);
|
||||
|
||||
@@ -95,18 +95,8 @@ using ClientState = ble_device_base::ClientState;
|
||||
using ConnectionType = ble_device_base::ConnectionType;
|
||||
using ble_device_base::client_state_to_string;
|
||||
|
||||
enum class ScannerState {
|
||||
// Scanner is idle, init state
|
||||
IDLE,
|
||||
// Scanner is starting
|
||||
STARTING,
|
||||
// Scanner is running
|
||||
RUNNING,
|
||||
// Scanner failed to start
|
||||
FAILED,
|
||||
// Scanner is stopping
|
||||
STOPPING,
|
||||
};
|
||||
// Neutral scanner lifecycle re-exported for backward compatibility.
|
||||
using ScannerState = ble_device_base::ScannerState;
|
||||
|
||||
/** Listener interface for BLE scanner state changes.
|
||||
*
|
||||
|
||||
@@ -252,6 +252,7 @@
|
||||
// platforms whose API/network types the proxy header cannot assume.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_RP2)
|
||||
#define USE_BLUETOOTH_PROXY
|
||||
#define USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// Mirror the codegen values per platform: _to_code_esp32() emits the connection
|
||||
// count (default 3), _to_code_ble_hub() emits the slot count (1 on rp2, 0 on
|
||||
// advertisement-only hubs) — so static analysis checks the same
|
||||
|
||||
@@ -103,17 +103,17 @@ def test_esp32_tracker_handler_counts(
|
||||
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") is None
|
||||
|
||||
|
||||
def test_esp32_bluetooth_proxy_requests_scanner_state_slot(
|
||||
def test_esp32_bluetooth_proxy_requests_client_slots_only(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The proxy requests one scanner state slot and a client slot per
|
||||
connection (three by default with active: true); advertisements arrive
|
||||
through the hub raw callback, so no listener slot exists."""
|
||||
"""The proxy requests a client slot per connection (three by default with
|
||||
active: true); advertisements and scanner state arrive through the hub
|
||||
callbacks, so no listener or scanner-state slot exists."""
|
||||
generate_main(component_config_path("esp32_bluetooth_proxy.yaml"))
|
||||
assert (
|
||||
get_define_value("ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT")
|
||||
== "1"
|
||||
is None
|
||||
)
|
||||
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None
|
||||
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") == "3"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Pins the ScannerStateCallback slot semantics, mirroring test_raw_callback:
|
||||
// a default-constructed slot is "no subscriber", a set slot delivers the
|
||||
// state, and a new registration replaces the old.
|
||||
namespace {
|
||||
|
||||
struct CapturingSubscriber {
|
||||
ScannerState last{ScannerState::IDLE};
|
||||
int calls{0};
|
||||
|
||||
static void trampoline(void *self, ScannerState state) {
|
||||
auto *sub = static_cast<CapturingSubscriber *>(self);
|
||||
sub->last = state;
|
||||
sub->calls++;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ScannerStateCallback, DefaultConstructedSlotIsNotSet) {
|
||||
const ScannerStateCallback callback{};
|
||||
EXPECT_FALSE(callback.is_set());
|
||||
}
|
||||
|
||||
TEST(ScannerStateCallback, SubscriberSeesState) {
|
||||
CapturingSubscriber subscriber;
|
||||
ScannerStateCallback callback{&subscriber, CapturingSubscriber::trampoline};
|
||||
ASSERT_TRUE(callback.is_set());
|
||||
callback.invoke(ScannerState::RUNNING);
|
||||
EXPECT_EQ(subscriber.calls, 1);
|
||||
EXPECT_EQ(subscriber.last, ScannerState::RUNNING);
|
||||
}
|
||||
|
||||
TEST(ScannerStateCallback, NewSubscriberReplacesOld) {
|
||||
CapturingSubscriber first;
|
||||
CapturingSubscriber second;
|
||||
ScannerStateCallback callback{&first, CapturingSubscriber::trampoline};
|
||||
callback = {&second, CapturingSubscriber::trampoline};
|
||||
callback.invoke(ScannerState::STOPPED);
|
||||
EXPECT_EQ(first.calls, 0);
|
||||
EXPECT_EQ(second.calls, 1);
|
||||
EXPECT_EQ(second.last, ScannerState::STOPPED);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
Reference in New Issue
Block a user