Deliver scanner state through a hub push callback

This commit is contained in:
J. Nick Koston
2026-08-08 00:31:18 -05:00
parent a6f81f3185
commit 3e9808162b
6 changed files with 64 additions and 43 deletions
@@ -49,6 +49,27 @@ 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. Hubs that cannot push simply never invoke it 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).
@@ -78,6 +99,10 @@ class BLEHub {
/// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time.
virtual void set_raw_advertisement_callback(RawAdvertisementCallback callback) = 0;
/// Push subscriber for scanner-state transitions (stored here; hubs invoke
/// scanner_state_callback_ where their state changes, if they can push).
void set_scanner_state_callback(ScannerStateCallback callback) { this->scanner_state_callback_ = callback; }
virtual HubCapabilities get_capabilities() const = 0;
/// Adapter MAC in printable (MSB-first) order, out[0] = MSB.
@@ -97,6 +122,9 @@ class BLEHub {
/// HubCapabilities::scan_mode_switch, so consumers can gate features on the
/// switch without probing.
virtual bool request_scan_mode(bool active) { return false; }
protected:
ScannerStateCallback scanner_state_callback_{};
};
} // namespace esphome::ble_device_base
@@ -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, []))
@@ -24,15 +24,18 @@ 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::STOPPED) ==
api::enums::BLUETOOTH_SCANNER_STATE_STOPPED);
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
void BluetoothProxy::on_scanner_state_(ble_device_base::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) {
api::BluetoothScannerStateResponse resp;
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
resp.mode = this->hub_->scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
@@ -40,30 +43,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.
@@ -76,6 +70,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)->on_scanner_state_(state);
}});
}
// The hub delivers raw advertisements on the ESPHome main loop.
@@ -507,7 +504,7 @@ void BluetoothProxy::loop() {
// The hub has no scanner-state listener interface; poll and report on change.
if (this->hub_->scan_running() != this->last_scan_running_) {
this->send_bluetooth_scanner_state_();
this->send_polled_scanner_state_();
}
this->flush_pending_advertisements_();
@@ -595,7 +592,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_();
}
}
@@ -616,9 +613,9 @@ 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_(static_cast<ble_device_base::ScannerState>(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;
@@ -138,11 +134,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;
@@ -216,10 +207,10 @@ 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_();
void on_scanner_state_(ble_device_base::ScannerState 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);
@@ -419,9 +419,16 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
#endif
}
// The neutral enum mirrors this tracker's wire-aligned values.
static_assert(static_cast<int>(ScannerState::IDLE) == static_cast<int>(ble_device_base::ScannerState::IDLE));
static_assert(static_cast<int>(ScannerState::STOPPING) == static_cast<int>(ble_device_base::ScannerState::STOPPING));
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state;
this->state_version_++;
if (this->scanner_state_callback_.is_set()) {
this->scanner_state_callback_.invoke(static_cast<ble_device_base::ScannerState>(state));
}
#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT
for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state);
@@ -107,13 +107,13 @@ def test_esp32_bluetooth_proxy_requests_scanner_state_slot(
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"