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