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