ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ble_client_base.cpp
Go to the documentation of this file.
1#include "ble_client_base.h"
2
4#include "esphome/core/log.h"
5
6#ifdef USE_ESP32
7
8#include <esp_gap_ble_api.h>
9#include <esp_gatt_defs.h>
10#include <esp_gattc_api.h>
11
13
14ESPHOME_LOG_TAG(TAG, "esp32_ble_client");
15
16// Connection parameters are shared with the other GATT client backends
17// (ble_device_base/ble_client_state.h) so the platforms cannot drift.
18using ble_device_base::FAST_CONN_TIMEOUT;
19using ble_device_base::FAST_MAX_CONN_INTERVAL;
20using ble_device_base::FAST_MIN_CONN_INTERVAL;
21using ble_device_base::MEDIUM_CONN_TIMEOUT;
22using ble_device_base::MEDIUM_MAX_CONN_INTERVAL;
23using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
24static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s
25static const esp_bt_uuid_t NOTIFY_DESC_UUID = {
26 .len = ESP_UUID_LEN_16,
27 .uuid =
28 {
29 .uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG,
30 },
31};
32
34 static uint8_t connection_index = 0;
35 this->connection_index_ = connection_index++;
36}
37
39 ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_, (int) st);
40 ESPBTClient::set_state(st);
41}
42
44 if (!esp32_ble::global_ble->is_active()) {
45 // ble_before_disabled_event_handler() resets the client.
46 return;
47 }
48 if (this->state() == espbt::ClientState::INIT) {
49 auto ret = esp_ble_gattc_app_register(this->app_id);
50 if (ret) {
51 ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
52 this->mark_failed();
53 }
54 this->set_state(espbt::ClientState::IDLE);
55 }
56 // If idle, we can disable the loop as connect()
57 // will enable it again when a connection is needed.
58 else if (this->state() == espbt::ClientState::IDLE) {
59 this->disable_loop();
60 } else if (this->state() == espbt::ClientState::DISCONNECTING &&
61 (millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) {
62 ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_,
63 this->address_str_);
64 // release_services() must be called before set_idle_() — if we entered DISCONNECTING
65 // via unconditional_disconnect() (which doesn't call release_services()), and ESP-IDF
66 // never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call.
67 this->release_services();
68 this->set_idle_();
69 this->on_disconnect_complete(ESP_GATT_CONN_TIMEOUT);
70 }
71}
72
74
76 auto st = this->state();
77 if (st != espbt::ClientState::IDLE && st != espbt::ClientState::INIT) {
78 // No CLOSE_EVT will come: free the services and settle the link.
79 this->release_services();
80 this->set_idle_();
81 this->on_disconnect_complete(ESP_GATT_CONN_TERMINATE_LOCAL_HOST);
82 }
83 // The interface belongs to the torn-down stack.
84 this->gattc_if_ = ESP_GATT_IF_NONE;
85 this->set_state(espbt::ClientState::INIT);
86 // An idle client runs no loop; the INIT branch must run to register again.
87 this->enable_loop();
88}
89
91 ESP_LOGCONFIG(TAG,
92 " Address: %s\n"
93 " Auto-Connect: %s\n"
94 " State: %s",
95 this->address_str(), TRUEFALSE(this->auto_connect_), espbt::client_state_to_string(this->state()));
96 if (this->status_ == ESP_GATT_NO_RESOURCES) {
97 ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
98 } else if (this->status_ != ESP_GATT_OK) {
99 ESP_LOGW(TAG, " Failed due to error code %d", this->status_);
100 }
101}
102
103#ifdef USE_ESP32_BLE_DEVICE
105 if (!this->auto_connect_)
106 return false;
107 if (this->address_ == 0 || device.address_uint64() != this->address_)
108 return false;
109 if (this->state() != espbt::ClientState::IDLE)
110 return false;
111 // Not registered on this stack yet; promoting now would stop the scan for a
112 // connect that connect() rejects anyway.
113 if (this->gattc_if_ == ESP_GATT_IF_NONE)
114 return false;
115
116 this->log_event_("Found device");
117 if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
119
120 this->set_state(espbt::ClientState::DISCOVERED);
121 this->set_address(device.address_uint64());
122 this->remote_addr_type_ = device.get_address_type();
123 return true;
124}
125#endif
126
128 // Prevent duplicate connection attempts or connecting while still disconnecting
129 if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
130 this->state() == espbt::ClientState::ESTABLISHED) {
131 ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
132 espbt::client_state_to_string(this->state()));
133 return;
134 } else if (this->state() == espbt::ClientState::DISCONNECTING) {
135 ESP_LOGW(TAG, "[%d] [%s] Cannot connect, still waiting for CLOSE_EVT to complete disconnect",
136 this->connection_index_, this->address_str_);
137 return;
138 }
139 if (this->gattc_if_ == ESP_GATT_IF_NONE) {
140 // Bluedroid drops an open on an unknown interface without any event.
141 this->log_warning_("Connect rejected, GATT app not registered");
142 // INIT stays so loop() still registers; only a promoted client goes back.
143 if (this->state() == espbt::ClientState::DISCOVERED) {
144 this->set_state(espbt::ClientState::IDLE);
145 }
146 return;
147 }
148 ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
149 this->paired_ = false;
150 // A registration whose event never arrived must not block this connection's release.
151 this->services_released_ = false;
152 this->pending_notify_regs_ = 0;
153 // A CONNECT_EVT seen while idle can leave the id of a link this attempt does not own.
154 this->conn_id_ = UNSET_CONN_ID;
155 // Enable loop for state processing
156 this->enable_loop();
157 // Immediately transition to CONNECTING to prevent duplicate connection attempts
158 this->set_state(espbt::ClientState::CONNECTING);
159
160 // Determine connection parameters based on connection type
161 if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
162 // V3 without cache needs fast params for service discovery
163 this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast");
164 } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
165 // V3 with cache can use medium params
166 this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
167 }
168 // For V1/Legacy, don't set params - use ESP-IDF defaults
169
170 // Open the connection
171 auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
173}
174
175esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
176
178 if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
179 ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
180 espbt::client_state_to_string(this->state()));
181 return;
182 }
183 if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
184 ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
185 this->address_str_);
186 this->want_disconnect_ = true;
187 return;
188 }
190}
191
193 // Disconnect without checking the state.
194 ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
195 if (this->state() == espbt::ClientState::DISCONNECTING) {
196 this->log_error_("Already disconnecting");
197 return;
198 }
199 if (this->conn_id_ == UNSET_CONN_ID) {
200 this->log_error_("conn id unset, cannot disconnect");
201 return;
202 }
203 auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
204 if (err != ESP_OK) {
205 //
206 // This is a fatal error, but we can't do anything about it
207 // and it likely means the BLE stack is in a bad state.
208 //
209 // In the future we might consider App.reboot() here since
210 // the BLE stack is in an indeterminate state.
211 //
212 this->log_gattc_warning_("esp_ble_gattc_close", err);
213 }
214
215 if (this->state() == espbt::ClientState::DISCOVERED) {
216 this->set_address(0);
217 this->set_state(espbt::ClientState::IDLE);
218 } else {
219 this->set_disconnecting_();
220 }
221}
222
224#ifdef USE_ESP32_BLE_DEVICE
225 for (auto &svc : this->services_)
226 delete svc; // NOLINT(cppcoreguidelines-owning-memory)
227 this->services_.clear();
228#endif
229#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
230 // Only the cache clean makes the stack's database unsafe to walk.
231 this->services_released_ = true;
232 // A stack on its way down frees its own cache.
233 if (esp32_ble::global_ble->is_active()) {
234 esp_ble_gattc_cache_clean(this->remote_bda_);
235 }
236#endif
237}
238
239esp_err_t BLEClientBase::register_for_notify(uint16_t char_handle) {
240 esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, char_handle);
241 if (err != ESP_OK)
242 return err;
243 if (this->pending_notify_regs_ == UINT8_MAX) {
244 // Saturating undercounts, so the release can run before the last registration completes.
245 // Wrapping to zero would undercount by the full range instead, which is worse.
246 this->log_warning_("Too many outstanding notify registrations to track");
247 return err;
248 }
249 this->pending_notify_regs_++;
250 return err;
251}
252
253void BLEClientBase::log_event_(const char *name) {
254 ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name);
255}
256
258 ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
259}
260
262 // Data transfer events are logged at VERBOSE level because logging to UART creates
263 // delays that cause timing issues during time-sensitive BLE operations. This is
264 // especially problematic during pairing or firmware updates which require rapid
265 // writes to many characteristics - the log spam can cause these operations to fail.
266 ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
267}
268
269void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
270 ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status);
271}
272
273void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) {
274 ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, err);
275}
276
277void BLEClientBase::log_connection_params_(const char *param_type) {
278 ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_, param_type);
279}
280
282 if (ret) {
283 this->log_gattc_warning_("esp_ble_gattc_open", ret);
284 // Don't use set_idle_() here — CONNECT_EVT never fired so conn_id_ is still UNSET_CONN_ID.
285 this->set_state(espbt::ClientState::IDLE);
286 }
287}
288
290 ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message);
291}
292
293void BLEClientBase::log_error_(const char *message, int code) {
294 ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_, message, code);
295}
296
298 ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message);
299}
300
301esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
302 uint16_t timeout, const char *param_type) {
303 esp_ble_conn_update_params_t conn_params = {};
304 memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
305 conn_params.min_int = min_interval;
306 conn_params.max_int = max_interval;
307 conn_params.latency = latency;
308 conn_params.timeout = timeout;
309 this->log_connection_params_(param_type);
310 esp_err_t err = esp_ble_gap_update_conn_params(&conn_params);
311 if (err != ESP_OK) {
312 this->log_gattc_warning_("esp_ble_gap_update_conn_params", err);
313 }
314 return err;
315}
316
317void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
318 const char *param_type) {
319 // Set preferred connection parameters before connecting
320 // These will be used when establishing the connection
321 this->log_connection_params_(param_type);
322 esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout);
323 if (err != ESP_OK) {
324 this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err);
325 }
326}
327
328bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
329 esp_ble_gattc_cb_param_t *param) {
330 if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
331 return false;
332 if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
333 return false;
334
335 ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, this->address_str_,
336 event, esp_gattc_if);
337
338 switch (event) {
339 case ESP_GATTC_REG_EVT: {
340 if (param->reg.status == ESP_GATT_OK) {
341 ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_,
342 this->app_id);
343 this->gattc_if_ = esp_gattc_if;
344 } else {
345 this->log_error_("gattc app registration failed status", param->reg.status);
346 this->status_ = param->reg.status;
347 this->mark_failed();
348 }
349 break;
350 }
351 case ESP_GATTC_OPEN_EVT: {
352 if (!this->check_addr(param->open.remote_bda))
353 return false;
354 this->log_gattc_lifecycle_event_("OPEN");
355 this->service_count_ = 0;
356
357 // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
358 // error, if the error occurred at the BTA/GATT layer. This can result in the event
359 // arriving after we've already transitioned to IDLE state.
360 if (this->state() == espbt::ClientState::IDLE) {
361 ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
362 this->address_str_, param->open.status);
363 break;
364 }
365
366 const bool open_failed = param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN;
367 // Bluedroid reports a failed open as DISCONNECT_EVT then a failing OPEN_EVT, with no CONNECT_EVT,
368 // so DISCONNECTING with no id is expected for a failure. Anything else means a bad assumption
369 // about how the ESP BT stack works, so log it.
370 const bool expected_failure =
371 open_failed && this->state() == espbt::ClientState::DISCONNECTING && this->conn_id_ == UNSET_CONN_ID;
372 if (this->state() != espbt::ClientState::CONNECTING && !expected_failure) {
373 ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
374 this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
375 }
376 if (open_failed) {
377 this->log_gattc_warning_("Connection open", param->open.status);
378 // Connection was never established so CLOSE_EVT may not follow
379 this->set_idle_();
380 break;
381 }
382 // An open on a link that is already up sends no CONNECT_EVT, so take the id here.
383 this->conn_id_ = param->open.conn_id;
384 if (this->want_disconnect_) {
385 // Disconnect was requested after connecting started,
386 // but before the connection was established. Now that we have
387 // this->conn_id_ set, we can disconnect it.
388 // Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_().
390 break;
391 }
392 // MTU negotiation already started in ESP_GATTC_CONNECT_EVT
393 this->set_state(espbt::ClientState::CONNECTED);
394 ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_);
395 if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
396 // Cached connections already connected with medium parameters, no update needed
397 // only set our state, subclients might have more stuff to do yet.
398 this->set_state_internal_(espbt::ClientState::ESTABLISHED);
399 break;
400 }
401 // For V3_WITHOUT_CACHE, we already set fast params before connecting
402 // No need to update them again here
403 this->log_event_("Searching for services");
404 esp_ble_gattc_search_service(esp_gattc_if, param->open.conn_id, nullptr);
405 break;
406 }
407 case ESP_GATTC_CONNECT_EVT: {
408 if (!this->check_addr(param->connect.remote_bda))
409 return false;
410 this->log_gattc_lifecycle_event_("CONNECT");
411 this->conn_id_ = param->connect.conn_id;
412 // Start MTU negotiation immediately as recommended by ESP-IDF examples
413 // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
414 // ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
415 // This saves ~3ms in the connection process.
416 auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
417 if (ret) {
418 this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
419 }
420 break;
421 }
422 case ESP_GATTC_DISCONNECT_EVT: {
423 if (!this->check_addr(param->disconnect.remote_bda))
424 return false;
425 // Check if we were disconnected while waiting for service discovery
426 if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
427 this->state() == espbt::ClientState::CONNECTED) {
428 this->log_warning_("Remote closed during discovery");
429 } else {
430 ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
431 param->disconnect.reason);
432 }
433 // For active disconnects (esp_ble_gattc_close), CLOSE_EVT arrives before
434 // DISCONNECT_EVT. If CLOSE_EVT already transitioned us to IDLE, don't go
435 // backwards to DISCONNECTING — the connection is already fully cleaned up.
436 if (this->state() == espbt::ClientState::IDLE) {
437 this->log_event_("DISCONNECT_EVT after CLOSE_EVT, already IDLE");
438 break;
439 }
440 // For passive disconnects (remote device disconnected or link lost),
441 // DISCONNECT_EVT arrives first. Don't transition to IDLE yet — wait for
442 // CLOSE_EVT to ensure the controller has fully freed resources (L2CAP
443 // channels, ATT resources, HCI connection handle). Transitioning to IDLE
444 // here would allow reconnection before cleanup is complete, causing the
445 // controller to reject the new connection (status=133) or crash with
446 // ASSERT_PARAM in lld_evt.c.
447 this->release_services();
448 this->set_disconnecting_();
449 break;
450 }
451
452 case ESP_GATTC_CFG_MTU_EVT: {
453 if (this->conn_id_ != param->cfg_mtu.conn_id)
454 return false;
455 if (param->cfg_mtu.status != ESP_GATT_OK) {
456 ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, this->address_str_,
457 param->cfg_mtu.mtu, param->cfg_mtu.status);
458 // No state change required here - disconnect event will follow if needed.
459 break;
460 }
461 ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_,
462 param->cfg_mtu.status, param->cfg_mtu.mtu);
463 this->mtu_ = param->cfg_mtu.mtu;
464 break;
465 }
466 case ESP_GATTC_CLOSE_EVT: {
467 if (this->conn_id_ != param->close.conn_id)
468 return false;
469 this->log_gattc_lifecycle_event_("CLOSE");
470 this->release_services();
471 this->set_idle_();
472 this->on_disconnect_complete(param->close.reason);
473 break;
474 }
475 case ESP_GATTC_SEARCH_RES_EVT: {
476 if (this->conn_id_ != param->search_res.conn_id)
477 return false;
478 this->service_count_++;
479 if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
480 // V3 clients don't need services initialized since
481 // as they use the ESP APIs to get services.
482 break;
483 }
484#ifdef USE_ESP32_BLE_DEVICE
485 BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory)
486 ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid);
487 ble_service->start_handle = param->search_res.start_handle;
488 ble_service->end_handle = param->search_res.end_handle;
489 ble_service->client = this;
490 this->services_.push_back(ble_service);
491#endif
492 break;
493 }
494 case ESP_GATTC_SEARCH_CMPL_EVT: {
495 if (this->conn_id_ != param->search_cmpl.conn_id)
496 return false;
497 this->log_gattc_lifecycle_event_("SEARCH_CMPL");
498 // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
499 // This balances performance with bandwidth usage after the critical discovery phase
500 if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
501 this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
502 } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
503#ifdef USE_ESP32_BLE_DEVICE
504#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
505 for (auto &svc : this->services_) {
506 char uuid_buf[espbt::UUID_STR_LEN];
507 svc->uuid.to_str(uuid_buf);
508 ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
509 ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
510 svc->start_handle, svc->end_handle);
511 }
512#endif
513#endif
514 }
515 ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
516 this->set_state_internal_(espbt::ClientState::ESTABLISHED);
517 break;
518 }
519 case ESP_GATTC_READ_DESCR_EVT: {
520 if (this->conn_id_ != param->write.conn_id)
521 return false;
522 this->log_gattc_data_event_("READ_DESCR");
523 break;
524 }
525 case ESP_GATTC_WRITE_DESCR_EVT: {
526 if (this->conn_id_ != param->write.conn_id)
527 return false;
528 this->log_gattc_data_event_("WRITE_DESCR");
529 break;
530 }
531 case ESP_GATTC_WRITE_CHAR_EVT: {
532 if (this->conn_id_ != param->write.conn_id)
533 return false;
534 this->log_gattc_data_event_("WRITE_CHAR");
535 break;
536 }
537 case ESP_GATTC_READ_CHAR_EVT: {
538 if (this->conn_id_ != param->read.conn_id)
539 return false;
540 this->log_gattc_data_event_("READ_CHAR");
541 break;
542 }
543 case ESP_GATTC_NOTIFY_EVT: {
544 if (this->conn_id_ != param->notify.conn_id)
545 return false;
546 this->log_gattc_data_event_("NOTIFY");
547 break;
548 }
549 case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
550 this->log_gattc_data_event_("REG_FOR_NOTIFY");
551 // The event carries no conn_id, so this is the only place the request can be retired.
552 if (this->pending_notify_regs_ > 0)
553 this->pending_notify_regs_--;
554 if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
555 this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
556 // Client is responsible for flipping the descriptor value
557 // when using the cache
558 break;
559 }
560 if (this->services_released_) {
561 // The lookup below walks the freed GATT cache, and Bluedroid asserts on it rather than erroring.
562 this->log_warning_("REG_FOR_NOTIFY after services released, notifications not enabled");
563 break;
564 }
565 esp_gattc_descr_elem_t desc_result;
566 uint16_t count = 1;
567 esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
568 this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
569 if (descr_status != ESP_GATT_OK) {
570 this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status);
571 break;
572 }
573 esp_gattc_char_elem_t char_result;
574 esp_gatt_status_t char_status =
575 esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
576 param->reg_for_notify.handle, &char_result, &count, 0);
577 if (char_status != ESP_GATT_OK) {
578 this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
579 break;
580 }
581
582 /*
583 1 = notify
584 2 = indicate
585 */
586 uint16_t notify_en = char_result.properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY ? 1 : 2;
587 esp_err_t status =
588 esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
589 (uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
590 ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
591 if (status) {
592 this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
593 }
594 break;
595 }
596
597 case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
598 this->log_gattc_data_event_("UNREG_FOR_NOTIFY");
599 break;
600 }
601
602 default:
603 // Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations
604 ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
605 break;
606 }
607 return true;
608}
609
610// clients can't call defer() directly since it's protected.
611void BLEClientBase::run_later(std::function<void()> &&f) { // NOLINT
612 this->defer(std::move(f));
613}
614
615void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
616 switch (event) {
617 // This event is sent by the server when it requests security
618 case ESP_GAP_BLE_SEC_REQ_EVT:
619 if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
620 return;
621 ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_, event);
622 esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
623 break;
624 // This event is sent once authentication has completed
625 case ESP_GAP_BLE_AUTH_CMPL_EVT:
626 if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
627 return;
628 char addr_str[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
629 format_mac_addr_upper(param->ble_security.auth_cmpl.bd_addr, addr_str);
630 ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, addr_str);
631 if (!param->ble_security.auth_cmpl.success) {
632 this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
633 } else {
634 this->paired_ = true;
635 ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_,
636 param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode);
637 }
638 break;
639
640 // There are other events we'll want to implement at some point to support things like pass key
641 // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
642 default:
643 break;
644 }
645}
646
647// Parse GATT values into a float for a sensor.
648// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/
649float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
650 // A length of one means a single octet value.
651 if (length == 0)
652 return 0;
653 if (length == 1)
654 return (float) ((uint8_t) value[0]);
655
656 switch (value[0]) {
657 case 0x1: // boolean.
658 case 0x2: // 2bit.
659 case 0x3: // nibble.
660 case 0x4: // uint8.
661 return (float) ((uint8_t) value[1]);
662 case 0x5: // uint12.
663 case 0x6: // uint16.
664 if (length > 2) {
665 return (float) encode_uint16(value[1], value[2]);
666 }
667 [[fallthrough]];
668 case 0x7: // uint24.
669 if (length > 3) {
670 return (float) encode_uint24(value[1], value[2], value[3]);
671 }
672 [[fallthrough]];
673 case 0x8: // uint32.
674 if (length > 4) {
675 return (float) encode_uint32(value[1], value[2], value[3], value[4]);
676 }
677 [[fallthrough]];
678 case 0xC: // int8.
679 return (float) ((int8_t) value[1]);
680 case 0xD: // int12.
681 case 0xE: // int16.
682 if (length > 2) {
683 return (float) encode_value<int16_t>(&value[1]);
684 }
685 [[fallthrough]];
686 case 0xF: // int24.
687 if (length > 3) {
688 return (float) encode_uint24(value[1], value[2], value[3]);
689 }
690 [[fallthrough]];
691 case 0x10: // int32.
692 if (length > 4) {
693 return (float) encode_value<int32_t>(&value[1]);
694 }
695 }
696 ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_,
697 this->address_str_, value[0], length);
698 return NAN;
699}
700
701#ifdef USE_ESP32_BLE_DEVICE
703 for (auto *svc : this->services_) {
704 if (svc->uuid == uuid)
705 return svc;
706 }
707 return nullptr;
708}
709
710BLEService *BLEClientBase::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); }
711
713 auto *svc = this->get_service(service);
714 if (svc == nullptr)
715 return nullptr;
716 return svc->get_characteristic(chr);
717}
718
719BLECharacteristic *BLEClientBase::get_characteristic(uint16_t service, uint16_t chr) {
720 return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr));
721}
722
724 for (auto *svc : this->services_) {
725 if (!svc->parsed)
726 svc->parse_characteristics();
727 for (auto *chr : svc->characteristics) {
728 if (chr->handle == handle)
729 return chr;
730 }
731 }
732 return nullptr;
733}
734
736 auto *chr = this->get_characteristic(handle);
737 if (chr != nullptr) {
738 if (!chr->parsed)
739 chr->parse_descriptors();
740 for (auto &desc : chr->descriptors) {
741 if (desc->uuid.get_uuid().uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG)
742 return desc;
743 }
744 }
745 return nullptr;
746}
747
749 auto *svc = this->get_service(service);
750 if (svc == nullptr)
751 return nullptr;
752 auto *ch = svc->get_characteristic(chr);
753 if (ch == nullptr)
754 return nullptr;
755 return ch->get_descriptor(descr);
756}
757
758BLEDescriptor *BLEClientBase::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) {
759 return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr),
760 espbt::ESPBTUUID::from_uint16(descr));
761}
762
764 for (auto *svc : this->services_) {
765 if (!svc->parsed)
766 svc->parse_characteristics();
767 for (auto *chr : svc->characteristics) {
768 if (!chr->parsed)
769 chr->parse_descriptors();
770 for (auto *desc : chr->descriptors) {
771 if (desc->handle == handle)
772 return desc;
773 }
774 }
775 }
776 return nullptr;
777}
778#endif // USE_ESP32_BLE_DEVICE
779
780} // namespace esphome::esp32_ble_client
781
782#endif // USE_ESP32
uint8_t status
Definition bl0942.h:8
void mark_failed()
Mark this component as failed.
void enable_loop()
Enable this component's loop.
Definition component.h:248
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
void disable_loop()
Disable this component's loop.
esp_ble_addr_type_t get_address_type() const
Definition ble_device.h:204
uint64_t address_uint64() const
Return MAC as packed uint64 (byte 0 in LSB — matches esp32's address_uint64).
std::vector< BLEService * > services_
char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]
void log_gattc_warning_(const char *operation, esp_gatt_status_t status)
void log_connection_params_(const char *param_type)
BLEDescriptor * get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr)
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override
void set_state(espbt::ClientState st) override
esp_err_t register_for_notify(uint16_t char_handle)
Register for notifications, holding the service release until the registration completes.
esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type)
BLECharacteristic * get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr)
virtual void set_address(uint64_t address)
void set_idle_()
Transition to IDLE and reset conn_id — call when the connection is fully dead.
void run_later(std::function< void()> &&f)
virtual void on_disconnect_complete(esp_err_t reason)
Hook called once a connection has been fully torn down (after release_services() and set_idle_()): CL...
BLEService * get_service(espbt::ESPBTUUID uuid)
void set_disconnecting_()
Transition to DISCONNECTING and start the safety timeout.
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override
bool parse_device(const espbt::ESPBTDevice &device) override
float parse_char_value(uint8_t *value, uint16_t length)
BLEDescriptor * get_config_descriptor(uint16_t handle)
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type)
void print_bt_device_info(const ESPBTDevice &device)
void set_state_internal_(ClientState st)
Set state without IDLE handling - use for direct state transitions.
const LogString * message
Definition component.cpp:35
unsigned count
Definition gif.cpp:51
int ret
ESPHOME_LOG_TAG(TAG, "esp32_ble_client")
ESP32BLETracker * global_esp32_ble_tracker
ESP32BLE * global_ble
Definition ble.cpp:798
constexpr float AFTER_BLUETOOTH
Definition component.h:49
constexpr uint32_t encode_uint24(uint8_t byte1, uint8_t byte2, uint8_t byte3)
Encode a 24-bit value given three bytes in most to least significant byte order.
Definition helpers.h:1002
constexpr T encode_value(const uint8_t *bytes)
Encode a value from its constituent bytes (from most to least significant) in an array with length si...
Definition helpers.h:1019
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:1006
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:998
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1676
static void uint32_t
uint16_t length
Definition tt21100.cpp:0
spi_device_handle_t handle