ESPHome 2026.9.0
Loading...
Searching...
No Matches
esp32_ble_tracker.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include "esp32_ble_tracker.h"
6#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
10#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
11#include <esp_bt.h>
12#endif
13#include <esp_bt_defs.h>
14#include <esp_bt_main.h>
15#include <esp_gap_ble_api.h>
16#include <freertos/FreeRTOS.h>
17#include <freertos/FreeRTOSConfig.h>
18#include <freertos/task.h>
19#include <nvs_flash.h>
20#include <cinttypes>
21
22#ifdef USE_OTA
24#endif
25
26#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
27#include <esp_coexist.h>
28#endif
29
30// bt_trace.h
31#undef TAG
32
34
35static const char *const TAG = "esp32_ble_tracker";
36
37ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
38
40
42 if (this->parent_->is_failed()) {
43 this->mark_failed();
44 ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
45 return;
46 }
47
49
50#ifdef USE_OTA_STATE_LISTENER
52#endif
53}
54
55#ifdef USE_OTA_STATE_LISTENER
57 if (state == ota::OTA_STARTED) {
58 ESP_LOGD(TAG, "Stopping scan for OTA");
60 this->stop_scan();
61#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
62 for (auto *client : this->clients_) {
63 client->disconnect();
64 }
65#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
66 // The OTA transfer blocks the main loop, so the revert in loop() cannot run. No
67 // active-connection gate here: every client was just told to disconnect.
68 this->update_coex_preference_(false);
69#endif
70#endif
71 } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
72 this->scan_continuous_before_ota_ = false;
73 this->scan_continuous_ = true;
74 // Do not restart scanning immediately here; allow loop() to
75 // safely restart scanning once the scanner and all clients are idle.
76 }
77}
78#endif
79
81 if (!this->parent_->is_active()) {
82 return;
83 }
84 if (this->ble_was_disabled_) {
85 this->ble_was_disabled_ = false;
86 // First start after boot or after the stack came back.
87 if (this->scan_continuous_) {
88 this->start_scan();
89 }
90 }
91
92 // Check for scan timeout - moved here from scheduler to avoid false reboots
93 // when the loop is blocked. This must run every iteration for safety.
94 if (this->scanner_state_ == ScannerState::RUNNING) {
95 switch (this->scan_timeout_state_) {
97 // Robust time comparison that handles rollover correctly
98 // This works because unsigned arithmetic wraps around predictably
99 if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
100 // First time we've seen the timeout exceeded - wait one more loop iteration
101 // This ensures all components have had a chance to process pending events
102 // This is because esp32_ble may not have run yet and called
103 // gap_scan_event_handler yet when the loop unblocks
104 ESP_LOGW(TAG, "Scan timeout exceeded");
106 }
107 break;
108 }
110 // We've waited at least one full loop iteration, and scan is still running
111 ESP_LOGE(TAG, "Scan never terminated, rebooting");
112 App.reboot();
113 break;
115 break;
116 }
117 }
118
119 // Fast path: skip expensive client state counting and processing
120 // if no state has changed since last loop iteration.
121 //
122 // How state changes ensure we reach the code below:
123 // - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
124 // scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
125 // set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
126 // STARTING, not RUNNING, so version is always incremented before this condition is true)
127 // - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
128 // - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
129 // connecting client finishes (state change), or scanner reaches RUNNING/IDLE
130 // - connection-window restart: scan_params_ is only written in start_scan_()
131 // (which changes scanner state via set_scanner_state_()), and
132 // counts.active/disconnecting only change on client state changes
133 //
134 // All conditions that affect the logic below are tied to state changes that increment
135 // state_version_, so the fast path is safe.
136 if (this->state_version_ == this->last_processed_version_) {
137 return;
138 }
140
141 // State changed - do full processing
143 if (counts != this->client_state_counts_) {
144 this->client_state_counts_ = counts;
145 ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d",
146 this->client_state_counts_.connecting, this->client_state_counts_.discovered,
147 this->client_state_counts_.disconnecting, this->client_state_counts_.active);
148 }
149
150 // Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
151 if (this->scanner_state_ == ScannerState::FAILED ||
152 (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
154 }
155
156#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
157 // The programmed window no longer matches the connection state (typically
158 // the last connection dropped): restart so the right window applies now
159 // instead of at the end of the scan period. Continuous only (a user-started
160 // scan would not restart); !disconnecting matches the restart gate below.
161 if (this->scanner_state_ == ScannerState::RUNNING && this->scan_continuous_ && !counts.disconnecting &&
162 this->scan_params_.scan_window != this->desired_scan_window_(counts.active)) {
163 // Same logical scan period continues: no on_scan_end sweeps for this
164 // restart. Only armed when the stop was issued.
165 this->skip_next_scan_end_ = this->stop_scan_();
166 }
167#endif
168 /*
169
170 Avoid starting the scanner if:
171 - we are already scanning
172 - we are connecting to a device
173 - we are disconnecting from a device
174
175 Otherwise the scanner could fail to ever start again
176 and our only way to recover is to reboot.
177
178 https://github.com/espressif/esp-idf/issues/6688
179
180 */
181
182 // Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
183 // no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states
184 // (their state changes increment version when they finish). CONNECTED / ESTABLISHED
185 // clients do NOT block this branch — the coex revert below has its own active-count gate.
186 if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
187#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
188 // Only revert to BALANCE when no connections are active. Established connections
189 // continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic
190 // (advertisement upload, log streaming) competes for the shared radio. Reverting too
191 // early causes Bluedroid to time out at ~20s and synthesize status=133.
192 if (!counts.active) {
193 this->update_coex_preference_(false);
194 }
195#endif
196 if (this->scan_continuous_) {
197 this->start_scan_(false); // first = false
198 }
199 }
200 // Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
201 // or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
202 // All these trigger state_version_ increment, so we'll process and check promotion eligibility.
203 // We check both RUNNING and IDLE states because:
204 // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
205 // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
206 if (counts.discovered && !counts.connecting &&
207 (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
209 }
210}
211
213
215 // V to match the start log: the mode-switch and OTA callers narrate their
216 // reason at D themselves, and the user-facing stop action is deliberate.
217 ESP_LOGV(TAG, "Stopping scan.");
218 this->scan_continuous_ = false;
219#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
220 // The window-change restart is abandoned with continuous scanning.
221 this->skip_next_scan_end_ = false;
222#endif
223 this->stop_scan_();
224}
225
227 // Tell the controller to stop; a scan still starting has nothing to stop yet.
228 if (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::FAILED) {
229 this->stop_scan_();
230 }
231#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
232 for (auto *client : this->clients_) {
233 client->ble_before_disabled_event_handler();
234 }
235 this->skip_next_scan_end_ = false;
236#endif
237 // The stop above never completes (stack torn down, events dropped); settle
238 // here so start_scan_() sees IDLE once the stack is back.
239 if (this->scanner_state_ != ScannerState::IDLE) {
240 this->cleanup_scan_state_(true);
241 }
242 // A failure latched by the old stack must not be handled against the next.
243 this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
244 this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
245 this->ble_was_disabled_ = true;
246}
247
249 if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
250 // IDLE means there is nothing to stop; STOPPING means a stop is already in
251 // flight and will finish on its own. Neither is an error.
252 if (this->scanner_state_ != ScannerState::IDLE && this->scanner_state_ != ScannerState::STOPPING) {
253 ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
254 }
255 return false;
256 }
257 // Reset timeout state machine when stopping scan
259 this->set_scanner_state_(ScannerState::STOPPING);
260 esp_err_t err = esp_ble_gap_stop_scanning();
261 if (err != ESP_OK) {
262 ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err);
263 return false;
264 }
265 return true;
266}
267
269 if (!this->parent_->is_active()) {
270 ESP_LOGW(TAG, "Cannot start scan while ESP32BLE is disabled.");
271 return;
272 }
273 if (this->scanner_state_ != ScannerState::IDLE) {
274 this->log_unexpected_state_("start scan", ScannerState::IDLE);
275 return;
276 }
277 this->set_scanner_state_(ScannerState::STARTING);
278 ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING.");
279 if (!first)
280 this->notify_scan_end_();
281#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
282 this->skip_next_scan_end_ = false;
283#endif
284#ifdef USE_ESP32_BLE_DEVICE
285 this->discovered_log_.clear();
286#endif
287 this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
288 this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
289 this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
290 this->scan_params_.scan_interval = this->scan_interval_;
291#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
292 // Count fresh: an automation can start a scan before loop() refreshes the counts.
293 const uint32_t window = this->desired_scan_window_(this->count_client_states_().active);
294 if (window != this->scan_window_) {
295 // Guarantee the connection airtime instead of scanning wall to wall.
296 ESP_LOGV(TAG, "Connection active, using %" PRIu32 " unit scan window", window);
297 }
298#else
299 const uint32_t window = this->scan_window_;
300#endif
301 this->scan_params_.scan_window = window;
302
303 // Start timeout monitoring in loop() instead of using scheduler
304 // This prevents false reboots when the loop is blocked
306 this->scan_timeout_ms_ = this->scan_duration_ * 2000;
308
309 esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
310 if (err != ESP_OK) {
311 ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", err);
312 return;
313 }
314 err = esp_ble_gap_start_scanning(this->scan_duration_);
315 if (err != ESP_OK) {
316 ESP_LOGE(TAG, "esp_ble_gap_start_scanning failed: %d", err);
317 return;
318 }
319}
320
322#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
323 client->app_id = ++this->app_id_;
324 // Give client a pointer to our state_version_ so it can notify us of state changes.
325 // This enables loop() fast-path optimization - we skip expensive work when no state changed.
326 // Safe because ESP32BLETracker (singleton) outlives all registered clients.
328 this->clients_.push_back(client);
329 // Registration is add-only, so the flag is a monotonic OR.
330 if (client->wants_parsed_advertisements())
331 this->parse_advertisements_ = true;
332#endif
333}
334
336#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
337 // Neutral BLEHub path (migrated sensors): parsed-advertisement consumers only.
338 this->neutral_listeners_.push_back(listener);
339 this->parse_advertisements_ = true;
340#endif
341}
342
344#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
345 listener->set_parent(this);
346 this->listeners_.push_back(listener);
347 this->parse_advertisements_ = true;
348#endif
349}
350
351void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
352 // Note: This handler is called from the main loop context, not directly from the BT task.
353 // The esp32_ble component queues events via enqueue_ble_event() and processes them in loop().
354 switch (event) {
355 case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
356 this->gap_scan_set_param_complete_(param->scan_param_cmpl);
357 break;
358 case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
359 this->gap_scan_start_complete_(param->scan_start_cmpl);
360 break;
361 case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
362 this->gap_scan_stop_complete_(param->scan_stop_cmpl);
363 break;
364 default:
365 break;
366 }
367 // Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
368#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
369 for (auto *client : this->clients_) {
370 client->gap_event_handler(event, param);
371 }
372#endif
373}
374
375void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
376 // Note: This handler is called from the main loop context via esp32_ble's event queue.
377 // We process advertisements immediately instead of buffering them.
378 ESP_LOGVV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
379
380 if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
381 // Process the scan result immediately
382 this->process_scan_result_(scan_result);
383 } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
384 // Scan finished on its own
385 if (this->scanner_state_ != ScannerState::RUNNING) {
386 this->log_unexpected_state_("scan complete", ScannerState::RUNNING);
387 }
388 // Scan completed naturally, perform cleanup and transition to IDLE
389 this->cleanup_scan_state_(false);
390 }
391}
392
393void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
394 // Called from main loop context via gap_event_handler after being queued from BT task
395 ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
396 if (param.status == ESP_BT_STATUS_DONE) {
397 this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
398 } else {
399 this->scan_set_param_failed_ = param.status;
400 }
401}
402
403void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
404 // Called from main loop context via gap_event_handler after being queued from BT task
405 ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
406 this->scan_start_failed_ = param.status;
407 if (this->scanner_state_ != ScannerState::STARTING) {
408 this->log_unexpected_state_("start complete", ScannerState::STARTING);
409 }
410 if (param.status == ESP_BT_STATUS_SUCCESS) {
411 this->scan_start_fail_count_ = 0;
412 this->set_scanner_state_(ScannerState::RUNNING);
413 } else {
414 this->set_scanner_state_(ScannerState::FAILED);
415 if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) {
417 }
418 }
419}
420
421void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) {
422 // Called from main loop context via gap_event_handler after being queued from BT task
423 // This allows us to safely transition to IDLE state and perform cleanup without race conditions
424 ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
425 if (this->scanner_state_ != ScannerState::STOPPING) {
426 this->log_unexpected_state_("stop complete", ScannerState::STOPPING);
427 }
428
429 // Perform cleanup and transition to IDLE
430 this->cleanup_scan_state_(true);
431}
432
433void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
434 esp_ble_gattc_cb_param_t *param) {
435#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
436 for (auto *client : this->clients_) {
437 client->gattc_event_handler(event, gattc_if, param);
438 }
439#endif
440}
441
443 this->scanner_state_ = state;
444 this->state_version_++;
445#ifdef USE_BLE_SCANNER_STATE_CALLBACK
446 if (this->scanner_state_callback_.is_set()) {
447 this->scanner_state_callback_.invoke(state);
448 }
449#endif
450}
451
453 ESP_LOGCONFIG(TAG, "BLE Tracker:");
454 ESP_LOGCONFIG(TAG,
455 " Scan Duration: %" PRIu32 " s\n"
456 " Scan Interval: %.1f ms\n"
457 " Scan Window: %.1f ms\n"
458 " Scan Type: %s\n"
459 " Continuous Scanning: %s",
460 this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
461 this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
462#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
463 if (this->connection_scan_window_ != 0) {
464 ESP_LOGCONFIG(TAG, " Connection Scan Window: %.1f ms", this->connection_scan_window_ * 0.625f);
465 }
466#endif
467 ESP_LOGCONFIG(TAG,
468 " Scanner State: %s\n"
469 " Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
471 this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
472 this->client_state_counts_.active);
473 if (this->scan_start_fail_count_) {
474 ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
475 }
476}
477
478#ifdef USE_ESP32_BLE_DEVICE
480 // Shared implementation in ble_device_base — identical output and per-period
481 // MAC dedup on every tracker backend.
482 this->discovered_log_.log_device(TAG, device);
483}
484
485// resolve_irk() is provided by ble_device_base (portable software AES).
486
487#endif // USE_ESP32_BLE_DEVICE
488
489void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
490 // Neutral raw-advertisement subscriber (the bluetooth_proxy path).
493 adv.address = esp32_ble::ble_addr_to_uint64(scan_result.bda);
494 adv.data = scan_result.ble_adv;
495 adv.data_len = static_cast<uint16_t>(scan_result.adv_data_len) + scan_result.scan_rsp_len;
496 adv.rssi = scan_result.rssi;
497 adv.addr_type = scan_result.ble_addr_type;
499 }
500
501 // Process parsed advertisements
502 if (this->parse_advertisements_) {
503#ifdef USE_ESP32_BLE_DEVICE
504 ESPBTDevice device;
505 // The historical ingest keeps the raw scan-result fields populated for
506 // external components.
507 device.parse_scan_rst(scan_result);
508
509 bool found = false;
510#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
511 for (auto *listener : this->listeners_) {
512 if (listener->parse_device(device))
513 found = true;
514 }
515#endif
516#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
517 for (auto *listener : this->neutral_listeners_) {
518 if (listener->parse_device(device))
519 found = true;
520 }
521#endif
522
523#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
524 for (auto *client : this->clients_) {
525 if (client->parse_device(device)) {
526 found = true;
527 }
528 }
529#endif
530
531 if (!found && !this->scan_continuous_) {
532 this->print_bt_device_info(device);
533 }
534#endif // USE_ESP32_BLE_DEVICE
535 }
536}
537
538void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
539 ESP_LOGV(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
540#ifdef USE_ESP32_BLE_DEVICE
541 this->discovered_log_.clear();
542#endif
543 // Reset timeout state machine instead of cancelling scheduler timeout
545
546 this->notify_scan_end_();
547
548 this->set_scanner_state_(ScannerState::IDLE);
549}
550
552#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
553 // Window-change restart continues the same scan period; the flag stays set
554 // across the stop and is cleared by the restart in start_scan_.
555 if (this->skip_next_scan_end_)
556 return;
557#endif
558#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
559 for (auto *listener : this->listeners_)
560 listener->on_scan_end();
561#endif
562#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
563 for (auto *listener : this->neutral_listeners_)
564 listener->on_scan_end();
565#endif
566}
567
569 this->stop_scan_();
570 if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
571 ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
572 std::numeric_limits<uint8_t>::max());
573 App.reboot();
574 }
575 if (this->scan_start_failed_) {
576 ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
577 this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
578 }
579 if (this->scan_set_param_failed_) {
580 ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
581 this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
582 }
583}
584
586 // Only promote the first discovered client to avoid multiple simultaneous connections
587#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
588 for (auto *client : this->clients_) {
589 if (client->state() != ClientState::DISCOVERED) {
590 continue;
591 }
592
593 if (this->scanner_state_ == ScannerState::RUNNING) {
594 ESP_LOGD(TAG, "Stopping scan to make connection");
595 this->stop_scan_();
596 // Don't wait for scan stop complete - promote immediately.
597 // This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
598 // This guarantees that the stop scan command will be fully processed before any subsequent connect command,
599 // preventing race conditions or overlapping operations.
600 }
601
602 ESP_LOGD(TAG, "Promoting client to connect");
603 // A connect ends the scan period a window-change restart was continuing.
604 this->skip_next_scan_end_ = false;
605#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
606 this->update_coex_preference_(true);
607#endif
608 client->connect();
609 break;
610 }
611#endif
612}
613
615 switch (state) {
616 case ScannerState::IDLE:
617 return "IDLE";
618 case ScannerState::STARTING:
619 return "STARTING";
620 case ScannerState::RUNNING:
621 return "RUNNING";
622 case ScannerState::STOPPING:
623 return "STOPPING";
624 case ScannerState::FAILED:
625 return "FAILED";
626 default:
627 return "UNKNOWN";
628 }
629}
630
631void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const {
632 ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_),
633 operation, this->scanner_state_to_string_(expected_state));
634}
635
636#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
638#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
639 if (force_ble && !this->coex_prefer_ble_) {
640 ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
641 this->coex_prefer_ble_ = true;
642 esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
643 } else if (!force_ble && this->coex_prefer_ble_) {
644 ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
645 this->coex_prefer_ble_ = false;
646 esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
647 }
648#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
649}
650#endif
651
652} // namespace esphome::esp32_ble_tracker
653
654#endif // USE_ESP32
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void clear()
Reset the per-period dedup list (call when a scan period ends).
Definition ble_device.h:274
void log_device(const char *tag, const ESPBTDevice &device)
Log the device at DEBUG the first time its MAC is seen this scan period.
void parse_scan_rst(const esp32_ble::BLEScanResult &scan_result)
Historical esp32 ingest (esp32 builds only): parse an ESP-IDF scan result.
void try_promote_discovered_clients_()
Try to promote discovered clients to ready to connect.
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
uint8_t state_version_
Version counter for loop() fast-path optimization.
StaticVector< ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT > clients_
void notify_scan_end_()
Fire on_scan_end on every listener unless a window-change restart suppressed it.
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param)
Called when a ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT event is received.
ClientStateCounts count_client_states_() const
Count clients in each state.
ble_device_base::ScannerStateCallback scanner_state_callback_
uint8_t last_processed_version_
Last state_version_ value when loop() did full processing.
void gap_scan_event_handler(const BLEScanResult &scan_result)
bool skip_next_scan_end_
Suppress the window-change restart's on_scan_end sweeps (stop and start).
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param)
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_
uint32_t desired_scan_window_(uint8_t active) const
The window to scan at for the given number of active GATT connections.
esp_ble_scan_params_t scan_params_
A structure holding the ESP BLE scan parameters.
StaticVector< ESPBTDeviceListener *, ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT > listeners_
void register_listener(ESPBTDeviceListener *listener)
uint32_t scan_timeout_ms_
Precomputed timeout value: scan_duration_ * 2000.
void update_coex_preference_(bool force_ble)
Update BLE coexistence preference.
const char * scanner_state_to_string_(ScannerState state) const
Convert scanner state enum to string for logging.
void gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param)
Called when a ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT event is received.
uint32_t scan_duration_
The interval in seconds to perform scans.
uint32_t connection_scan_window_
Window used while a GATT connection is active; set by the user, or defaulted when the window was rais...
void setup() override
Setup the FreeRTOS task and the Bluetooth stack.
void handle_scanner_failure_()
Handle scanner failure states.
void cleanup_scan_state_(bool is_stop_complete)
Common cleanup logic when transitioning scanner to IDLE state.
ble_device_base::DiscoveredDeviceLog discovered_log_
Per-period "Found device" DEBUG log with MAC dedup (shared ble_device_base impl)
void set_scanner_state_(ScannerState state)
Called to set the scanner state. Will also call callbacks to let listeners know when state is changed...
void print_bt_device_info(const ESPBTDevice &device)
StaticVector< ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT > neutral_listeners_
void process_scan_result_(const BLEScanResult &scan_result)
Process a single scan result immediately.
void gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param)
Called when a ESP_GAP_BLE_SCAN_START_COMPLETE_EVT event is received.
void log_unexpected_state_(const char *operation, ScannerState expected_state) const
Log an unexpected scanner state.
bool stop_scan_()
Returns true when a stop was issued to the controller.
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override
void start_scan_(bool first)
Start a single scan by setting up the parameters and doing some esp-idf calls.
Base class for BLE GATT clients that connect to remote devices.
void set_tracker_state_version(uint8_t *version)
Called by ESP32BLETracker::register_client() to enable state change notifications.
virtual bool wants_parsed_advertisements()
False keeps the tracker from building parsed ESPBTDevice objects on this client's account (raw consum...
void add_global_state_listener(OTAGlobalStateListener *listener)
bool state
Definition fan.h:2
ScannerState
Scanner lifecycle, wire-value aligned with the api enum so consumers cast directly (pinned by static_...
Definition ble_hub.h:53
ESP32BLETracker * global_esp32_ble_tracker
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address)
Definition ble.h:38
OTAGlobalCallback * get_global_ota_callback()
constexpr float AFTER_BLUETOOTH
Definition component.h:49
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
bool is_set() const
A default-constructed slot is "no subscriber"; hubs must guard on this.
Definition ble_hub.h:47
void invoke(const RawAdvertisement &adv) const
Definition ble_hub.h:48
One raw advertisement as delivered by the controller — a borrowed view, valid only for the duration o...
Definition ble_hub.h:24
uint64_t address
Producers convert their native byte order at the emit site, so no byte-order convention crosses this ...
Definition ble_hub.h:27
void invoke(ScannerState state) const
Definition ble_hub.h:69