ESPHome 2026.3.0-dev
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#define MBEDTLS_AES_ALT
31#include <aes_alt.h>
32
33// bt_trace.h
34#undef TAG
35
37
38static const char *const TAG = "esp32_ble_tracker";
39
40// BLE advertisement max: 31 bytes adv data + 31 bytes scan response
41static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62;
42
43ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
44
46 switch (state) {
48 return "INIT";
50 return "DISCONNECTING";
52 return "IDLE";
54 return "DISCOVERED";
56 return "CONNECTING";
58 return "CONNECTED";
60 return "ESTABLISHED";
61 default:
62 return "UNKNOWN";
63 }
64}
65
67
69 if (this->parent_->is_failed()) {
70 this->mark_failed();
71 ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
72 return;
73 }
74
76
77#ifdef USE_OTA_STATE_LISTENER
79#endif
80}
81
82#ifdef USE_OTA_STATE_LISTENER
84 if (state == ota::OTA_STARTED) {
85 this->stop_scan();
86#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
87 for (auto *client : this->clients_) {
88 client->disconnect();
89 }
90#endif
91 }
92}
93#endif
94
96 if (!this->parent_->is_active()) {
97 this->ble_was_disabled_ = true;
98 return;
99 } else if (this->ble_was_disabled_) {
100 this->ble_was_disabled_ = false;
101 // If the BLE stack was disabled, we need to start the scan again.
102 if (this->scan_continuous_) {
103 this->start_scan();
104 }
105 }
106
107 // Check for scan timeout - moved here from scheduler to avoid false reboots
108 // when the loop is blocked. This must run every iteration for safety.
110 switch (this->scan_timeout_state_) {
112 // Robust time comparison that handles rollover correctly
113 // This works because unsigned arithmetic wraps around predictably
114 if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
115 // First time we've seen the timeout exceeded - wait one more loop iteration
116 // This ensures all components have had a chance to process pending events
117 // This is because esp32_ble may not have run yet and called
118 // gap_scan_event_handler yet when the loop unblocks
119 ESP_LOGW(TAG, "Scan timeout exceeded");
121 }
122 break;
123 }
125 // We've waited at least one full loop iteration, and scan is still running
126 ESP_LOGE(TAG, "Scan never terminated, rebooting");
127 App.reboot();
128 break;
130 break;
131 }
132 }
133
134 // Fast path: skip expensive client state counting and processing
135 // if no state has changed since last loop iteration.
136 //
137 // How state changes ensure we reach the code below:
138 // - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
139 // scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
140 // set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
141 // STARTING, not RUNNING, so version is always incremented before this condition is true)
142 // - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
143 // - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
144 // connecting client finishes (state change), or scanner reaches RUNNING/IDLE
145 //
146 // All conditions that affect the logic below are tied to state changes that increment
147 // state_version_, so the fast path is safe.
148 if (this->state_version_ == this->last_processed_version_) {
149 return;
150 }
152
153 // State changed - do full processing
155 if (counts != this->client_state_counts_) {
156 this->client_state_counts_ = counts;
157 ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
158 this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
159 }
160
161 // Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
165 }
166 /*
167
168 Avoid starting the scanner if:
169 - we are already scanning
170 - we are connecting to a device
171 - we are disconnecting from a device
172
173 Otherwise the scanner could fail to ever start again
174 and our only way to recover is to reboot.
175
176 https://github.com/espressif/esp-idf/issues/6688
177
178 */
179
180 // Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
181 // all clients are idle (their state changes increment version when they finish)
182 if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
183#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
184 this->update_coex_preference_(false);
185#endif
186 if (this->scan_continuous_) {
187 this->start_scan_(false); // first = false
188 }
189 }
190 // Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
191 // or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
192 // All these trigger state_version_ increment, so we'll process and check promotion eligibility.
193 // We check both RUNNING and IDLE states because:
194 // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
195 // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
196 if (counts.discovered && !counts.connecting &&
197 (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
199 }
200}
201
203
205 ESP_LOGD(TAG, "Stopping scan.");
206 this->scan_continuous_ = false;
207 this->stop_scan_();
208}
209
211
214 // If scanner is already idle, there's nothing to stop - this is not an error
215 if (this->scanner_state_ != ScannerState::IDLE) {
216 ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
217 }
218 return;
219 }
220 // Reset timeout state machine when stopping scan
223 esp_err_t err = esp_ble_gap_stop_scanning();
224 if (err != ESP_OK) {
225 ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err);
226 return;
227 }
228}
229
231 if (!this->parent_->is_active()) {
232 ESP_LOGW(TAG, "Cannot start scan while ESP32BLE is disabled.");
233 return;
234 }
235 if (this->scanner_state_ != ScannerState::IDLE) {
236 this->log_unexpected_state_("start scan", ScannerState::IDLE);
237 return;
238 }
240 ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
241 if (!first) {
242#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
243 for (auto *listener : this->listeners_)
244 listener->on_scan_end();
245#endif
246 }
247#ifdef USE_ESP32_BLE_DEVICE
248 this->already_discovered_.clear();
249#endif
250 this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
251 this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
252 this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
253 this->scan_params_.scan_interval = this->scan_interval_;
254 this->scan_params_.scan_window = this->scan_window_;
255
256 // Start timeout monitoring in loop() instead of using scheduler
257 // This prevents false reboots when the loop is blocked
259 this->scan_timeout_ms_ = this->scan_duration_ * 2000;
261
262 esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
263 if (err != ESP_OK) {
264 ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", err);
265 return;
266 }
267 err = esp_ble_gap_start_scanning(this->scan_duration_);
268 if (err != ESP_OK) {
269 ESP_LOGE(TAG, "esp_ble_gap_start_scanning failed: %d", err);
270 return;
271 }
272}
273
275#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
276 client->app_id = ++this->app_id_;
277 // Give client a pointer to our state_version_ so it can notify us of state changes.
278 // This enables loop() fast-path optimization - we skip expensive work when no state changed.
279 // Safe because ESP32BLETracker (singleton) outlives all registered clients.
281 this->clients_.push_back(client);
283#endif
284}
285
287#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
288 listener->set_parent(this);
289 this->listeners_.push_back(listener);
291#endif
292}
293
295 this->raw_advertisements_ = false;
296 this->parse_advertisements_ = false;
297#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
298 for (auto *listener : this->listeners_) {
299 if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
300 this->parse_advertisements_ = true;
301 } else {
302 this->raw_advertisements_ = true;
303 }
304 }
305#endif
306#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
307 for (auto *client : this->clients_) {
308 if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
309 this->parse_advertisements_ = true;
310 } else {
311 this->raw_advertisements_ = true;
312 }
313 }
314#endif
315}
316
317void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
318 // Note: This handler is called from the main loop context, not directly from the BT task.
319 // The esp32_ble component queues events via enqueue_ble_event() and processes them in loop().
320 switch (event) {
321 case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
322 this->gap_scan_set_param_complete_(param->scan_param_cmpl);
323 break;
324 case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
325 this->gap_scan_start_complete_(param->scan_start_cmpl);
326 break;
327 case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
328 this->gap_scan_stop_complete_(param->scan_stop_cmpl);
329 break;
330 default:
331 break;
332 }
333 // Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
334#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
335 for (auto *client : this->clients_) {
336 client->gap_event_handler(event, param);
337 }
338#endif
339}
340
341void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
342 // Note: This handler is called from the main loop context via esp32_ble's event queue.
343 // We process advertisements immediately instead of buffering them.
344 ESP_LOGVV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
345
346 if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
347 // Process the scan result immediately
348 this->process_scan_result_(scan_result);
349 } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
350 // Scan finished on its own
352 this->log_unexpected_state_("scan complete", ScannerState::RUNNING);
353 }
354 // Scan completed naturally, perform cleanup and transition to IDLE
355 this->cleanup_scan_state_(false);
356 }
357}
358
359void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
360 // Called from main loop context via gap_event_handler after being queued from BT task
361 ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
362 if (param.status == ESP_BT_STATUS_DONE) {
363 this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
364 } else {
365 this->scan_set_param_failed_ = param.status;
366 }
367}
368
369void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
370 // Called from main loop context via gap_event_handler after being queued from BT task
371 ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
372 this->scan_start_failed_ = param.status;
374 this->log_unexpected_state_("start complete", ScannerState::STARTING);
375 }
376 if (param.status == ESP_BT_STATUS_SUCCESS) {
377 this->scan_start_fail_count_ = 0;
379 } else {
381 if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) {
383 }
384 }
385}
386
387void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) {
388 // Called from main loop context via gap_event_handler after being queued from BT task
389 // This allows us to safely transition to IDLE state and perform cleanup without race conditions
390 ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
392 this->log_unexpected_state_("stop complete", ScannerState::STOPPING);
393 }
394
395 // Perform cleanup and transition to IDLE
396 this->cleanup_scan_state_(true);
397}
398
399void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
400 esp_ble_gattc_cb_param_t *param) {
401#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
402 for (auto *client : this->clients_) {
403 client->gattc_event_handler(event, gattc_if, param);
404 }
405#endif
406}
407
409 this->scanner_state_ = state;
410 this->state_version_++;
411 for (auto *listener : this->scanner_state_listeners_) {
412 listener->on_scanner_state(state);
413 }
414}
415
416#ifdef USE_ESP32_BLE_DEVICE
417ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); }
419 if (!data.uuid.contains(0x4C, 0x00))
420 return {};
421
422 if (data.data.size() != 23)
423 return {};
424 return ESPBLEiBeacon(data.data.data());
425}
426
427void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
428 this->scan_result_ = &scan_result;
429 for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
430 this->address_[i] = scan_result.bda[i];
431 this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
432 this->rssi_ = scan_result.rssi;
433
434 // Parse advertisement data directly
435 uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len;
436 this->parse_adv_(scan_result.ble_adv, total_len);
437
438#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
439 ESP_LOGVV(TAG, "Parse Result:");
440 const char *address_type;
441 switch (this->address_type_) {
442 case BLE_ADDR_TYPE_PUBLIC:
443 address_type = "PUBLIC";
444 break;
445 case BLE_ADDR_TYPE_RANDOM:
446 address_type = "RANDOM";
447 break;
448 case BLE_ADDR_TYPE_RPA_PUBLIC:
449 address_type = "RPA_PUBLIC";
450 break;
451 case BLE_ADDR_TYPE_RPA_RANDOM:
452 address_type = "RPA_RANDOM";
453 break;
454 default:
455 address_type = "UNKNOWN";
456 break;
457 }
458 ESP_LOGVV(TAG, " Address: %02X:%02X:%02X:%02X:%02X:%02X (%s)", this->address_[0], this->address_[1],
459 this->address_[2], this->address_[3], this->address_[4], this->address_[5], address_type);
460
461 ESP_LOGVV(TAG, " RSSI: %d", this->rssi_);
462 ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str());
463 for (auto &it : this->tx_powers_) {
464 ESP_LOGVV(TAG, " TX Power: %d", it);
465 }
466 if (this->appearance_.has_value()) {
467 ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_);
468 }
469 if (this->ad_flag_.has_value()) {
470 ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
471 }
472 for (auto &uuid : this->service_uuids_) {
473 char uuid_buf[esp32_ble::UUID_STR_LEN];
474 uuid.to_str(uuid_buf);
475 ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
476 }
477 char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)];
478 for (auto &data : this->manufacturer_datas_) {
479 auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
480 if (ibeacon.has_value()) {
481 ESP_LOGVV(TAG, " Manufacturer iBeacon:");
482 char uuid_buf[esp32_ble::UUID_STR_LEN];
483 ibeacon.value().get_uuid().to_str(uuid_buf);
484 ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
485 ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
486 ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
487 ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
488 } else {
489 char uuid_buf[esp32_ble::UUID_STR_LEN];
490 data.uuid.to_str(uuid_buf);
491 ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf,
492 format_hex_pretty_to(hex_buf, data.data.data(), data.data.size()));
493 }
494 }
495 for (auto &data : this->service_datas_) {
496 ESP_LOGVV(TAG, " Service data:");
497 char uuid_buf[esp32_ble::UUID_STR_LEN];
498 data.uuid.to_str(uuid_buf);
499 ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
500 ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size()));
501 }
502
503 ESP_LOGVV(TAG, " Adv data: %s",
504 format_hex_pretty_to(hex_buf, scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len));
505#endif
506}
507
508void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
509 size_t offset = 0;
510
511 while (offset + 2 < len) {
512 const uint8_t field_length = payload[offset++]; // First byte is length of adv record
513 if (field_length == 0) {
514 continue; // Possible zero padded advertisement data
515 }
516
517 // first byte of adv record is adv record type
518 const uint8_t record_type = payload[offset++];
519 const uint8_t *record = &payload[offset];
520 const uint8_t record_length = field_length - 1;
521 offset += record_length;
522
523 // See also Generic Access Profile Assigned Numbers:
524 // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ See also ADVERTISING AND SCAN
525 // RESPONSE DATA FORMAT: https://www.bluetooth.com/specifications/bluetooth-core-specification/ (vol 3, part C, 11)
526 // See also Core Specification Supplement: https://www.bluetooth.com/specifications/bluetooth-core-specification/
527 // (called CSS here)
528
529 switch (record_type) {
530 case ESP_BLE_AD_TYPE_NAME_SHORT:
531 case ESP_BLE_AD_TYPE_NAME_CMPL: {
532 // CSS 1.2 LOCAL NAME
533 // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the
534 // device." CSS 1: Optional in this context; shall not appear more than once in a block.
535 // SHORTENED LOCAL NAME
536 // "The Shortened Local Name data type defines a shortened version of the Local Name data type. The Shortened
537 // Local Name data type shall not be used to advertise a name that is longer than the Local Name data type."
538 if (record_length > this->name_.length()) {
539 this->name_ = std::string(reinterpret_cast<const char *>(record), record_length);
540 }
541 break;
542 }
543 case ESP_BLE_AD_TYPE_TX_PWR: {
544 // CSS 1.5 TX POWER LEVEL
545 // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type."
546 // CSS 1: Optional in this context (may appear more than once in a block).
547 this->tx_powers_.push_back(*payload);
548 break;
549 }
550 case ESP_BLE_AD_TYPE_APPEARANCE: {
551 // CSS 1.12 APPEARANCE
552 // "The Appearance data type defines the external appearance of the device."
553 // See also https://www.bluetooth.com/specifications/gatt/characteristics/
554 // CSS 1: Optional in this context; shall not appear more than once in a block and shall not appear in both
555 // the AD and SRD of the same extended advertising interval.
556 this->appearance_ = *reinterpret_cast<const uint16_t *>(record);
557 break;
558 }
559 case ESP_BLE_AD_TYPE_FLAG: {
560 // CSS 1.3 FLAGS
561 // "The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the
562 // Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be
563 // omitted."
564 // CSS 1: Optional in this context; shall not appear more than once in a block.
565 this->ad_flag_ = *record;
566 break;
567 }
568 // CSS 1.1 SERVICE UUID
569 // The Service UUID data type is used to include a list of Service or Service Class UUIDs.
570 // There are six data types defined for the three sizes of Service UUIDs that may be returned:
571 // CSS 1: Optional in this context (may appear more than once in a block).
572 case ESP_BLE_AD_TYPE_16SRV_CMPL:
573 case ESP_BLE_AD_TYPE_16SRV_PART: {
574 // • 16-bit Bluetooth Service UUIDs
575 for (uint8_t i = 0; i < record_length / 2; i++) {
576 this->service_uuids_.push_back(ESPBTUUID::from_uint16(*reinterpret_cast<const uint16_t *>(record + 2 * i)));
577 }
578 break;
579 }
580 case ESP_BLE_AD_TYPE_32SRV_CMPL:
581 case ESP_BLE_AD_TYPE_32SRV_PART: {
582 // • 32-bit Bluetooth Service UUIDs
583 for (uint8_t i = 0; i < record_length / 4; i++) {
584 this->service_uuids_.push_back(ESPBTUUID::from_uint32(*reinterpret_cast<const uint32_t *>(record + 4 * i)));
585 }
586 break;
587 }
588 case ESP_BLE_AD_TYPE_128SRV_CMPL:
589 case ESP_BLE_AD_TYPE_128SRV_PART: {
590 // • Global 128-bit Service UUIDs
591 this->service_uuids_.push_back(ESPBTUUID::from_raw(record));
592 break;
593 }
594 case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: {
595 // CSS 1.4 MANUFACTURER SPECIFIC DATA
596 // "The Manufacturer Specific data type is used for manufacturer specific data. The first two data octets shall
597 // contain a company identifier from Assigned Numbers. The interpretation of any other octets within the data
598 // shall be defined by the manufacturer specified by the company identifier."
599 // CSS 1: Optional in this context (may appear more than once in a block).
600 if (record_length < 2) {
601 ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE");
602 break;
603 }
604 ServiceData data{};
605 data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast<const uint16_t *>(record));
606 data.data.assign(record + 2UL, record + record_length);
607 this->manufacturer_datas_.push_back(data);
608 break;
609 }
610
611 // CSS 1.11 SERVICE DATA
612 // "The Service Data data type consists of a service UUID with the data associated with that service."
613 // CSS 1: Optional in this context (may appear more than once in a block).
614 case ESP_BLE_AD_TYPE_SERVICE_DATA: {
615 // «Service Data - 16 bit UUID»
616 // Size: 2 or more octets
617 // The first 2 octets contain the 16 bit Service UUID fol- lowed by additional service data
618 if (record_length < 2) {
619 ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_SERVICE_DATA");
620 break;
621 }
622 ServiceData data{};
623 data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast<const uint16_t *>(record));
624 data.data.assign(record + 2UL, record + record_length);
625 this->service_datas_.push_back(data);
626 break;
627 }
628 case ESP_BLE_AD_TYPE_32SERVICE_DATA: {
629 // «Service Data - 32 bit UUID»
630 // Size: 4 or more octets
631 // The first 4 octets contain the 32 bit Service UUID fol- lowed by additional service data
632 if (record_length < 4) {
633 ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA");
634 break;
635 }
636 ServiceData data{};
637 data.uuid = ESPBTUUID::from_uint32(*reinterpret_cast<const uint32_t *>(record));
638 data.data.assign(record + 4UL, record + record_length);
639 this->service_datas_.push_back(data);
640 break;
641 }
642 case ESP_BLE_AD_TYPE_128SERVICE_DATA: {
643 // «Service Data - 128 bit UUID»
644 // Size: 16 or more octets
645 // The first 16 octets contain the 128 bit Service UUID followed by additional service data
646 if (record_length < 16) {
647 ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA");
648 break;
649 }
650 ServiceData data{};
651 data.uuid = ESPBTUUID::from_raw(record);
652 data.data.assign(record + 16UL, record + record_length);
653 this->service_datas_.push_back(data);
654 break;
655 }
656 case ESP_BLE_AD_TYPE_INT_RANGE:
657 // Avoid logging this as it's very verbose
658 break;
659 default: {
660 ESP_LOGV(TAG, "Unhandled type: advType: 0x%02x", record_type);
661 break;
662 }
663 }
664 }
665}
666
667std::string ESPBTDevice::address_str() const {
668 char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
669 return this->address_str_to(buf);
670}
671
673#endif // USE_ESP32_BLE_DEVICE
674
676 ESP_LOGCONFIG(TAG, "BLE Tracker:");
677 ESP_LOGCONFIG(TAG,
678 " Scan Duration: %" PRIu32 " s\n"
679 " Scan Interval: %.1f ms\n"
680 " Scan Window: %.1f ms\n"
681 " Scan Type: %s\n"
682 " Continuous Scanning: %s",
683 this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
684 this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
685 ESP_LOGCONFIG(TAG,
686 " Scanner State: %s\n"
687 " Connecting: %d, discovered: %d, disconnecting: %d",
689 this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
690 if (this->scan_start_fail_count_) {
691 ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
692 }
693}
694
695#ifdef USE_ESP32_BLE_DEVICE
697 const uint64_t address = device.address_uint64();
698 for (auto &disc : this->already_discovered_) {
699 if (disc == address)
700 return;
701 }
702 this->already_discovered_.push_back(address);
703
704 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
705 ESP_LOGD(TAG, "Found device %s RSSI=%d", device.address_str_to(addr_buf), device.get_rssi());
706
707 const char *address_type_s;
708 switch (device.get_address_type()) {
709 case BLE_ADDR_TYPE_PUBLIC:
710 address_type_s = "PUBLIC";
711 break;
712 case BLE_ADDR_TYPE_RANDOM:
713 address_type_s = "RANDOM";
714 break;
715 case BLE_ADDR_TYPE_RPA_PUBLIC:
716 address_type_s = "RPA_PUBLIC";
717 break;
718 case BLE_ADDR_TYPE_RPA_RANDOM:
719 address_type_s = "RPA_RANDOM";
720 break;
721 default:
722 address_type_s = "UNKNOWN";
723 break;
724 }
725
726 ESP_LOGD(TAG, " Address Type: %s", address_type_s);
727 if (!device.get_name().empty()) {
728 ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str());
729 }
730 for (auto &tx_power : device.get_tx_powers()) {
731 ESP_LOGD(TAG, " TX Power: %d", tx_power);
732 }
733}
734
735bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
736 uint8_t ecb_key[16];
737 uint8_t ecb_plaintext[16];
738 uint8_t ecb_ciphertext[16];
739
740 uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_);
741
742 memcpy(&ecb_key, irk, 16);
743 memset(&ecb_plaintext, 0, 16);
744
745 ecb_plaintext[13] = (addr64 >> 40) & 0xff;
746 ecb_plaintext[14] = (addr64 >> 32) & 0xff;
747 ecb_plaintext[15] = (addr64 >> 24) & 0xff;
748
749 mbedtls_aes_context ctx = {0, 0, {0}};
750 mbedtls_aes_init(&ctx);
751
752 if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) {
753 mbedtls_aes_free(&ctx);
754 return false;
755 }
756
757 if (mbedtls_aes_crypt_ecb(&ctx, ESP_AES_ENCRYPT, ecb_plaintext, ecb_ciphertext) != 0) {
758 mbedtls_aes_free(&ctx);
759 return false;
760 }
761
762 mbedtls_aes_free(&ctx);
763
764 return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
765 ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
766}
767
768#endif // USE_ESP32_BLE_DEVICE
769
770void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
771 // Process raw advertisements
772 if (this->raw_advertisements_) {
773#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
774 for (auto *listener : this->listeners_) {
775 listener->parse_devices(&scan_result, 1);
776 }
777#endif
778#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
779 for (auto *client : this->clients_) {
780 client->parse_devices(&scan_result, 1);
781 }
782#endif
783 }
784
785 // Process parsed advertisements
786 if (this->parse_advertisements_) {
787#ifdef USE_ESP32_BLE_DEVICE
788 ESPBTDevice device;
789 device.parse_scan_rst(scan_result);
790
791 bool found = false;
792#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
793 for (auto *listener : this->listeners_) {
794 if (listener->parse_device(device))
795 found = true;
796 }
797#endif
798
799#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
800 for (auto *client : this->clients_) {
801 if (client->parse_device(device)) {
802 found = true;
803 }
804 }
805#endif
806
807 if (!found && !this->scan_continuous_) {
808 this->print_bt_device_info(device);
809 }
810#endif // USE_ESP32_BLE_DEVICE
811 }
812}
813
814void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
815 ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
816#ifdef USE_ESP32_BLE_DEVICE
817 this->already_discovered_.clear();
818#endif
819 // Reset timeout state machine instead of cancelling scheduler timeout
821
822#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
823 for (auto *listener : this->listeners_)
824 listener->on_scan_end();
825#endif
826
828}
829
831 this->stop_scan_();
832 if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
833 ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
834 std::numeric_limits<uint8_t>::max());
835 App.reboot();
836 }
837 if (this->scan_start_failed_) {
838 ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
839 this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
840 }
841 if (this->scan_set_param_failed_) {
842 ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
843 this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
844 }
845}
846
848 // Only promote the first discovered client to avoid multiple simultaneous connections
849#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
850 for (auto *client : this->clients_) {
851 if (client->state() != ClientState::DISCOVERED) {
852 continue;
853 }
854
856 ESP_LOGD(TAG, "Stopping scan to make connection");
857 this->stop_scan_();
858 // Don't wait for scan stop complete - promote immediately.
859 // This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
860 // This guarantees that the stop scan command will be fully processed before any subsequent connect command,
861 // preventing race conditions or overlapping operations.
862 }
863
864 ESP_LOGD(TAG, "Promoting client to connect");
865#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
866 this->update_coex_preference_(true);
867#endif
868 client->connect();
869 break;
870 }
871#endif
872}
873
875 switch (state) {
877 return "IDLE";
879 return "STARTING";
881 return "RUNNING";
883 return "STOPPING";
885 return "FAILED";
886 default:
887 return "UNKNOWN";
888 }
889}
890
891void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const {
892 ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_),
893 operation, this->scanner_state_to_string_(expected_state));
894}
895
896#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
898#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
899 if (force_ble && !this->coex_prefer_ble_) {
900 ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
901 this->coex_prefer_ble_ = true;
902 esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
903 } else if (!force_ble && this->coex_prefer_ble_) {
904 ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
905 this->coex_prefer_ble_ = false;
906 esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
907 }
908#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
909}
910#endif
911
912} // namespace esphome::esp32_ble_tracker
913
914#endif // USE_ESP32
uint8_t address
Definition bl0906.h:4
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.
static ESPBTUUID from_uint32(uint32_t uuid)
Definition ble_uuid.cpp:23
static ESPBTUUID from_uint16(uint16_t uuid)
Definition ble_uuid.cpp:17
static ESPBTUUID from_raw(const uint8_t *data)
Definition ble_uuid.cpp:29
bool contains(uint8_t data1, uint8_t data2) const
Definition ble_uuid.cpp:112
void try_promote_discovered_clients_()
Try to promote discovered clients to ready to connect.
std::vector< uint64_t > already_discovered_
Vector of addresses that have already been printed in print_bt_device_info.
uint8_t state_version_
Version counter for loop() fast-path optimization.
StaticVector< ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT > clients_
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.
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override
ClientStateCounts count_client_states_() const
Count clients in each state.
uint8_t last_processed_version_
Last state_version_ value when loop() did full processing.
std::vector< BLEScannerStateListener * > scanner_state_listeners_
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.
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.
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 gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override
void print_bt_device_info(const ESPBTDevice &device)
void gap_scan_event_handler(const BLEScanResult &scan_result) override
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.
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.
static optional< ESPBLEiBeacon > from_manufacturer_data(const ServiceData &data)
struct esphome::esp32_ble_tracker::ESPBLEiBeacon::@83 beacon_data_
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.
esp_ble_addr_type_t get_address_type() const
void parse_adv_(const uint8_t *payload, uint8_t len)
void parse_scan_rst(const BLEScanResult &scan_result)
std::vector< ServiceData > manufacturer_datas_
const char * address_str_to(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf) const
Format MAC address into provided buffer, returns pointer to buffer for convenience.
const std::vector< int8_t > & get_tx_powers() const
bool resolve_irk(const uint8_t *irk) const
std::vector< ServiceData > service_datas_
bool has_value() const
Definition optional.h:92
void add_global_state_listener(OTAGlobalStateListener *listener)
bool state
Definition fan.h:2
ESP32BLETracker * global_esp32_ble_tracker
const char * client_state_to_string(ClientState state)
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address)
Definition ble.cpp:730
OTAGlobalCallback * get_global_ota_callback()
constexpr float AFTER_BLUETOOTH
Definition component.h:35
std::string size_t len
Definition helpers.h:817
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:353
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1103
Application App
Global storage of Application pointer - only one Application can exist.