ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
bluetooth_connection_rp2.cpp
Go to the documentation of this file.
2
4
5#if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
6
7#include "esphome/core/hal.h"
9#include "esphome/core/log.h"
10
11#include <BluetoothLock.h>
12
13#include <cstring>
14#include <new>
15
17
18ESPHOME_LOG_TAG(TAG, "bluetooth_connection");
19
20using ble_device_base::ESPBTUUID;
21using ble_device_base::GATT_ERR_NOT_CONNECTED;
22using ble_device_base::GATT_ERR_NO_MEMORY;
23
24// Engine-owned timeouts: BTstack has a 30 s ATT transaction timeout but no
25// connect timeout — a stuck LE_CONNECTING both blocks future gap_connect calls
26// and keeps the scan inhibited, so the engine cancels after 20 s. The
27// disconnect timeout mirrors the esp32 CLOSE_EVT safety net.
28static constexpr uint32_t CONNECT_TIMEOUT_MS = 20000;
29// Budget after a cancel is in flight: its completion normally lands within
30// tens of ms, and while the engine waits it pins the stack-wide connect slot,
31// so a lost completion must cost seconds, not another full connect budget.
32static constexpr uint32_t CONNECT_CANCEL_TIMEOUT_MS = 2000;
33// Pending engines re-attempt gap_connect on this cadence instead of every
34// loop pass: the DISALLOWED path (teardown overlap) takes BluetoothLock.
35static constexpr uint32_t CONNECT_RETRY_INTERVAL_MS = 50;
36// Can-send windows normally open within a connection interval (tens of ms).
37static constexpr uint32_t WRITE_NO_RSP_TIMEOUT_MS = 500;
38
39// HCI "connection timeout" reason, reported when a teardown had to be forced.
40static constexpr uint8_t HCI_REASON_CONNECTION_TIMEOUT = 0x08;
41
42// Initiating-scan parameters and connection-event lengths for outgoing
43// connections (BTstack-specific knobs; the connection intervals themselves are
44// the shared FAST/MEDIUM parameters from ble_device_base/ble_client_state.h,
45// used in the same lifecycle places as esp32: FAST for connect and service
46// discovery, MEDIUM once established).
47static constexpr uint16_t CONN_SCAN_INTERVAL = 96; // 60 ms in 0.625 ms units
48static constexpr uint16_t CONN_SCAN_WINDOW = 48; // 30 ms in 0.625 ms units
49static constexpr uint16_t CONN_CE_MIN = 16; // 10 ms in 0.625 ms units
50static constexpr uint16_t CONN_CE_MAX = 48; // 30 ms in 0.625 ms units
51
52using ble_device_base::FAST_CONN_TIMEOUT;
53using ble_device_base::FAST_MAX_CONN_INTERVAL;
54using ble_device_base::FAST_MIN_CONN_INTERVAL;
55using ble_device_base::MEDIUM_CONN_TIMEOUT;
56using ble_device_base::MEDIUM_MAX_CONN_INTERVAL;
57using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
58
59// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
60RP2GattClient *RP2GattClient::instances[ESPHOME_BLE_GATT_CLIENT_COUNT] = {};
62btstack_packet_callback_registration_t RP2GattClient::hci_event_registration = {};
63btstack_packet_callback_registration_t RP2GattClient::sm_event_registration = {};
64RP2GattClient *RP2GattClient::connect_owner = nullptr;
65// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
66
67static ESPBTUUID uuid_from_btstack(uint16_t uuid16, const uint8_t uuid128[16]) {
68 if (uuid16 != 0) {
69 return ESPBTUUID::from_uint16(uuid16);
70 }
71 // BTstack structs carry the 128-bit form big-endian (printable order).
72 return ESPBTUUID::from_raw_reversed(uuid128);
73}
74
76 // Pre-create every pool entry so the packet handlers' allocate() calls are
77 // always a free-list pop -- the IRQ path must never reach malloc().
78 if (!this->event_pool_.warm() || !this->notify_pool_.warm()) {
79 this->mark_failed(LOG_STR("GATT event pool warm-up failed"));
80 return;
81 }
82
83 // Register this engine for IRQ-context event routing.
84 if (instance_count >= ESPHOME_BLE_GATT_CLIENT_COUNT) {
85 // Cannot happen with codegen-sized storage; refuse loudly if it ever does.
86 this->mark_failed(LOG_STR("GATT client registry full"));
87 return;
88 }
89 {
90 // One locked section: the slot store lands before the count bump, and a
91 // live HCI handler (N > 1 builds) cannot read a half-written registry.
92 BluetoothLock lock;
96 // One HCI event handler for all engine instances (BTstack supports
97 // multiple registrations, so rp2040_ble's own handler is unaffected).
98 if (hci_event_registration.callback == nullptr) {
100 hci_add_event_handler(&hci_event_registration);
102 sm_add_event_handler(&sm_event_registration);
103 }
104 }
105
106#ifdef USE_OTA_STATE_LISTENER
108#endif
109
110 this->disable_loop();
111}
112
113#ifdef USE_OTA_STATE_LISTENER
114void RP2GattClient::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
115 // esp32 parity (its tracker disconnects every client at OTA start): free
116 // the shared radio for the transfer. No restore needed; the client
117 // reconnects, and on success the device reboots anyway.
118 if (state == ota::OTA_STARTED && this->state_ != EngineState::IDLE) {
119 this->gatt_disconnect();
120 }
121}
122#endif
123
125
126void RP2GattClient::dump_config() { ESP_LOGCONFIG(TAG, "RP2 GATT client (BTstack)"); }
127
128// ---- IRQ-context handlers: copy-and-enqueue only ----
129
131 for (uint8_t i = 0; i < instance_count; i++) {
132 if (instances[i]->con_handle_ == con_handle) {
133 return instances[i];
134 }
135 }
136 return nullptr;
137}
138
139void RP2GattClient::hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
140 if (type != HCI_EVENT_PACKET) {
141 return;
142 }
143 uint8_t event_type = hci_event_packet_get_type(packet);
144 switch (event_type) {
145 case HCI_EVENT_META_GAP: {
146 if (hci_event_gap_meta_get_subevent_code(packet) != GAP_SUBEVENT_LE_CONNECTION_COMPLETE) {
147 break;
148 }
149 uint8_t status = gap_subevent_le_connection_complete_get_status(packet);
150 hci_con_handle_t con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
151 bd_addr_t peer;
152 gap_subevent_le_connection_complete_get_peer_address(packet, peer);
153 // Route by ownership, not address: gap_connect refuses a new
154 // create-connection until the previous completion is processed, so the
155 // event belongs to the owner by construction. Cancel completions carry
156 // a zeroed peer address on this controller, so an address match would
157 // drop them and pin the owner until its backstop.
159 static constexpr bd_addr_t ZERO_ADDR = {};
160 if (inst != nullptr && memcmp(peer, ZERO_ADDR, sizeof(bd_addr_t)) != 0 &&
161 memcmp(inst->peer_addr_, peer, sizeof(bd_addr_t)) != 0) {
162 // Addressed completion for a peer the owner is not connecting to: a
163 // success delayed past a cancel and an ownership handoff (the cancel
164 // idles the stack's request immediately) must not stamp the old
165 // procedure's link onto the new owner. Zero-address (cancel)
166 // completions need no such guard: BTstack only emits them while its
167 // request state is idle, and a new owner re-arms that state when it
168 // claims the token, so a stale cancel completion is swallowed by the
169 // stack, never re-attributed. A successful stale link still needs
170 // disposal (same hazard as the unowned branch below).
171 if (status == 0) {
172 gap_disconnect(con_handle);
173 }
174 break;
175 }
176 connect_owner = nullptr;
177 if (inst == nullptr) {
178 if (status == 0) {
179 // Nobody owns this late link (the owner escalated first): tear it
180 // down here or the hci_connection_t leaks and the peer answers
181 // DISALLOWED until reboot.
182 gap_disconnect(con_handle);
183 }
184 break;
185 }
186 if (status == 0) {
187 // Stamp the handle here in the BTstack context: a disconnection
188 // racing the queued CONNECTED event arrives in this same context
189 // and must route by handle (it carries no address).
190 inst->con_handle_ = con_handle;
191 }
193 break;
194 }
195 case HCI_EVENT_DISCONNECTION_COMPLETE: {
196 // Routable even against a still-queued CONNECTED event: the handle is
197 // stamped in this context at connection-complete time.
198 RP2GattClient *inst = instance_for_con_handle(hci_event_disconnection_complete_get_connection_handle(packet));
199 if (inst != nullptr) {
200 inst->enqueue_event_irq_(RP2GattEvent::DISCONNECTED, hci_event_disconnection_complete_get_reason(packet), 0);
201 }
202 break;
203 }
204 default:
205 break;
206 }
207}
208
209void RP2GattClient::sm_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
210 if (type != HCI_EVENT_PACKET) {
211 return;
212 }
213 switch (hci_event_packet_get_type(packet)) {
214 case SM_EVENT_JUST_WORKS_REQUEST:
215 // Confirming from the SM callback is the intended BTstack pattern.
216 // Unscoped on purpose: no peripheral role exists in-tree, and scoping
217 // would drop a request racing the queued CONNECTED event.
218 sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
219 break;
220 case SM_EVENT_PAIRING_COMPLETE: {
221 RP2GattClient *inst = instance_for_con_handle(sm_event_pairing_complete_get_handle(packet));
222 if (inst != nullptr) {
223 inst->enqueue_event_irq_(RP2GattEvent::PAIRING_RESULT, sm_event_pairing_complete_get_status(packet), 0);
224 }
225 break;
226 }
227 case SM_EVENT_REENCRYPTION_COMPLETE: {
228 // A bonded peer re-encrypts instead of pairing; BTstack emits only this
229 // event on that path, so it answers the PAIR request too.
230 RP2GattClient *inst = instance_for_con_handle(sm_event_reencryption_complete_get_handle(packet));
231 if (inst != nullptr) {
232 inst->enqueue_event_irq_(RP2GattEvent::PAIRING_RESULT, sm_event_reencryption_complete_get_status(packet), 0);
233 }
234 break;
235 }
236 default:
237 break;
238 }
239}
240
241void RP2GattClient::gatt_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
242 if (type != HCI_EVENT_PACKET) {
243 return;
244 }
245 uint8_t event_type = hci_event_packet_get_type(packet);
246 // Every GATT event carries the connection handle in the same position via
247 // its accessor; route on it.
248 hci_con_handle_t con_handle;
249 switch (event_type) {
250 case GATT_EVENT_MTU:
251 con_handle = gatt_event_mtu_get_handle(packet);
252 break;
253 case GATT_EVENT_SERVICE_QUERY_RESULT:
254 con_handle = gatt_event_service_query_result_get_handle(packet);
255 break;
256 case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
257 con_handle = gatt_event_characteristic_query_result_get_handle(packet);
258 break;
259 case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT:
260 con_handle = gatt_event_all_characteristic_descriptors_query_result_get_handle(packet);
261 break;
262 case GATT_EVENT_LONG_CHARACTERISTIC_VALUE_QUERY_RESULT:
263 con_handle = gatt_event_long_characteristic_value_query_result_get_handle(packet);
264 break;
265 case GATT_EVENT_LONG_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT:
266 con_handle = gatt_event_long_characteristic_descriptor_query_result_get_handle(packet);
267 break;
268 case GATT_EVENT_NOTIFICATION:
269 con_handle = gatt_event_notification_get_handle(packet);
270 break;
271 case GATT_EVENT_INDICATION:
272 con_handle = gatt_event_indication_get_handle(packet);
273 break;
274 case GATT_EVENT_QUERY_COMPLETE:
275 con_handle = gatt_event_query_complete_get_handle(packet);
276 break;
277 default:
278 return;
279 }
280 RP2GattClient *inst = instance_for_con_handle(con_handle);
281 if (inst != nullptr) {
282 inst->handle_gatt_event_irq_(event_type, packet);
283 }
284}
285
286void RP2GattClient::handle_gatt_event_irq_(uint8_t event_type, const uint8_t *packet) {
287 switch (event_type) {
288 case GATT_EVENT_MTU:
289 this->enqueue_event_irq_(RP2GattEvent::MTU_EXCHANGED, 0, gatt_event_mtu_get_MTU(packet));
290 break;
291 case GATT_EVENT_QUERY_COMPLETE:
292 this->enqueue_event_irq_(RP2GattEvent::QUERY_COMPLETE, gatt_event_query_complete_get_att_status(packet), 0);
293 break;
294 case GATT_EVENT_SERVICE_QUERY_RESULT: {
295 if (this->arena_ == nullptr) {
296 break;
297 }
298 if (this->service_count_ >= RP2_GATT_MAX_SERVICES) {
299 this->truncated_ = true;
300 break;
301 }
302 gatt_client_service_t service;
303 gatt_event_service_query_result_get_service(packet, &service);
304 auto &dst = this->arena_->services[this->service_count_];
305 dst.uuid = uuid_from_btstack(service.uuid16, service.uuid128);
306 dst.start_handle = service.start_group_handle;
307 dst.end_handle = service.end_group_handle;
308 dst.first_characteristic = 0;
309 dst.characteristic_count = 0;
310 this->service_count_++;
311 break;
312 }
313 case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT: {
314 if (this->arena_ == nullptr) {
315 break;
316 }
317 if (this->char_count_ >= RP2_GATT_MAX_CHARACTERISTICS) {
318 this->truncated_ = true;
319 break;
320 }
321 gatt_client_characteristic_t characteristic;
322 gatt_event_characteristic_query_result_get_characteristic(packet, &characteristic);
323 auto &dst = this->arena_->characteristics[this->char_count_];
324 dst.uuid = uuid_from_btstack(characteristic.uuid16, characteristic.uuid128);
325 dst.value_handle = characteristic.value_handle;
326 dst.end_handle = characteristic.end_handle;
327 dst.properties = static_cast<uint8_t>(characteristic.properties);
328 dst.first_descriptor = 0;
329 dst.descriptor_count = 0;
330 this->char_count_++;
331 break;
332 }
333 case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT: {
334 if (this->arena_ == nullptr) {
335 break;
336 }
337 if (this->desc_count_ >= RP2_GATT_MAX_DESCRIPTORS) {
338 this->truncated_ = true;
339 break;
340 }
341 gatt_client_characteristic_descriptor_t descriptor;
342 gatt_event_all_characteristic_descriptors_query_result_get_characteristic_descriptor(packet, &descriptor);
343 auto &dst = this->arena_->descriptors[this->desc_count_];
344 dst.uuid = uuid_from_btstack(descriptor.uuid16, descriptor.uuid128);
345 dst.handle = descriptor.handle;
346 this->desc_count_++;
347 break;
348 }
349 case GATT_EVENT_LONG_CHARACTERISTIC_VALUE_QUERY_RESULT:
350 // One blob per event at the reported offset; assemble into the op buffer.
351 this->assemble_blob_irq_(gatt_event_long_characteristic_value_query_result_get_value_offset(packet),
352 gatt_event_long_characteristic_value_query_result_get_value(packet),
353 gatt_event_long_characteristic_value_query_result_get_value_length(packet));
354 break;
355 case GATT_EVENT_LONG_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT:
356 this->assemble_blob_irq_(gatt_event_long_characteristic_descriptor_query_result_get_descriptor_offset(packet),
357 gatt_event_long_characteristic_descriptor_query_result_get_descriptor(packet),
358 gatt_event_long_characteristic_descriptor_query_result_get_descriptor_length(packet));
359 break;
360 case GATT_EVENT_NOTIFICATION:
361 this->enqueue_notify_irq_(gatt_event_notification_get_value_handle(packet),
362 gatt_event_notification_get_value(packet),
363 gatt_event_notification_get_value_length(packet));
364 break;
365 case GATT_EVENT_INDICATION:
366 // BTstack auto-confirms indications; deliver like a notification.
367 this->enqueue_notify_irq_(gatt_event_indication_get_value_handle(packet), gatt_event_indication_get_value(packet),
368 gatt_event_indication_get_value_length(packet));
369 break;
370 default:
371 break;
372 }
373}
374
375// NOLINTBEGIN(clang-analyzer-unix.Malloc)
376void RP2GattClient::assemble_blob_irq_(uint16_t offset, const uint8_t *data, uint16_t len) {
377 if (offset >= RP2_GATT_MAX_ATTR_LEN) {
378 return;
379 }
380 if (len > RP2_GATT_MAX_ATTR_LEN - offset) {
381 len = RP2_GATT_MAX_ATTR_LEN - offset;
382 }
383 memcpy(this->op_buffer_ + offset, data, len);
384 if (offset + len > this->op_len_) {
385 this->op_len_ = offset + len;
386 }
387}
388
389void RP2GattClient::enqueue_event_irq_(RP2GattEvent::Type type, uint8_t status, uint16_t value) {
390 RP2GattEvent *event = this->event_pool_.allocate();
391 if (event == nullptr) {
392 this->event_queue_.increment_dropped_count();
394 return;
395 }
396 event->type = type;
397 event->status = status;
398 event->value = value;
399 this->event_queue_.push(event);
401}
402
403void RP2GattClient::enqueue_notify_irq_(uint16_t handle, const uint8_t *data, uint16_t len) {
404 RP2GattNotifyEvent *event = this->notify_pool_.allocate();
405 if (event == nullptr) {
406 this->notify_queue_.increment_dropped_count();
408 return;
409 }
410 event->handle = handle;
411 event->len = len > RP2_GATT_MAX_ATTR_LEN ? RP2_GATT_MAX_ATTR_LEN : len;
412 memcpy(event->data, data, event->len);
413 this->notify_queue_.push(event);
415}
416// NOLINTEND(clang-analyzer-unix.Malloc)
417
418// ---- Main-loop state machine ----
419
421 RP2GattEvent *event;
422 while ((event = this->event_queue_.pop()) != nullptr) {
423 RP2GattEvent copy = *event;
424 this->event_pool_.release(event);
425 this->handle_event_(copy);
426 }
427
428 RP2GattNotifyEvent *notify;
429 while ((notify = this->notify_queue_.pop()) != nullptr) {
430 if (this->notify_subscribed_(notify->handle)) {
431 this->listener_->on_notify_data(notify->handle, notify->data, notify->len);
432 }
433 this->notify_pool_.release(notify);
434 }
435
436 uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
437 if (dropped > 0) {
438 // Control events must not be lost; the connection state is no longer
439 // trustworthy — recover with a forced teardown.
440 ESP_LOGE(TAG, "[%u] Dropped %u GATT control events, disconnecting", this->engine_index_, dropped);
441 this->gatt_disconnect();
442 }
443 uint16_t notify_dropped = this->notify_queue_.get_and_reset_dropped_count();
444 if (notify_dropped > 0) {
445 ESP_LOGW(TAG, "[%u] Dropped %u GATT notifications (queue full)", this->engine_index_, notify_dropped);
446 }
447
449 uint32_t now = millis();
450 if (now - this->connect_started_ > CONNECT_TIMEOUT_MS) {
451 // Never reached the radio; nothing stack-side to cancel.
452 ESP_LOGW(TAG, "[%u] Connect timeout (queued)", this->engine_index_);
453 this->fail_connection_(HCI_REASON_CONNECTION_TIMEOUT);
454 } else if (now - this->connect_retry_ms_ >= CONNECT_RETRY_INTERVAL_MS) {
455 this->connect_retry_ms_ = now;
456 if (int err = this->try_gap_connect_(); err != 0) {
457 this->fail_connection_(static_cast<uint8_t>(err));
458 }
459 }
460 } else if (this->state_ == EngineState::CONNECTING || this->state_ == EngineState::MTU_EXCHANGE) {
461 uint32_t now = millis();
462 bool cancel_in_flight = this->state_ == EngineState::CONNECTING && this->con_handle_ == HCI_CON_HANDLE_INVALID &&
464 uint32_t budget = cancel_in_flight ? CONNECT_CANCEL_TIMEOUT_MS : CONNECT_TIMEOUT_MS;
465 if (now - this->connect_started_ > budget) {
466 ESP_LOGW(TAG, "[%u] Connect timeout", this->engine_index_);
467 bool link_up = this->state_ != EngineState::CONNECTING;
468 bool cancel_sent = false;
469 if (!link_up) {
470 BluetoothLock lock;
471 // Handle check under the lock: a success completion can stamp it in
472 // the BTstack context right up to this point, and escalating past a
473 // live link would orphan it (the queued CONNECTED event is dropped
474 // by the state guard once fail_connection_ runs).
475 link_up = this->con_handle_ != HCI_CON_HANDLE_INVALID;
476 if (!link_up && connect_owner == this) {
477 // gap_connect_cancel is stack-global; only the engine whose
478 // create-connection is in flight may issue it. First timeout:
479 // cancel and give the completion a grace period. Second: the
480 // completion was lost, re-issue the cancel in case the procedure
481 // still runs (a no-op on an idle stack), then escalate.
482 gap_connect_cancel();
483 cancel_sent = !this->connect_cancel_attempted_;
484 }
485 this->connect_cancel_attempted_ = true;
486 }
487 if (link_up) {
488 // The link is up (stamped mid-timeout or MTU exchange stalled): tear
489 // it down properly so the controller frees its side; the
490 // DISCONNECTING safety net below reclaims state if the disconnection
491 // event is lost. Dropping engine state without gap_disconnect would
492 // leak the live link and this engine's GATT slot for the rest of the
493 // boot.
494 this->gatt_disconnect();
495 } else if (cancel_sent) {
496 // The cancel produces a connection-complete event with a failure
497 // status, which drives the normal failure path; restart the timer so
498 // a lost event escalates on the short cancel budget.
499 this->connect_started_ = now;
500 } else {
501 this->fail_connection_(HCI_REASON_CONNECTION_TIMEOUT);
502 }
503 }
504 } else if (this->state_ == EngineState::DISCONNECTING) {
505 if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
506 ESP_LOGW(TAG, "[%u] Disconnect timeout, forcing idle", this->engine_index_);
507 this->handle_disconnected_(HCI_REASON_CONNECTION_TIMEOUT);
508 }
509 } else if (this->state_ == EngineState::READY && this->op_type_ == OpType::WRITE_CHAR_NO_RSP &&
510 millis() - this->write_no_rsp_started_ > WRITE_NO_RSP_TIMEOUT_MS) {
511 // The can-send window never opened; report instead of hanging the op slot.
512 bool timed_out = false;
513 {
514 BluetoothLock lock;
515 // The trampoline may have just sent it; its queued result wins.
516 if (this->event_queue_.empty()) {
517 this->op_type_ = OpType::NONE;
518 timed_out = true;
519 }
520 }
521 if (timed_out) {
522 ESP_LOGW(TAG, "[%u] Deferred write timeout, handle=0x%04x", this->engine_index_, this->op_handle_);
523 this->listener_->on_write_result(this->op_handle_, GATT_CLIENT_BUSY);
524 }
525 } else if (this->state_ == EngineState::IDLE || (this->state_ == EngineState::READY && !this->op_in_flight_() &&
526 this->event_queue_.empty() && this->notify_queue_.empty())) {
527 // Nothing pending: the enqueue path re-arms the loop from any context.
528 this->disable_loop();
529 }
530}
531
533 switch (event.type) {
535 this->handle_connected_(event.status, event.value);
536 break;
538 this->handle_disconnected_(event.status);
539 break;
541 if (this->state_ == EngineState::MTU_EXCHANGE) {
542 this->mtu_ = event.value;
543 ESP_LOGV(TAG, "[%u] MTU %u", this->engine_index_, this->mtu_);
545 // Scanning resumes and runs alongside the established connection.
546 this->release_scan_inhibit_();
547 this->listener_->on_connection_state(true, this->mtu_, 0);
548 }
549 break;
551 this->handle_query_complete_(event.status);
552 break;
554 this->finish_write_no_rsp_(event.status);
555 break;
557 this->listener_->on_pairing_result(event.status);
558 break;
559 }
560}
561
563 // BTstack context: this callback IS the can-send window, so the deferred
564 // write happens here; only the result is enqueued for the main loop.
565 auto *self = static_cast<RP2GattClient *>(context);
566 if (self->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
567 return;
568 }
569 uint8_t status = gatt_client_write_value_of_characteristic_without_response(self->con_handle_, self->op_handle_,
570 self->op_len_, self->op_buffer_);
571 if ((status == GATT_CLIENT_BUSY || status == BTSTACK_ACL_BUFFERS_FULL) &&
572 gatt_client_request_to_write_without_response(&self->can_write_registration_, self->con_handle_) == 0) {
573 return; // next window retries; a failed re-arm falls through as an error
574 }
575 self->enqueue_event_irq_(RP2GattEvent::WRITE_NO_RSP_DONE, status, 0);
576}
577
579 if (this->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
580 return;
581 }
582 this->op_type_ = OpType::NONE;
583 this->listener_->on_write_result(this->op_handle_, status);
584}
585
586void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
587 if (this->state_ != EngineState::CONNECTING) {
588 return;
589 }
590 if (status != 0) {
591 ESP_LOGW(TAG, "[%u] Connect failed, status=0x%02x", this->engine_index_, status);
592 this->fail_connection_(status);
593 return;
594 }
595 if (this->cancel_requested_) {
596 // A disconnect request raced the connection complete and lost; finish
597 // the teardown instead of reporting a connection nobody wants.
598 this->con_handle_ = con_handle;
601 // No more initiating: give the radio back to the scanner during teardown.
602 this->release_scan_inhibit_();
603 uint8_t disc_status;
604 {
605 BluetoothLock lock;
606 disc_status = gap_disconnect(this->con_handle_);
607 }
608 if (disc_status != 0) {
609 this->handle_disconnected_(HCI_REASON_CONNECTION_TIMEOUT);
610 }
611 return;
612 }
613 this->con_handle_ = con_handle;
615 ESP_LOGV(TAG, "[%u] Link up, handle=0x%04x, negotiating MTU", this->engine_index_, con_handle);
616 BluetoothLock lock;
617 // One wildcard listener covers notifications/indications for every
618 // characteristic on this connection; the CCCD writes come from the API
619 // client as plain descriptor writes.
620 gatt_client_listen_for_characteristic_value_updates(&this->notification_registration_,
622 // Auto MTU negotiation is disabled (see rp2040_ble enable hooks), so the
623 // exchange is kicked explicitly; GATT_EVENT_MTU completes it. Without the
624 // explicit kick the MTU would only be exchanged on the first GATT query,
625 // which never happens on a V3_WITH_CACHE connection.
626 // Both registration calls above return void (BTstack 075a078, arduino-pico
627 // 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
628 // the connect timeout in loop().
629 gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
630}
631
633 if (this->holds_scan_inhibit_) {
634 this->holds_scan_inhibit_ = false;
636 }
637}
638
640 {
641 // Timeout escalation can fire with the completion event lost; release the
642 // stack-wide connect slot so pending engines can proceed. Until the old
643 // completion is processed, gap_connect answers any peer with DISALLOWED
644 // (the request-level guard in hci.c); a cancel idles that request
645 // immediately, and a late addressed completion from the old procedure is
646 // then dropped by the owner-peer cross-check in the handler.
647 BluetoothLock lock;
648 if (connect_owner == this) {
649 connect_owner = nullptr;
650 }
651 if (this->state_ == EngineState::CONNECTING && this->con_handle_ != HCI_CON_HANDLE_INVALID) {
652 // A success completion stamped the handle between the escalation
653 // decision and this lock: tear the link down before cleanup wipes the
654 // handle, or it leaks its pool block for the rest of the boot.
655 gap_disconnect(this->con_handle_);
656 }
657 }
658 this->cleanup_link_state_();
659 this->release_scan_inhibit_();
661 this->listener_->on_connection_state(false, 0, reason);
662}
663
665 // Drop notifications queued behind the disconnect so they cannot emit
666 // against a freed slot (address 0) on the next loop.
667 RP2GattNotifyEvent *stale;
668 while ((stale = this->notify_queue_.pop()) != nullptr) {
669 this->notify_pool_.release(stale);
670 }
671 // con_handle_ may be stamped in the BTstack context before the main loop
672 // registers the listener, so a valid handle does not imply a registration;
673 // stop_listening on an unregistered entry is a benign no-op. One lock
674 // scope around check and reset so an IRQ stamp cannot land in between
675 // (unreachable today — ownership is released before cleanup — but the
676 // invariant lives three functions away).
677 {
678 BluetoothLock lock;
679 if (this->con_handle_ != HCI_CON_HANDLE_INVALID) {
680 gatt_client_stop_listening_for_characteristic_value_updates(&this->notification_registration_);
681 }
682 this->con_handle_ = HCI_CON_HANDLE_INVALID;
683 }
685 this->cancel_requested_ = false;
686 this->op_type_ = OpType::NONE;
688 this->release_services();
689}
690
692 if (this->state_ == EngineState::IDLE) {
693 return;
694 }
695 ESP_LOGV(TAG, "[%u] Disconnected, reason=0x%02x", this->engine_index_, reason);
696 this->fail_connection_(reason);
697}
698
700 // Stale completions cannot cross connections: the loop drains the whole
701 // event queue every iteration, teardown resets op/discovery state, and a
702 // new discovery is only issued after the new link's MTU event — which in
703 // this BTstack emits no QUERY_COMPLETE (the MTU state machine is separate
704 // from the query state machine). Completions with nothing in flight are
705 // dropped below.
706 if (this->op_type_ != OpType::NONE && this->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
707 OpType op = this->op_type_;
708 this->op_type_ = OpType::NONE;
709 switch (op) {
712 // A value that is an exact multiple of MTU - 1 ends with a trailing
713 // blob request some peers refuse with INVALID_OFFSET; the read is
714 // complete, not failed.
715 if ((att_status == ATT_ERROR_INVALID_OFFSET || att_status == ATT_ERROR_ATTRIBUTE_NOT_LONG) &&
716 this->op_len_ > 0) {
717 att_status = 0;
718 }
719 this->listener_->on_read_result(this->op_handle_, this->op_buffer_, att_status == 0 ? this->op_len_ : 0,
720 att_status);
721 break;
724 this->listener_->on_write_result(this->op_handle_, att_status);
725 break;
726 default:
727 break;
728 }
729 return;
730 }
732 this->advance_discovery_(att_status);
733 }
734}
735
736// ---- Service discovery ----
737
739 if (this->state_ != EngineState::READY) {
740 return GATT_ERR_NOT_CONNECTED;
741 }
742 if (this->op_in_flight_()) {
743 return GATT_CLIENT_IN_WRONG_STATE;
744 }
745 if (this->arena_ == nullptr) {
746 // Transient: freed in release_services() right after the table streams
747 // to the API client (mirrors Bluedroid's own per-connection GATT DB
748 // lifetime on esp32). Checked: a fragmented heap must surface as a
749 // stack error the proxy can report, not a device reset.
751 this->arena_ = allocator.allocate(1);
752 if (this->arena_ == nullptr) {
753 ESP_LOGE(TAG, "[%u] Service table allocation failed", this->engine_index_);
754 return ble_device_base::GATT_ERR_NO_MEMORY;
755 }
756 new (this->arena_) ServiceArena();
757 }
758 this->service_count_ = 0;
759 this->char_count_ = 0;
760 this->desc_count_ = 0;
761 this->truncated_ = false;
763 BluetoothLock lock;
764 uint8_t status = gatt_client_discover_primary_services(&RP2GattClient::gatt_packet_handler, this->con_handle_);
765 if (status != 0) {
767 this->release_services();
768 return status;
769 }
770 return 0;
771}
772
774 auto &service = this->arena_->services[service_index];
775 gatt_client_service_t btstack_service = {};
776 btstack_service.start_group_handle = service.start_handle;
777 btstack_service.end_group_handle = service.end_handle;
778 service.first_characteristic = this->char_count_;
779 BluetoothLock lock;
780 return gatt_client_discover_characteristics_for_service(&RP2GattClient::gatt_packet_handler, this->con_handle_,
781 &btstack_service);
782}
783
785 auto &chr = this->arena_->characteristics[char_index];
786 gatt_client_characteristic_t btstack_characteristic = {};
787 btstack_characteristic.value_handle = chr.value_handle;
788 btstack_characteristic.end_handle = chr.end_handle;
789 chr.first_descriptor = this->desc_count_;
790 BluetoothLock lock;
791 return gatt_client_discover_characteristic_descriptors(&RP2GattClient::gatt_packet_handler, this->con_handle_,
792 &btstack_characteristic);
793}
794
795void RP2GattClient::advance_discovery_(uint8_t att_status) {
796 if (this->arena_ == nullptr) {
797 // release_services() is publicly callable; a table freed mid-discovery
798 // must end the discovery instead of dereferencing a null arena.
799 this->finish_discovery_(GATT_ERR_NOT_CONNECTED);
800 return;
801 }
802 if (att_status != 0) {
803 this->finish_discovery_(att_status);
804 return;
805 }
806 switch (this->discovery_phase_) {
808 if (this->service_count_ == 0) {
809 this->finish_discovery_(0);
810 return;
811 }
813 this->disc_service_cursor_ = 0;
814 if (int err = this->issue_characteristic_query_(0); err != 0) {
815 this->finish_discovery_(err);
816 }
817 break;
819 auto &service = this->arena_->services[this->disc_service_cursor_];
820 service.characteristic_count = this->char_count_ - service.first_characteristic;
821 this->disc_service_cursor_++;
823 if (int err = this->issue_characteristic_query_(this->disc_service_cursor_); err != 0) {
824 this->finish_discovery_(err);
825 }
826 return;
827 }
828 if (this->char_count_ == 0) {
829 this->finish_discovery_(0);
830 return;
831 }
833 this->disc_char_cursor_ = 0;
834 if (int err = this->issue_descriptor_query_(0); err != 0) {
835 this->finish_discovery_(err);
836 }
837 break;
838 }
840 auto &chr = this->arena_->characteristics[this->disc_char_cursor_];
841 chr.descriptor_count = this->desc_count_ - chr.first_descriptor;
842 this->disc_char_cursor_++;
844 if (int err = this->issue_descriptor_query_(this->disc_char_cursor_); err != 0) {
845 this->finish_discovery_(err);
846 }
847 return;
848 }
849 this->finish_discovery_(0);
850 break;
851 }
852 default:
853 break;
854 }
855}
856
859 ESP_LOGV(TAG, "[%u] Discovery done (err=%d): %u services, %u characteristics, %u descriptors", this->engine_index_,
860 error, this->service_count_, this->char_count_, this->desc_count_);
861 if (error == 0 && this->truncated_) {
862 // A partial table must not stream: V3 clients cache the database
863 // permanently, so an incomplete one would be wrong forever.
864 error = ATT_ERROR_INSUFFICIENT_RESOURCES;
865 }
866 if (error == 0) {
867 // Discovery no longer needs the fast interval; settle into the shared
868 // steady-state parameters (same lifecycle place as esp32). Status
869 // discarded: BTstack fails this only for an already-gone handle.
870 BluetoothLock lock;
871 gap_update_connection_parameters(this->con_handle_, MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0,
872 MEDIUM_CONN_TIMEOUT);
873 }
874 if (this->truncated_) {
875 ESP_LOGE(TAG, "Service table truncated (device exceeds %u services / %u characteristics / %u descriptors)",
876 RP2_GATT_MAX_SERVICES, RP2_GATT_MAX_CHARACTERISTICS, RP2_GATT_MAX_DESCRIPTORS);
877 }
878 if (error != 0) {
879 this->release_services();
880 }
882}
883
886 if (this->arena_ != nullptr) {
887 table.services = this->arena_->services;
889 table.descriptors = this->arena_->descriptors;
890 table.service_count = this->service_count_;
891 table.characteristic_count = this->char_count_;
892 table.descriptor_count = this->desc_count_;
893 }
894 return table;
895}
896
898 if (this->arena_ != nullptr) {
899 // Under BluetoothLock so a discovery result landing in the BTstack
900 // context cannot write into the arena mid-free.
901 BluetoothLock lock;
903 this->arena_->~ServiceArena();
904 allocator.deallocate(this->arena_, 1);
905 this->arena_ = nullptr;
906 }
907 this->service_count_ = 0;
908 this->char_count_ = 0;
909 this->desc_count_ = 0;
910 this->truncated_ = false;
911}
912
913// ---- Connection control ----
914
915int RP2GattClient::connect(uint64_t address, uint8_t addr_type) {
916 if (this->is_failed()) {
917 // setup() failed: nothing is registered for event routing and loop()
918 // never runs, so a connect could not complete or time out.
919 return GATT_ERR_NOT_CONNECTED;
920 }
921 if (this->state_ != EngineState::IDLE) {
922 return GATT_CLIENT_IN_WRONG_STATE;
923 }
924 if (!this->parent_->is_active()) {
925 return GATT_ERR_NOT_CONNECTED;
926 }
928 // BLE_ADDR_TYPE_* code space: bit 0 distinguishes public from random
929 // (resolved RPA types 2/3 connect with the underlying kind).
930 this->peer_addr_type_ = (addr_type & 1) != 0 ? BD_ADDR_TYPE_LE_RANDOM : BD_ADDR_TYPE_LE_PUBLIC;
931
932 // Stop the shared radio's scan for the duration of the connect attempt
933 // (esp32 parity: initiating and scanning contend for the radio).
934 this->holds_scan_inhibit_ = true;
935 this->parent_->inhibit_scan();
936 this->connect_cancel_attempted_ = false;
937 this->cancel_requested_ = false;
938 // Bounds the queued wait; restarted when gap_connect is accepted so the
939 // radio attempt gets its full budget (HA's own ~20 s timeout arbitrates the
940 // sum via a disconnect request).
941 this->connect_started_ = millis();
942 if (int err = this->try_gap_connect_(); err != 0) {
943 this->release_scan_inhibit_();
944 return err;
945 }
946 this->enable_loop();
947 return 0;
948}
949
950// One outgoing LE create-connection exists stack-wide: issue it if no other
951// engine owns it, otherwise park in CONNECT_PENDING for loop() to retry.
952// Returns nonzero only for hard failures (state untouched; caller cleans up).
954 // Unlocked peek: single core, aligned pointer; a stale value costs one loop
955 // pass and the locked re-check below is authoritative. Keeps the per-loop
956 // pending retry from taking BluetoothLock just to find the radio busy.
957 if (connect_owner != nullptr) {
959 return 0;
960 }
961 uint8_t status;
962 {
963 BluetoothLock lock;
964 if (connect_owner != nullptr) {
965 status = ERROR_CODE_COMMAND_DISALLOWED;
966 } else {
967 // esp32 parity: cached connections come up at MEDIUM already (nothing
968 // consumes the fast interval without a discovery phase), so there is no
969 // post-connect update procedure to race or silently lose; sustained
970 // FAST intervals also starve WiFi on the shared CYW43 radio.
971 // Without-cache runs FAST for discovery and steps down in
972 // finish_discovery_.
974 gap_set_connection_parameters(CONN_SCAN_INTERVAL, CONN_SCAN_WINDOW,
975 cached ? MEDIUM_MIN_CONN_INTERVAL : FAST_MIN_CONN_INTERVAL,
976 cached ? MEDIUM_MAX_CONN_INTERVAL : FAST_MAX_CONN_INTERVAL, 0,
977 cached ? MEDIUM_CONN_TIMEOUT : FAST_CONN_TIMEOUT, CONN_CE_MIN, CONN_CE_MAX);
978 status = gap_connect(this->peer_addr_, this->peer_addr_type_);
979 if (status == 0) {
980 connect_owner = this;
981 // Still under the lock: a synthesized failure completion can fire in
982 // the BTstack context the instant it releases, and completion routing
983 // requires CONNECTING — set after the fact, the event is discarded
984 // and the engine burns its whole budget waiting for it.
986 this->connect_started_ = millis();
987 }
988 }
989 }
990 if (status == 0) {
991 return 0;
992 }
993 if (status == ERROR_CODE_COMMAND_DISALLOWED) {
994 // Radio busy with another engine's connect; resolved from loop().
996 return 0;
997 }
998 ESP_LOGW(TAG, "[%u] gap_connect failed, status=0x%02x", this->engine_index_, status);
999 return status;
1000}
1001
1003 switch (this->state_) {
1004 case EngineState::IDLE:
1005 return GATT_ERR_NOT_CONNECTED;
1007 return 0; // already on its way down
1009 // Nothing issued stack-side; the invalid handle takes the refused
1010 // path below without touching the stack.
1011 break;
1013 if (this->con_handle_ == HCI_CON_HANDLE_INVALID) {
1014 // The cancel can lose the race against a successful connection
1015 // complete; handle_connected_ checks this flag and finishes the
1016 // teardown instead of proceeding. It also counts as the one cancel
1017 // attempt, so a lost completion escalates on the next timeout tick.
1018 this->cancel_requested_ = true;
1019 this->connect_cancel_attempted_ = true;
1020 // Grace period for the cancel completion: the client's disconnect
1021 // often lands right at the engine's own deadline, and without the
1022 // restart the loop timeout fires first and reports before the
1023 // completion can finish the teardown cleanly.
1024 this->connect_started_ = millis();
1025 BluetoothLock lock;
1026 // Owner: the cancel completes as a failed connection-complete. Not
1027 // the owner (completion already resolved in the BTstack context): the
1028 // queued event drives the same teardown, nothing to cancel.
1029 if (connect_owner == this) {
1030 gap_connect_cancel();
1031 }
1032 return 0;
1033 }
1034 break;
1035 }
1036 default:
1037 break;
1038 }
1039 uint8_t status = ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER;
1040 if (this->con_handle_ != HCI_CON_HANDLE_INVALID) {
1041 {
1042 BluetoothLock lock;
1043 status = gap_disconnect(this->con_handle_);
1044 }
1045 if (status != 0) {
1046 ESP_LOGW(TAG, "[%u] gap_disconnect failed, status=0x%02x", this->engine_index_, status);
1047 }
1048 }
1049 if (status != 0) {
1050 // Refused (handle already gone) or never issued (CONNECT_PENDING):
1051 // complete via the event queue so the listener cannot re-enter
1052 // disconnect mid-call. BluetoothLock stops the IRQ producer, so this
1053 // main-loop push is SPSC-safe.
1054 BluetoothLock lock;
1055 this->enqueue_event_irq_(RP2GattEvent::DISCONNECTED, HCI_REASON_CONNECTION_TIMEOUT, 0);
1056 }
1059 // No more initiating: give the radio back to the scanner during teardown.
1060 this->release_scan_inhibit_();
1061 this->enable_loop();
1062 return 0;
1063}
1064
1065// ---- GATT operations (single outstanding op) ----
1066
1068 if (this->state_ != EngineState::READY) {
1069 return GATT_ERR_NOT_CONNECTED;
1070 }
1071 if (this->op_in_flight_()) {
1072 return GATT_CLIENT_IN_WRONG_STATE;
1073 }
1075 this->op_handle_ = handle;
1076 this->op_len_ = 0;
1077 BluetoothLock lock;
1078 // Long variant: plain read first, blob continuations only past MTU - 1.
1079 uint8_t status = gatt_client_read_long_value_of_characteristic_using_value_handle(&RP2GattClient::gatt_packet_handler,
1080 this->con_handle_, handle);
1081 if (status != 0) {
1082 this->op_type_ = OpType::NONE;
1083 return status;
1084 }
1085 return 0;
1086}
1087
1088int RP2GattClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
1089 if (this->state_ != EngineState::READY) {
1090 return GATT_ERR_NOT_CONNECTED;
1091 }
1092 if (len > RP2_GATT_MAX_ATTR_LEN) {
1093 return ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LENGTH;
1094 }
1095 if (!response) {
1096 // Synchronous in BTstack: the data is copied into the L2CAP buffer before
1097 // the call returns, and no completion event exists — synthesize one so
1098 // the wire behavior matches esp32 (which reports write-no-response too).
1099 uint8_t status;
1100 {
1101 BluetoothLock lock;
1102 if (this->op_type_ == OpType::WRITE_CHAR_NO_RSP) {
1103 // A deferred write is parked; sending now would overtake it.
1104 return GATT_CLIENT_BUSY;
1105 }
1106 status = gatt_client_write_value_of_characteristic_without_response(this->con_handle_, handle, len,
1107 const_cast<uint8_t *>(data));
1108 // BTSTACK_ACL_BUFFERS_FULL is the same transient flow control one layer
1109 // down (L2CAP), so it defers identically.
1110 if (status == GATT_CLIENT_BUSY || status == BTSTACK_ACL_BUFFERS_FULL) {
1111 if (this->op_in_flight_()) {
1112 // The op buffer is owned; bounce the busy to the caller as before.
1113 return status;
1114 }
1115 // Stash the payload and send from the can-send callback.
1116 memcpy(this->op_buffer_, data, len);
1118 this->op_handle_ = handle;
1119 this->op_len_ = len;
1120 this->write_no_rsp_started_ = millis();
1122 this->can_write_registration_.context = this;
1123 uint8_t req = gatt_client_request_to_write_without_response(&this->can_write_registration_, this->con_handle_);
1124 if (req != 0 && req != ERROR_CODE_COMMAND_DISALLOWED) {
1125 this->op_type_ = OpType::NONE;
1126 return req;
1127 }
1128 // COMMAND_DISALLOWED = still armed from a timed-out deferral; that
1129 // registration sends the newly parked payload. Keep the loop running
1130 // so the deadline below can fire on a stalled link.
1131 this->enable_loop();
1132 return 0;
1133 }
1134 }
1135 if (status == 0) {
1136 this->listener_->on_write_result(handle, 0);
1137 }
1138 return status;
1139 }
1140 if (this->op_in_flight_()) {
1141 return GATT_CLIENT_IN_WRONG_STATE;
1142 }
1143 // BTstack keeps the caller's pointer until the request is sent; the payload
1144 // must live in engine-owned storage across the async operation.
1145 memcpy(this->op_buffer_, data, len);
1147 this->op_handle_ = handle;
1148 BluetoothLock lock;
1149 uint8_t status;
1150 if (len <= this->mtu_ - 3) {
1151 status = gatt_client_write_value_of_characteristic(&RP2GattClient::gatt_packet_handler, this->con_handle_, handle,
1152 len, this->op_buffer_);
1153 } else {
1154 status = gatt_client_write_long_value_of_characteristic(&RP2GattClient::gatt_packet_handler, this->con_handle_,
1155 handle, len, this->op_buffer_);
1156 }
1157 if (status != 0) {
1158 this->op_type_ = OpType::NONE;
1159 return status;
1160 }
1161 return 0;
1162}
1163
1165 if (this->state_ != EngineState::READY) {
1166 return GATT_ERR_NOT_CONNECTED;
1167 }
1168 if (this->op_in_flight_()) {
1169 return GATT_CLIENT_IN_WRONG_STATE;
1170 }
1172 this->op_handle_ = handle;
1173 this->op_len_ = 0;
1174 BluetoothLock lock;
1175 uint8_t status = gatt_client_read_long_characteristic_descriptor_using_descriptor_handle(
1177 if (status != 0) {
1178 this->op_type_ = OpType::NONE;
1179 return status;
1180 }
1181 return 0;
1182}
1183
1184int RP2GattClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
1185 if (this->state_ != EngineState::READY) {
1186 return GATT_ERR_NOT_CONNECTED;
1187 }
1188 if (this->op_in_flight_()) {
1189 return GATT_CLIENT_IN_WRONG_STATE;
1190 }
1191 if (len > RP2_GATT_MAX_ATTR_LEN) {
1192 return ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LENGTH;
1193 }
1194 memcpy(this->op_buffer_, data, len);
1196 this->op_handle_ = handle;
1197 BluetoothLock lock;
1198 uint8_t status = gatt_client_write_characteristic_descriptor_using_descriptor_handle(
1200 if (status != 0) {
1201 this->op_type_ = OpType::NONE;
1202 return status;
1203 }
1204 return 0;
1205}
1206
1208 if (this->state_ != EngineState::READY) {
1209 return GATT_ERR_NOT_CONNECTED;
1210 }
1211 BluetoothLock lock;
1212 sm_request_pairing(this->con_handle_); // void API; completion via SM events
1213 return 0;
1214}
1215
1217 if (this->state_ != EngineState::READY) {
1218 return GATT_ERR_NOT_CONNECTED;
1219 }
1220 // The CCCD write arrives separately as a descriptor write (V3 semantics);
1221 // this call only gates local delivery via the subscription list.
1222 if (enable) {
1223 if (!this->notify_subscribed_(handle)) {
1224 if (this->notify_subscription_count_ >= RP2_GATT_MAX_NOTIFY_SUBSCRIPTIONS) {
1225 return GATT_ERR_NO_MEMORY;
1226 }
1228 }
1229 } else {
1230 for (uint8_t i = 0; i < this->notify_subscription_count_; i++) {
1231 if (this->notify_subscriptions_[i] == handle) {
1233 break;
1234 }
1235 }
1236 }
1237 this->listener_->on_notify_state(handle, enable, 0);
1238 return 0;
1239}
1240
1242 for (uint8_t i = 0; i < this->notify_subscription_count_; i++) {
1243 if (this->notify_subscriptions_[i] == handle) {
1244 return true;
1245 }
1246 }
1247 return false;
1248}
1249
1250int RP2GattClient::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
1251 uint16_t timeout) {
1252 if (this->state_ != EngineState::READY) {
1253 return GATT_ERR_NOT_CONNECTED;
1254 }
1255 BluetoothLock lock;
1256 return gap_update_connection_parameters(this->con_handle_, min_interval, max_interval, latency, timeout);
1257}
1258
1260 uint8_t mac[MAC_ADDRESS_SIZE];
1262 bool found = false;
1263 BluetoothLock lock;
1264 // Exhaustive: the db keys on (type, address), so stale entries can share
1265 // the same address bytes under different types.
1266 for (int i = 0; i < le_device_db_max_count(); i++) {
1267 int addr_type = 0;
1268 bd_addr_t addr;
1269 le_device_db_info(i, &addr_type, addr, nullptr);
1270 if (addr_type != BD_ADDR_TYPE_UNKNOWN && memcmp(addr, mac, sizeof(bd_addr_t)) == 0) {
1271 le_device_db_remove(i);
1272 found = true;
1273 }
1274 }
1275 if (found) {
1276 return CONN_OK;
1277 }
1278 // No bond for this address; the shared error domain has no closer code
1279 // (esp32 parity: its remove-bond call also errors for an unknown address).
1280 return GATT_NOT_CONNECTED;
1281}
1282
1283} // namespace esphome::bluetooth_connection
1284
1285#endif // USE_RP2040_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:274
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2222
void deallocate(T *p, size_t n)
Definition helpers.h:2279
T * allocate(size_t n)
Definition helpers.h:2249
static ESPBTUUID from_uint16(uint16_t uuid)
static ESPBTUUID from_raw_reversed(const uint8_t *data)
Construct from raw 16-byte big-endian UUID (reversed on store).
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)
void enqueue_notify_irq_(uint16_t handle, const uint8_t *data, uint16_t len)
esphome::EventPool< RP2GattNotifyEvent, RP2_GATT_NOTIFY_QUEUE_SIZE - 1 > notify_pool_
btstack_context_callback_registration_t can_write_registration_
void enqueue_event_irq_(RP2GattEvent::Type type, uint8_t status, uint16_t value)
void handle_connected_(uint8_t status, uint16_t con_handle)
void handle_gatt_event_irq_(uint8_t event_type, const uint8_t *packet)
static void sm_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size)
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response)
static btstack_packet_callback_registration_t hci_event_registration
static RP2GattClient * instance_for_con_handle(hci_con_handle_t con_handle)
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override
static void hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size)
ble_device_base::GattServiceTable get_service_table()
int connect(uint64_t address, uint8_t addr_type)
std::array< uint16_t, RP2_GATT_MAX_NOTIFY_SUBSCRIPTIONS > notify_subscriptions_
esphome::LockFreeQueue< RP2GattNotifyEvent, RP2_GATT_NOTIFY_QUEUE_SIZE > notify_queue_
ble_device_base::GattClientListener * listener_
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout)
static btstack_packet_callback_registration_t sm_event_registration
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len)
static RP2GattClient * instances[ESPHOME_BLE_GATT_CLIENT_COUNT]
void assemble_blob_irq_(uint16_t offset, const uint8_t *data, uint16_t len)
static void gatt_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size)
esphome::LockFreeQueue< RP2GattEvent, RP2_GATT_EVENT_QUEUE_SIZE > event_queue_
esphome::EventPool< RP2GattEvent, RP2_GATT_EVENT_QUEUE_SIZE - 1 > event_pool_
void add_global_state_listener(OTAGlobalStateListener *listener)
void inhibit_scan()
Pause the physical scan for the duration of a GATT connect attempt (initiating and scanning contend f...
uint8_t type
bool state
Definition fan.h:2
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
conn_err_t unpair_device(uint64_t address)
ESPHOME_LOG_TAG(TAG, "bluetooth_connection")
ble_device_base::ESPBTUUID ESPBTUUID
Definition ble_uuid.h:18
OTAGlobalCallback * get_global_ota_callback()
constexpr float AFTER_BLUETOOTH
Definition component.h:49
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
Borrowed view of the backend-owned service table.
const GattCharacteristic * characteristics
ble_device_base::GattService services[RP2_GATT_MAX_SERVICES]
ble_device_base::GattCharacteristic characteristics[RP2_GATT_MAX_CHARACTERISTICS]
ble_device_base::GattDescriptor descriptors[RP2_GATT_MAX_DESCRIPTORS]
SemaphoreHandle_t lock
spi_device_handle_t handle