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