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