ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
bluetooth_connection_bluedroid.cpp
Go to the documentation of this file.
2
3#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
4
5// The in-place streamer serves the proxy's service-discovery API; backend-only
6// builds compile without the proxy headers or the streamer.
7#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
10
12#endif
13
15#include "esphome/core/hal.h"
17#include "esphome/core/log.h"
18
19#include <cstring>
20
22
23static const char *const TAG = "bluetooth_connection.bluedroid";
24
25using ble_device_base::FAST_CONN_TIMEOUT;
26using ble_device_base::FAST_MAX_CONN_INTERVAL;
27using ble_device_base::FAST_MIN_CONN_INTERVAL;
28using ble_device_base::MEDIUM_CONN_TIMEOUT;
29using ble_device_base::MEDIUM_MAX_CONN_INTERVAL;
30using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
31using esp32_ble_tracker::ClientState;
32using esp32_ble_tracker::ConnectionType;
33
34// ---- tracker surface ----
35
38
39// ---- component ----
40
42 static uint8_t connection_index = 0;
43 this->connection_index_ = connection_index++;
44}
45
47 if (!esp32_ble::global_ble->is_active()) {
48 // Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
49 // frees its slot, then re-register the app on the next enable.
50 auto down_st = this->state();
51 if (down_st != ClientState::IDLE && down_st != ClientState::INIT) {
52 this->release_services();
53 this->set_idle_();
54 this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
55 }
56 this->set_state(ClientState::INIT);
57 return;
58 }
59 auto st = this->state();
60 if (st == ClientState::INIT) {
61 // Parity with BLEClientBase: a failed registration marks the slot
62 // failed and idles it without retry.
63 auto ret = esp_ble_gattc_app_register(this->app_id);
64 if (ret) {
65 ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
66 this->mark_failed();
67 }
68 // Do not wait for REG_EVT; a dropped event must not wedge the slot.
69 this->set_idle_();
70 } else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
71 // The one teardown safety net: a lost CLOSE_EVT, or a scheduled
72 // teardown whose OPEN_EVT never arrives.
73 if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
74 ESP_LOGE(TAG, "[%d] Timeout waiting for teardown, forcing IDLE", this->connection_index_);
75 // Release before idling: a lost completion must not leak the cache.
76 this->release_services();
77 this->set_idle_(); // also clears want_disconnect_
78 this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT);
79 }
80 } else {
81 // The loop stays on while a link exists (stack-down watch, pre-started
82 // search flush); it settles only back at IDLE.
84 if (this->state() == ClientState::IDLE) {
85 this->disable_loop();
86 }
87 }
88}
89
91 ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
92 if (this->is_failed()) {
93 ESP_LOGE(TAG, " Registration failed; if the error was ESP_GATT_NO_RESOURCES, reduce the connection slots");
94 }
95}
96
97// ---- contract ops ----
98
99int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
100 // Only from idle: clobbering DISCONNECTING would open a new link the
101 // stale CLOSE_EVT then tears down.
102 if (this->state() != ClientState::IDLE) {
103 ESP_LOGW(TAG, "[%d] Connect rejected, slot busy", this->connection_index_);
104 return ESP_GATT_BUSY;
105 }
107 this->remote_addr_type_ = addr_type;
108 // Hand the request to the tracker's promote loop: it stops the scan, raises
109 // coex, and calls tracker_connect_() - the tracker owns connect timing here.
110 this->set_state(ClientState::DISCOVERED);
111 return 0;
112}
113
115 auto st = this->state();
116 if (st == ClientState::CONNECTING || st == ClientState::CONNECTED || st == ClientState::ESTABLISHED) {
117 ESP_LOGW(TAG, "[%d] Connection already in progress", this->connection_index_);
118 return;
119 }
120 if (st == ClientState::DISCONNECTING) {
121 ESP_LOGW(TAG, "[%d] Cannot connect, still waiting for CLOSE_EVT", this->connection_index_);
122 return;
123 }
124 ESP_LOGI(TAG, "[%d] 0x%02x Connecting", this->connection_index_, this->remote_addr_type_);
125 // Per-attempt latches; the search machine is reset by set_idle_(), the
126 // one door back to IDLE.
127 this->services_released_ = false;
128 this->seen_mtu_ = false;
129 this->mtu_failed_ = false;
130 this->enable_loop();
131 this->set_state(ClientState::CONNECTING);
132 if (this->connection_type_ == ConnectionType::V3_WITHOUT_CACHE) {
133 // Fast params for the discovery phase; stepped down at SEARCH_CMPL.
134 this->check_and_log_error_("esp_ble_gap_set_prefer_conn_params",
135 esp_ble_gap_set_prefer_conn_params(this->remote_bda_, FAST_MIN_CONN_INTERVAL,
136 FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT));
137 } else {
138 this->check_and_log_error_("esp_ble_gap_set_prefer_conn_params",
139 esp_ble_gap_set_prefer_conn_params(this->remote_bda_, MEDIUM_MIN_CONN_INTERVAL,
140 MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT));
141 }
142 auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_,
143 static_cast<esp_ble_addr_type_t>(this->remote_addr_type_), true);
144 if (ret) {
145 this->log_gattc_warning_("esp_ble_gattc_open", ret);
146 // CONNECT_EVT never fired; nothing to close.
147 this->set_idle_();
148 this->listener_->on_connection_state(false, 0, ret);
149 }
150}
151
153 auto st = this->state();
154 if (st == ClientState::DISCONNECTING) {
155 return 0;
156 }
157 // Nothing was opened, so no completion event will follow: report
158 // not-connected and the hub frees the slot at once (rp2 convention).
159 if (st == ClientState::IDLE) {
160 return ble_device_base::GATT_ERR_NOT_CONNECTED;
161 }
162 if (st == ClientState::DISCOVERED) {
163 // Parked for the tracker promote loop, never opened.
164 this->set_idle_();
165 return ble_device_base::GATT_ERR_NOT_CONNECTED;
166 }
167 if (st == ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
168 ESP_LOGD(TAG, "[%d] Disconnect scheduled", this->connection_index_);
169 this->want_disconnect_ = true;
170 // Arm the safety window: a lost OPEN_EVT must not leak the teardown.
172 this->enable_loop();
173 return 0;
174 }
176 return 0;
177}
178
180 ESP_LOGI(TAG, "[%d] Disconnecting (conn_id: %d)", this->connection_index_, this->conn_id_);
181 if (this->conn_id_ == UNSET_CONN_ID) {
182 // Terminal state now rather than leaning on the scheduled-teardown timer.
183 ESP_LOGE(TAG, "[%d] conn id unset, cannot disconnect", this->connection_index_);
184 this->release_services();
185 this->set_idle_();
186 this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
187 return;
188 }
189 auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
190 if (err != ESP_OK) {
191 // The stack is now in an indeterminate state for this link.
192 ESP_LOGE(TAG, "[%d] esp_ble_gattc_close error: %d", this->connection_index_, err);
193 }
194 this->set_disconnecting_();
195}
196
198 // Only a scheduled teardown (want_disconnect_ latched while the open is
199 // still in flight) is cancellable; once closing started the terminal
200 // report settles the race.
201 if (this->state() != ClientState::CONNECTING || !this->disconnect_pending()) {
202 return false;
203 }
204 this->want_disconnect_ = false;
205 return true;
206}
207
209 if (this->conn_id_ == UNSET_CONN_ID) {
210 return ble_device_base::GATT_ERR_NOT_CONNECTED;
211 }
212 switch (this->search_state_) {
214 // The pending SEARCH_CMPL reports once it lands.
216 return 0;
218 // Already landed: the flush after the connected report delivers
219 // (loop() covers a claim made outside that event drain).
221 this->enable_loop();
222 return 0;
225 return 0; // One completion is already owed to this claimant.
227 break;
228 }
229 int err = this->check_and_log_error_("esp_ble_gattc_search_service",
230 esp_ble_gattc_search_service(this->gattc_if_, this->conn_id_, nullptr));
231 if (err == 0) {
233 }
234 return err;
235}
236
238 if (this->conn_id_ == UNSET_CONN_ID) {
239 return ble_device_base::GATT_ERR_NOT_CONNECTED;
240 }
241 return this->check_and_log_error_("esp_ble_gattc_read_char", esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_,
242 handle, ESP_GATT_AUTH_REQ_NONE));
243}
244
245int BluedroidGattClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
246 if (this->conn_id_ == UNSET_CONN_ID) {
247 return ble_device_base::GATT_ERR_NOT_CONNECTED;
248 }
249 // The BTC layer copies the payload immediately, so the const_cast is safe.
250 return this->check_and_log_error_(
251 "esp_ble_gattc_write_char",
252 esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
253 response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP,
254 ESP_GATT_AUTH_REQ_NONE));
255}
256
258 if (this->conn_id_ == UNSET_CONN_ID) {
259 return ble_device_base::GATT_ERR_NOT_CONNECTED;
260 }
261 return this->check_and_log_error_(
262 "esp_ble_gattc_read_char_descr",
263 esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE));
264}
265
266int BluedroidGattClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
267 if (this->conn_id_ == UNSET_CONN_ID) {
268 return ble_device_base::GATT_ERR_NOT_CONNECTED;
269 }
270 return this->check_and_log_error_(
271 "esp_ble_gattc_write_char_descr",
272 esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
273 ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE));
274}
275
277 if (this->conn_id_ == UNSET_CONN_ID) {
278 return ble_device_base::GATT_ERR_NOT_CONNECTED;
279 }
280 // Local registration only; the CCCD write is the API client's responsibility.
281 if (enable) {
282 return this->check_and_log_error_("esp_ble_gattc_register_for_notify",
283 esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle));
284 }
285 return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify",
286 esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle));
287}
288
290 if (this->conn_id_ == UNSET_CONN_ID) {
291 return ble_device_base::GATT_ERR_NOT_CONNECTED;
292 }
293 return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT);
294}
295
296int BluedroidGattClient::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
297 uint16_t timeout) {
298 return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom");
299}
300
302 this->service_total_ = 0;
303 // Always set: terminates any in-flight stream on every cache config.
304 this->services_released_ = true;
305#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
306 // A failed clean leaves a stale database the next connection could serve
307 // as authoritative. A disabled stack invalidates its own cache; skip the
308 // meaningless call instead of warning on every OTA/ble.disable teardown.
309 if (esp32_ble::global_ble->is_active()) {
310 this->check_and_log_error_("esp_ble_gattc_cache_clean", esp_ble_gattc_cache_clean(this->remote_bda_));
311 }
312#endif
313}
314
315// ---- internals ----
316
317bool BluedroidGattClient::check_addr_(const esp_bd_addr_t &addr) const {
318 return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0;
319}
320
322 this->set_state(ClientState::IDLE);
323 this->conn_id_ = UNSET_CONN_ID;
325 this->search_status_ = 0;
326}
327
330 this->set_state(ClientState::DISCONNECTING);
331 // The loop may be disabled while idle; the safety timeout needs it.
332 this->enable_loop();
333}
334
335esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
336 uint16_t timeout, const char *param_type) {
337 esp_ble_conn_update_params_t conn_params = {{0}};
338 memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
339 conn_params.min_int = min_interval;
340 conn_params.max_int = max_interval;
341 conn_params.latency = latency;
342 conn_params.timeout = timeout;
343 ESP_LOGD(TAG, "[%d] %s conn params", this->connection_index_, param_type);
344 return this->check_and_log_error_("esp_ble_gap_update_conn_params", esp_ble_gap_update_conn_params(&conn_params));
345}
346
347int BluedroidGattClient::check_and_log_error_(const char *operation, esp_err_t err) {
348 if (err != ESP_OK) {
349 this->log_gattc_warning_(operation, err);
350 }
351 return err;
352}
353
354void BluedroidGattClient::log_gattc_warning_(const char *operation, int code) {
355 ESP_LOGW(TAG, "[%d] %s failed, status=%d", this->connection_index_, operation, code);
356}
357
358// ---- service streaming ----
359
360int BluedroidGattClient::handle_search_cmpl_(esp_gatt_status_t status) {
361 // Step down from the fast discovery params.
362 this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
363 if (status != ESP_GATT_OK) {
364 // A failed discovery reads as a clean zero from the count calls below;
365 // honoring the event status stops it becoming an authoritative empty
366 // list.
367 return status;
368 }
369 uint16_t primary = 0;
370 uint16_t secondary = 0;
371 auto primary_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_PRIMARY_SERVICE,
372 0x0001, 0xFFFF, 0, &primary);
373 auto secondary_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_SECONDARY_SERVICE,
374 0x0001, 0xFFFF, 0, &secondary);
375 if (primary_status != ESP_GATT_OK || secondary_status != ESP_GATT_OK) {
376 // A failed count must not become an authoritative empty database.
377 auto count_status = primary_status != ESP_GATT_OK ? primary_status : secondary_status;
378 this->log_gattc_warning_("esp_ble_gattc_get_attr_count", count_status);
379 return count_status;
380 }
381 this->service_total_ = primary + secondary;
382 return 0;
383}
384
385// Reports a completed search once claimed; delivery consumes the state so
386// a re-discovery issues a real search.
393
394#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
395// The wrapper's compile-time streamer detection must keep finding this
396// method; a signature drift would silently fall back to the table streamer,
397// which proxy builds compile without a materializer.
398static_assert(requires(BluedroidGattClient c, BluetoothConnection &conn) { c.stream_service_batch(conn); });
399
400// Bound by the SERVICE STREAMING HAZARD note at the top of
401// bluetooth_connection_hub.cpp: never skip a batch, never send done early.
403 if (this->services_released_) {
404 // Released under the stream: park without services-done so a partial
405 // list is never cached as authoritative (the client retries after its
406 // GetServices timeout).
407 ESP_LOGW(TAG, "[%d] [%s] Services released mid-stream, parking", conn.connection_index_, conn.address_str_);
408 conn.send_service_ = DONE_SENDING_SERVICES;
409 return;
410 }
411 if (conn.send_service_ >= this->service_total_) {
412 this->release_services();
413 conn.send_services_done_();
414 return;
415 }
416
417 // The subscriber vanished mid-stream.
418 auto *api_conn = conn.proxy_->get_api_connection();
419 if (api_conn == nullptr) {
420 ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", conn.connection_index_, conn.address_str_);
422 return;
423 }
424
425 bool use_efficient_uuids = conn.proxy_->client_supports_efficient_uuids();
427 resp.address = conn.address_;
428 size_t current_size = resp.calculate_size();
429 int16_t batch_start = conn.send_service_;
430
431 while (conn.send_service_ < this->service_total_) {
432 esp_gattc_service_elem_t service_result;
433 uint16_t svc_count = 1;
434 esp_gatt_status_t svc_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &service_result,
435 &svc_count, conn.send_service_);
436 if (svc_status != ESP_GATT_OK || svc_count == 0) {
437 ESP_LOGE(TAG, "[%d] [%s] Service walk failed (service %d), aborting stream", conn.connection_index_,
438 conn.address_str_, conn.send_service_);
439 conn.abort_service_stream(svc_status != ESP_GATT_OK ? svc_status : ESP_GATT_NOT_FOUND);
440 return;
441 }
442 uint16_t total_char_count = 0;
443 auto char_count_status =
444 esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
445 service_result.start_handle, service_result.end_handle, 0, &total_char_count);
446 if (char_count_status != ESP_GATT_OK) {
447 this->log_gattc_warning_("esp_ble_gattc_get_attr_count", char_count_status);
448 conn.abort_service_stream(char_count_status);
449 return;
450 }
451
452 // If this service likely won't fit, send the current batch first.
453 size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
454 if (!resp.services.empty() && current_size + estimated_size > MAX_PACKET_SIZE) {
455 break;
456 }
457
458 resp.services.emplace_back();
459 auto &service_resp = resp.services.back();
460 fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid,
461 ble_device_base::ESPBTUUID::from_uuid(service_result.uuid), use_efficient_uuids);
462 service_resp.handle = service_result.start_handle;
463
464 if (total_char_count > 0) {
465 service_resp.characteristics.init(total_char_count);
466 uint16_t char_offset = 0;
467 esp_gattc_char_elem_t char_result;
468 // Bounded by the count query: a misbehaving peripheral can make the
469 // enumeration return more entries than it reported.
470 while (char_offset < total_char_count) {
471 uint16_t cc = 1;
472 auto char_status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
473 service_result.end_handle, &char_result, &cc, char_offset);
474 if (char_status != ESP_GATT_OK || cc == 0) {
475 // An early terminator contradicts the count from the same cache;
476 // never stream a silently truncated list.
477 this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
478 conn.abort_service_stream(char_status != ESP_GATT_OK ? char_status : ESP_GATT_NOT_FOUND);
479 return;
480 }
481 service_resp.characteristics.emplace_back();
482 auto &characteristic_resp = service_resp.characteristics.back();
483 fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid,
484 ble_device_base::ESPBTUUID::from_uuid(char_result.uuid), use_efficient_uuids);
485 characteristic_resp.handle = char_result.char_handle;
486 characteristic_resp.properties = char_result.properties;
487
488 uint16_t total_desc_count = 0;
489 auto desc_count_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR,
490 0, 0, char_result.char_handle, &total_desc_count);
491 if (desc_count_status != ESP_GATT_OK) {
492 // Abort rather than stream the characteristic descriptor-less: a
493 // missing CCCD in a cached database breaks notifications for good.
494 this->log_gattc_warning_("esp_ble_gattc_get_attr_count", desc_count_status);
495 conn.abort_service_stream(desc_count_status);
496 return;
497 }
498 if (total_desc_count > 0) {
499 characteristic_resp.descriptors.init(total_desc_count);
500 uint16_t desc_offset = 0;
501 esp_gattc_descr_elem_t desc_result;
502 while (desc_offset < total_desc_count) {
503 uint16_t dc = 1;
504 auto desc_status = esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, char_result.char_handle,
505 &desc_result, &dc, desc_offset);
506 if (desc_status != ESP_GATT_OK || dc == 0) {
507 this->log_gattc_warning_("esp_ble_gattc_get_all_descr", desc_status);
508 conn.abort_service_stream(desc_status != ESP_GATT_OK ? desc_status : ESP_GATT_NOT_FOUND);
509 return;
510 }
511 characteristic_resp.descriptors.emplace_back();
512 auto &descriptor_resp = characteristic_resp.descriptors.back();
513 fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid,
514 ble_device_base::ESPBTUUID::from_uuid(desc_result.uuid), use_efficient_uuids);
515 descriptor_resp.handle = desc_result.handle;
516 desc_offset++;
517 }
518 }
519 char_offset++;
520 }
521 }
522
523 if (close_service_batch(resp, current_size, conn.send_service_, conn.connection_index_, conn.address_str_) !=
525 break;
526 }
527 }
528
529 // On a failed send, rewind the cursor so the batch is retried instead of
530 // silently skipped.
531 if (!api_conn->send_message(resp)) {
532 conn.note_batch_stalled_();
533 conn.send_service_ = batch_start;
534 return;
535 }
536 conn.batch_stalled_ = false;
537}
538#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
539
540// ---- events ----
541
542void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
543 auto st = this->state();
544 if (st == ClientState::IDLE) {
545 // Late OPEN_EVT after the slot went IDLE (open-error race, or the
546 // teardown net gave up): close a won link, never resurrect the slot.
547 ESP_LOGD(TAG, "[%d] OPEN_EVT in IDLE state (status=%d)", this->connection_index_, param->open.status);
548 if (param->open.status == ESP_GATT_OK || param->open.status == ESP_GATT_ALREADY_OPEN) {
549 // A failed close here leaks a live link nothing tracks; make it heard.
550 this->check_and_log_error_("esp_ble_gattc_close", esp_ble_gattc_close(this->gattc_if_, param->open.conn_id));
551 }
552 return;
553 }
554 if (st != ClientState::CONNECTING) {
555 ESP_LOGE(TAG, "[%d] OPEN_EVT in unexpected state", this->connection_index_);
556 }
557 if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
558 this->log_gattc_warning_("Connection open", param->open.status);
559 // Never established, CLOSE_EVT may not follow.
560 this->set_idle_();
561 this->listener_->on_connection_state(false, 0, param->open.status);
562 return;
563 }
564 if (this->disconnect_pending()) {
565 // Open resolved with a teardown scheduled: close now (conn_id_ stays set
566 // so CLOSE_EVT still matches).
568 return;
569 }
570 this->set_state(ClientState::CONNECTED);
571 ESP_LOGI(TAG, "[%d] Connection open", this->connection_index_);
572 if (this->connection_type_ == ConnectionType::V3_WITH_CACHE) {
573 this->set_state(ClientState::ESTABLISHED);
574 // No discovery phase: report immediately with the default MTU. The
575 // cached path never waits for (or reports) the exchange - seen_mtu_
576 // suppresses the CFG_MTU report, matching the previous esp32 behavior.
577 this->seen_mtu_ = true;
578 this->listener_->on_connection_state(true, ble_device_base::DEFAULT_ATT_MTU, 0);
579 } else {
580 // Discovery-bound connection: start the search now so it overlaps the
581 // MTU exchange. On a refusal fall back to the serialized path - the
582 // consumer's own discover_services() call retries the real search.
583 if (this->check_and_log_error_("esp_ble_gattc_search_service",
584 esp_ble_gattc_search_service(this->gattc_if_, param->open.conn_id, nullptr)) == 0) {
586 }
587 if (this->mtu_failed_ && !this->seen_mtu_) {
588 // Refused MTU request: report with the default so the consumer
589 // proceeds.
590 this->seen_mtu_ = true;
591 this->listener_->on_connection_state(true, ble_device_base::DEFAULT_ATT_MTU, 0);
593 }
594 }
595}
596
597void BluedroidGattClient::handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param) {
598 if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && this->state() == ClientState::CONNECTED) {
599 ESP_LOGW(TAG, "[%d] Remote closed during discovery", this->connection_index_);
600 } else {
601 ESP_LOGD(TAG, "[%d] DISCONNECT_EVT reason=0x%02x", this->connection_index_, param->disconnect.reason);
602 }
603 if (this->state() == ClientState::IDLE) {
604 // Active close delivers CLOSE_EVT first; never walk back to DISCONNECTING.
605 return;
606 }
607 // Passive disconnect: wait for CLOSE_EVT before going IDLE (reconnecting
608 // earlier makes the controller reject with 133 or assert) and before
609 // reporting - the wrapper frees the slot on the report, and a freed slot
610 // invites a reconnect into the still-closing link.
611 this->release_services();
612 this->set_disconnecting_();
613}
614
615bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
616 esp_ble_gattc_cb_param_t *param) {
617 if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
618 return false;
619 if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
620 return false;
621
622 switch (event) {
623 case ESP_GATTC_REG_EVT: {
624 if (param->reg.status == ESP_GATT_OK) {
625 this->gattc_if_ = esp_gattc_if;
626 } else {
627 ESP_LOGE(TAG, "[%d] gattc app registration failed, status=%d", this->connection_index_, param->reg.status);
628 this->mark_failed();
629 }
630 break;
631 }
632 case ESP_GATTC_CONNECT_EVT: {
633 if (!this->check_addr_(param->connect.remote_bda))
634 return false;
635 this->conn_id_ = param->connect.conn_id;
636 // MTU request here rather than OPEN_EVT, matching the IDF examples.
637 auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
638 if (ret) {
639 this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
640 // No CFG_MTU_EVT will follow; OPEN_EVT reports with the default.
641 this->mtu_failed_ = true;
642 }
643 break;
644 }
645 case ESP_GATTC_OPEN_EVT: {
646 if (!this->check_addr_(param->open.remote_bda))
647 return false;
648 this->handle_open_evt_(param);
649 break;
650 }
651 case ESP_GATTC_CFG_MTU_EVT: {
652 if (this->conn_id_ != param->cfg_mtu.conn_id)
653 return false;
654 if (param->cfg_mtu.status != ESP_GATT_OK) {
655 // Warn only; a disconnect will follow if the link is dead.
656 this->log_gattc_warning_("MTU exchange", param->cfg_mtu.status);
657 }
658 if (!this->seen_mtu_ && !this->disconnect_pending() && this->state() != ClientState::DISCONNECTING) {
659 // Teardown owns the link: suppress the connected report here like
660 // OPEN_EVT and SEARCH_CMPL do; the terminal report settles it.
661 this->seen_mtu_ = true;
662 // The connected report waited for the MTU; forwarded, not stored.
664 true, param->cfg_mtu.status == ESP_GATT_OK ? param->cfg_mtu.mtu : ble_device_base::DEFAULT_ATT_MTU, 0);
665 // The consumer requests discovery from inside that report; when the
666 // pre-started search already finished, complete it in the same drain.
668 }
669 break;
670 }
671 case ESP_GATTC_DISCONNECT_EVT: {
672 if (!this->check_addr_(param->disconnect.remote_bda))
673 return false;
674 this->handle_disconnect_evt_(param);
675 break;
676 }
677 case ESP_GATTC_CLOSE_EVT: {
678 if (this->conn_id_ != param->close.conn_id)
679 return false;
680 this->release_services();
681 this->set_idle_();
682 // The one connected=false report: the wrapper frees the slot on it,
683 // so it must not fire before the controller finished closing.
684 this->listener_->on_connection_state(false, 0, param->close.reason);
685 break;
686 }
687 case ESP_GATTC_SEARCH_CMPL_EVT: {
688 if (this->conn_id_ != param->search_cmpl.conn_id)
689 return false;
690 ESP_LOGI(TAG, "[%d] Service discovery complete", this->connection_index_);
691 if (this->state() == ClientState::DISCONNECTING) {
692 // Teardown owns the link; the result is never delivered, skip the
693 // work.
694 break;
695 }
696 this->search_status_ = this->handle_search_cmpl_(static_cast<esp_gatt_status_t>(param->search_cmpl.status));
697 this->search_state_ =
699 this->set_state(ClientState::ESTABLISHED);
701 break;
702 }
703 case ESP_GATTC_READ_CHAR_EVT:
704 case ESP_GATTC_READ_DESCR_EVT: {
705 if (this->conn_id_ != param->read.conn_id)
706 return false;
707 bool ok = param->read.status == ESP_GATT_OK;
708 this->listener_->on_read_result(param->read.handle, ok ? param->read.value : nullptr,
709 ok ? param->read.value_len : 0, ok ? 0 : param->read.status);
710 break;
711 }
712 case ESP_GATTC_WRITE_CHAR_EVT:
713 case ESP_GATTC_WRITE_DESCR_EVT: {
714 if (this->conn_id_ != param->write.conn_id)
715 return false;
716 this->listener_->on_write_result(param->write.handle,
717 param->write.status == ESP_GATT_OK ? 0 : param->write.status);
718 break;
719 }
720 case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
721 this->listener_->on_notify_state(param->reg_for_notify.handle, true,
722 param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status);
723 break;
724 }
725 case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
727 param->unreg_for_notify.handle, false,
728 param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
729 break;
730 }
731 case ESP_GATTC_NOTIFY_EVT: {
732 if (this->conn_id_ != param->notify.conn_id)
733 return false;
734 ESP_LOGV(TAG, "[%d] NOTIFY_EVT handle=0x%2X", this->connection_index_, param->notify.handle);
735 this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len);
736 break;
737 }
738 default:
739 break;
740 }
741 return true;
742}
743
744void BluedroidGattClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
745 switch (event) {
746 case ESP_GAP_BLE_SEC_REQ_EVT: {
747 if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
748 break;
749 // Always accept; a refused response means no AUTH_CMPL, so answer the
750 // pairing request with the failure.
751 int sec_err = this->check_and_log_error_("esp_ble_gap_security_rsp",
752 esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true));
753 if (sec_err != 0) {
754 this->listener_->on_pairing_result(sec_err);
755 }
756 break;
757 }
758 case ESP_GAP_BLE_AUTH_CMPL_EVT: {
759 if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
760 break;
762 param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason);
763 break;
764 }
765 default:
766 break;
767 }
768}
769
770} // namespace esphome::bluetooth_connection
771
772#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT
uint8_t address
Definition bl0906.h:4
uint8_t status
Definition bl0942.h:8
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:272
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
std::vector< BluetoothGATTService > services
Definition api_pb2.h:2108
static ESPBTUUID from_uuid(esp_bt_uuid_t uuid)
Source compatibility with the historical esp32_ble API (esp32 builds only).
virtual void on_notify_state(uint16_t handle, bool enabled, int error)
virtual void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len)
virtual void on_write_result(uint16_t handle, int error)
virtual void on_connection_state(bool connected, uint16_t mtu, int error)
virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error)
esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type)
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len)
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response)
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout)
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override
void stream_service_batch(BluetoothConnection &conn)
In-place service streamer (the proxy wrapper detects and prefers it): builds one api response batch d...
void abort_service_stream(conn_err_t err)
Streamer abort: latch the GATT cause, park the cursor, tear down.
bool batch_stalled_
Set while a refused batch is retrying, so only the first one warns.
void send_services_done_()
Send services-done and settle the cursor: DONE when it lands (or no subscriber), SERVICES_DONE_PENDIN...
void park_service_stream_()
Park the stream without services-done and free any held table: an interrupted stream must never be de...
void note_batch_stalled_()
Warn on the stall's leading edge only.
bool client_supports_efficient_uuids() const
Whether the subscribed API client understands 16/32-bit UUID fields.
virtual void set_state(ClientState st)
Set the client state with IDLE handling (clears want_disconnect_).
int ret
void uint64_to_mac_msb_first(uint64_t address, uint8_t out[6])
Unpack a uint64 BLE address into printable (MSB-first) byte order — the order bd_addr_t / esp_bd_addr...
Definition ble_device.h:169
size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids)
Estimate the wire size of a service (service overhead + its characteristics, assuming 128-bit UUIDs a...
BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size_t &current_size, int16_t &send_service, uint8_t connection_index, const char *address_str)
Close out the service just packed into resp (account its actual wire size, advance the cursor) and de...
void fill_gatt_uuid(std::array< uint64_t, 2 > &uuid_128, uint32_t &short_uuid, const ble_device_base::ESPBTUUID &uuid, bool use_efficient_uuids)
Fill the UUID in the appropriate wire format based on client support and UUID type (128-bit array for...
ESP32BLE * global_ble
Definition ble.cpp:775
const void size_t len
Definition hal.h:64
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
spi_device_handle_t handle