Files
esphome/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp
T

1288 lines
52 KiB
C++

#include "bluetooth_connection_rp2.h"
#include "bluetooth_connection.h"
#if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <BluetoothLock.h>
#include <cstring>
#include <new>
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_connection.rp2";
using ble_device_base::ESPBTUUID;
using ble_device_base::GATT_ERR_NOT_CONNECTED;
using ble_device_base::GATT_ERR_NO_MEMORY;
// Engine-owned timeouts: BTstack has a 30 s ATT transaction timeout but no
// connect timeout — a stuck LE_CONNECTING both blocks future gap_connect calls
// and keeps the scan inhibited, so the engine cancels after 20 s. The
// disconnect timeout mirrors the esp32 CLOSE_EVT safety net.
static constexpr uint32_t CONNECT_TIMEOUT_MS = 20000;
// Budget after a cancel is in flight: its completion normally lands within
// tens of ms, and while the engine waits it pins the stack-wide connect slot,
// so a lost completion must cost seconds, not another full connect budget.
static constexpr uint32_t CONNECT_CANCEL_TIMEOUT_MS = 2000;
// Pending engines re-attempt gap_connect on this cadence instead of every
// loop pass: the DISALLOWED path (teardown overlap) takes BluetoothLock.
static constexpr uint32_t CONNECT_RETRY_INTERVAL_MS = 50;
// Can-send windows normally open within a connection interval (tens of ms).
static constexpr uint32_t WRITE_NO_RSP_TIMEOUT_MS = 500;
// HCI "connection timeout" reason, reported when a teardown had to be forced.
static constexpr uint8_t HCI_REASON_CONNECTION_TIMEOUT = 0x08;
// Initiating-scan parameters and connection-event lengths for outgoing
// connections (BTstack-specific knobs; the connection intervals themselves are
// the shared FAST/MEDIUM parameters from ble_device_base/ble_client_state.h,
// used in the same lifecycle places as esp32: FAST for connect and service
// discovery, MEDIUM once established).
static constexpr uint16_t CONN_SCAN_INTERVAL = 96; // 60 ms in 0.625 ms units
static constexpr uint16_t CONN_SCAN_WINDOW = 48; // 30 ms in 0.625 ms units
static constexpr uint16_t CONN_CE_MIN = 16; // 10 ms in 0.625 ms units
static constexpr uint16_t CONN_CE_MAX = 48; // 30 ms in 0.625 ms units
using ble_device_base::FAST_CONN_TIMEOUT;
using ble_device_base::FAST_MAX_CONN_INTERVAL;
using ble_device_base::FAST_MIN_CONN_INTERVAL;
using ble_device_base::MEDIUM_CONN_TIMEOUT;
using ble_device_base::MEDIUM_MAX_CONN_INTERVAL;
using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
RP2GattClient *RP2GattClient::instances[ESPHOME_BLE_GATT_CLIENT_COUNT] = {};
uint8_t RP2GattClient::instance_count = 0;
btstack_packet_callback_registration_t RP2GattClient::hci_event_registration = {};
btstack_packet_callback_registration_t RP2GattClient::sm_event_registration = {};
RP2GattClient *RP2GattClient::connect_owner = nullptr;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static ESPBTUUID uuid_from_btstack(uint16_t uuid16, const uint8_t uuid128[16]) {
if (uuid16 != 0) {
return ESPBTUUID::from_uint16(uuid16);
}
// BTstack structs carry the 128-bit form big-endian (printable order).
return ESPBTUUID::from_raw_reversed(uuid128);
}
void RP2GattClient::setup() {
// Pre-create every pool entry so the packet handlers' allocate() calls are
// always a free-list pop -- the IRQ path must never reach malloc().
if (!this->event_pool_.warm() || !this->notify_pool_.warm()) {
ESP_LOGE(TAG, "GATT event pool warm-up failed");
this->mark_failed();
return;
}
// Register this engine for IRQ-context event routing.
if (instance_count >= ESPHOME_BLE_GATT_CLIENT_COUNT) {
// Cannot happen with codegen-sized storage; refuse loudly if it ever does.
ESP_LOGE(TAG, "GATT client registry full");
this->mark_failed();
return;
}
{
// One locked section: the slot store lands before the count bump, and a
// live HCI handler (N > 1 builds) cannot read a half-written registry.
BluetoothLock lock;
this->engine_index_ = instance_count;
instances[instance_count] = this;
instance_count++;
// One HCI event handler for all engine instances (BTstack supports
// multiple registrations, so rp2040_ble's own handler is unaffected).
if (hci_event_registration.callback == nullptr) {
hci_event_registration.callback = &RP2GattClient::hci_packet_handler;
hci_add_event_handler(&hci_event_registration);
sm_event_registration.callback = &RP2GattClient::sm_packet_handler;
sm_add_event_handler(&sm_event_registration);
}
}
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
this->disable_loop();
}
#ifdef USE_OTA_STATE_LISTENER
void RP2GattClient::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
// esp32 parity (its tracker disconnects every client at OTA start): free
// the shared radio for the transfer. No restore needed; the client
// reconnects, and on success the device reboots anyway.
if (state == ota::OTA_STARTED && this->state_ != EngineState::IDLE) {
this->gatt_disconnect();
}
}
#endif
float RP2GattClient::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void RP2GattClient::dump_config() { ESP_LOGCONFIG(TAG, "RP2 GATT client (BTstack)"); }
// ---- IRQ-context handlers: copy-and-enqueue only ----
RP2GattClient *RP2GattClient::instance_for_con_handle(hci_con_handle_t con_handle) {
for (uint8_t i = 0; i < instance_count; i++) {
if (instances[i]->con_handle_ == con_handle) {
return instances[i];
}
}
return nullptr;
}
void RP2GattClient::hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
if (type != HCI_EVENT_PACKET) {
return;
}
uint8_t event_type = hci_event_packet_get_type(packet);
switch (event_type) {
case HCI_EVENT_META_GAP: {
if (hci_event_gap_meta_get_subevent_code(packet) != GAP_SUBEVENT_LE_CONNECTION_COMPLETE) {
break;
}
uint8_t status = gap_subevent_le_connection_complete_get_status(packet);
hci_con_handle_t con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
bd_addr_t peer;
gap_subevent_le_connection_complete_get_peer_address(packet, peer);
// Route by ownership, not address: gap_connect refuses a new
// create-connection until the previous completion is processed, so the
// event belongs to the owner by construction. Cancel completions carry
// a zeroed peer address on this controller, so an address match would
// drop them and pin the owner until its backstop.
RP2GattClient *inst = connect_owner;
static constexpr bd_addr_t ZERO_ADDR = {};
if (inst != nullptr && memcmp(peer, ZERO_ADDR, sizeof(bd_addr_t)) != 0 &&
memcmp(inst->peer_addr_, peer, sizeof(bd_addr_t)) != 0) {
// Addressed completion for a peer the owner is not connecting to: a
// success delayed past a cancel and an ownership handoff (the cancel
// idles the stack's request immediately) must not stamp the old
// procedure's link onto the new owner. Zero-address (cancel)
// completions need no such guard: BTstack only emits them while its
// request state is idle, and a new owner re-arms that state when it
// claims the token, so a stale cancel completion is swallowed by the
// stack, never re-attributed. A successful stale link still needs
// disposal (same hazard as the unowned branch below).
if (status == 0) {
gap_disconnect(con_handle);
}
break;
}
connect_owner = nullptr;
if (inst == nullptr) {
if (status == 0) {
// Nobody owns this late link (the owner escalated first): tear it
// down here or the hci_connection_t leaks and the peer answers
// DISALLOWED until reboot.
gap_disconnect(con_handle);
}
break;
}
if (status == 0) {
// Stamp the handle here in the BTstack context: a disconnection
// racing the queued CONNECTED event arrives in this same context
// and must route by handle (it carries no address).
inst->con_handle_ = con_handle;
}
inst->enqueue_event_irq_(RP2GattEvent::CONNECTED, status, con_handle);
break;
}
case HCI_EVENT_DISCONNECTION_COMPLETE: {
// Routable even against a still-queued CONNECTED event: the handle is
// stamped in this context at connection-complete time.
RP2GattClient *inst = instance_for_con_handle(hci_event_disconnection_complete_get_connection_handle(packet));
if (inst != nullptr) {
inst->enqueue_event_irq_(RP2GattEvent::DISCONNECTED, hci_event_disconnection_complete_get_reason(packet), 0);
}
break;
}
default:
break;
}
}
void RP2GattClient::sm_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
if (type != HCI_EVENT_PACKET) {
return;
}
switch (hci_event_packet_get_type(packet)) {
case SM_EVENT_JUST_WORKS_REQUEST:
// Confirming from the SM callback is the intended BTstack pattern.
// Unscoped on purpose: no peripheral role exists in-tree, and scoping
// would drop a request racing the queued CONNECTED event.
sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
break;
case SM_EVENT_PAIRING_COMPLETE: {
RP2GattClient *inst = instance_for_con_handle(sm_event_pairing_complete_get_handle(packet));
if (inst != nullptr) {
inst->enqueue_event_irq_(RP2GattEvent::PAIRING_RESULT, sm_event_pairing_complete_get_status(packet), 0);
}
break;
}
case SM_EVENT_REENCRYPTION_COMPLETE: {
// A bonded peer re-encrypts instead of pairing; BTstack emits only this
// event on that path, so it answers the PAIR request too.
RP2GattClient *inst = instance_for_con_handle(sm_event_reencryption_complete_get_handle(packet));
if (inst != nullptr) {
inst->enqueue_event_irq_(RP2GattEvent::PAIRING_RESULT, sm_event_reencryption_complete_get_status(packet), 0);
}
break;
}
default:
break;
}
}
void RP2GattClient::gatt_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
if (type != HCI_EVENT_PACKET) {
return;
}
uint8_t event_type = hci_event_packet_get_type(packet);
// Every GATT event carries the connection handle in the same position via
// its accessor; route on it.
hci_con_handle_t con_handle;
switch (event_type) {
case GATT_EVENT_MTU:
con_handle = gatt_event_mtu_get_handle(packet);
break;
case GATT_EVENT_SERVICE_QUERY_RESULT:
con_handle = gatt_event_service_query_result_get_handle(packet);
break;
case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
con_handle = gatt_event_characteristic_query_result_get_handle(packet);
break;
case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT:
con_handle = gatt_event_all_characteristic_descriptors_query_result_get_handle(packet);
break;
case GATT_EVENT_LONG_CHARACTERISTIC_VALUE_QUERY_RESULT:
con_handle = gatt_event_long_characteristic_value_query_result_get_handle(packet);
break;
case GATT_EVENT_LONG_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT:
con_handle = gatt_event_long_characteristic_descriptor_query_result_get_handle(packet);
break;
case GATT_EVENT_NOTIFICATION:
con_handle = gatt_event_notification_get_handle(packet);
break;
case GATT_EVENT_INDICATION:
con_handle = gatt_event_indication_get_handle(packet);
break;
case GATT_EVENT_QUERY_COMPLETE:
con_handle = gatt_event_query_complete_get_handle(packet);
break;
default:
return;
}
RP2GattClient *inst = instance_for_con_handle(con_handle);
if (inst != nullptr) {
inst->handle_gatt_event_irq_(event_type, packet);
}
}
void RP2GattClient::handle_gatt_event_irq_(uint8_t event_type, const uint8_t *packet) {
switch (event_type) {
case GATT_EVENT_MTU:
this->enqueue_event_irq_(RP2GattEvent::MTU_EXCHANGED, 0, gatt_event_mtu_get_MTU(packet));
break;
case GATT_EVENT_QUERY_COMPLETE:
this->enqueue_event_irq_(RP2GattEvent::QUERY_COMPLETE, gatt_event_query_complete_get_att_status(packet), 0);
break;
case GATT_EVENT_SERVICE_QUERY_RESULT: {
if (this->arena_ == nullptr) {
break;
}
if (this->service_count_ >= RP2_GATT_MAX_SERVICES) {
this->truncated_ = true;
break;
}
gatt_client_service_t service;
gatt_event_service_query_result_get_service(packet, &service);
auto &dst = this->arena_->services[this->service_count_];
dst.uuid = uuid_from_btstack(service.uuid16, service.uuid128);
dst.start_handle = service.start_group_handle;
dst.end_handle = service.end_group_handle;
dst.first_characteristic = 0;
dst.characteristic_count = 0;
this->service_count_++;
break;
}
case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT: {
if (this->arena_ == nullptr) {
break;
}
if (this->char_count_ >= RP2_GATT_MAX_CHARACTERISTICS) {
this->truncated_ = true;
break;
}
gatt_client_characteristic_t characteristic;
gatt_event_characteristic_query_result_get_characteristic(packet, &characteristic);
auto &dst = this->arena_->characteristics[this->char_count_];
dst.uuid = uuid_from_btstack(characteristic.uuid16, characteristic.uuid128);
dst.value_handle = characteristic.value_handle;
dst.end_handle = characteristic.end_handle;
dst.properties = static_cast<uint8_t>(characteristic.properties);
dst.first_descriptor = 0;
dst.descriptor_count = 0;
this->char_count_++;
break;
}
case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT: {
if (this->arena_ == nullptr) {
break;
}
if (this->desc_count_ >= RP2_GATT_MAX_DESCRIPTORS) {
this->truncated_ = true;
break;
}
gatt_client_characteristic_descriptor_t descriptor;
gatt_event_all_characteristic_descriptors_query_result_get_characteristic_descriptor(packet, &descriptor);
auto &dst = this->arena_->descriptors[this->desc_count_];
dst.uuid = uuid_from_btstack(descriptor.uuid16, descriptor.uuid128);
dst.handle = descriptor.handle;
this->desc_count_++;
break;
}
case GATT_EVENT_LONG_CHARACTERISTIC_VALUE_QUERY_RESULT:
// One blob per event at the reported offset; assemble into the op buffer.
this->assemble_blob_irq_(gatt_event_long_characteristic_value_query_result_get_value_offset(packet),
gatt_event_long_characteristic_value_query_result_get_value(packet),
gatt_event_long_characteristic_value_query_result_get_value_length(packet));
break;
case GATT_EVENT_LONG_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT:
this->assemble_blob_irq_(gatt_event_long_characteristic_descriptor_query_result_get_descriptor_offset(packet),
gatt_event_long_characteristic_descriptor_query_result_get_descriptor(packet),
gatt_event_long_characteristic_descriptor_query_result_get_descriptor_length(packet));
break;
case GATT_EVENT_NOTIFICATION:
this->enqueue_notify_irq_(gatt_event_notification_get_value_handle(packet),
gatt_event_notification_get_value(packet),
gatt_event_notification_get_value_length(packet));
break;
case GATT_EVENT_INDICATION:
// BTstack auto-confirms indications; deliver like a notification.
this->enqueue_notify_irq_(gatt_event_indication_get_value_handle(packet), gatt_event_indication_get_value(packet),
gatt_event_indication_get_value_length(packet));
break;
default:
break;
}
}
// NOLINTBEGIN(clang-analyzer-unix.Malloc)
void RP2GattClient::assemble_blob_irq_(uint16_t offset, const uint8_t *data, uint16_t len) {
if (offset >= RP2_GATT_MAX_ATTR_LEN) {
return;
}
if (len > RP2_GATT_MAX_ATTR_LEN - offset) {
len = RP2_GATT_MAX_ATTR_LEN - offset;
}
memcpy(this->op_buffer_ + offset, data, len);
if (offset + len > this->op_len_) {
this->op_len_ = offset + len;
}
}
void RP2GattClient::enqueue_event_irq_(RP2GattEvent::Type type, uint8_t status, uint16_t value) {
RP2GattEvent *event = this->event_pool_.allocate();
if (event == nullptr) {
this->event_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
event->type = type;
event->status = status;
event->value = value;
this->event_queue_.push(event);
this->enable_loop_soon_any_context();
}
void RP2GattClient::enqueue_notify_irq_(uint16_t handle, const uint8_t *data, uint16_t len) {
RP2GattNotifyEvent *event = this->notify_pool_.allocate();
if (event == nullptr) {
this->notify_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
event->handle = handle;
event->len = len > RP2_GATT_MAX_ATTR_LEN ? RP2_GATT_MAX_ATTR_LEN : len;
memcpy(event->data, data, event->len);
this->notify_queue_.push(event);
this->enable_loop_soon_any_context();
}
// NOLINTEND(clang-analyzer-unix.Malloc)
// ---- Main-loop state machine ----
void RP2GattClient::loop() {
RP2GattEvent *event;
while ((event = this->event_queue_.pop()) != nullptr) {
RP2GattEvent copy = *event;
this->event_pool_.release(event);
this->handle_event_(copy);
}
RP2GattNotifyEvent *notify;
while ((notify = this->notify_queue_.pop()) != nullptr) {
if (this->notify_subscribed_(notify->handle)) {
this->listener_->on_notify_data(notify->handle, notify->data, notify->len);
}
this->notify_pool_.release(notify);
}
uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
// Control events must not be lost; the connection state is no longer
// trustworthy — recover with a forced teardown.
ESP_LOGE(TAG, "[%u] Dropped %u GATT control events, disconnecting", this->engine_index_, dropped);
this->gatt_disconnect();
}
uint16_t notify_dropped = this->notify_queue_.get_and_reset_dropped_count();
if (notify_dropped > 0) {
ESP_LOGW(TAG, "[%u] Dropped %u GATT notifications (queue full)", this->engine_index_, notify_dropped);
}
if (this->state_ == EngineState::CONNECT_PENDING) {
uint32_t now = millis();
if (now - this->connect_started_ > CONNECT_TIMEOUT_MS) {
// Never reached the radio; nothing stack-side to cancel.
ESP_LOGW(TAG, "[%u] Connect timeout (queued)", this->engine_index_);
this->fail_connection_(HCI_REASON_CONNECTION_TIMEOUT);
} else if (now - this->connect_retry_ms_ >= CONNECT_RETRY_INTERVAL_MS) {
this->connect_retry_ms_ = now;
if (int err = this->try_gap_connect_(); err != 0) {
this->fail_connection_(static_cast<uint8_t>(err));
}
}
} else if (this->state_ == EngineState::CONNECTING || this->state_ == EngineState::MTU_EXCHANGE) {
uint32_t now = millis();
bool cancel_in_flight = this->state_ == EngineState::CONNECTING && this->con_handle_ == HCI_CON_HANDLE_INVALID &&
this->connect_cancel_attempted_;
uint32_t budget = cancel_in_flight ? CONNECT_CANCEL_TIMEOUT_MS : CONNECT_TIMEOUT_MS;
if (now - this->connect_started_ > budget) {
ESP_LOGW(TAG, "[%u] Connect timeout", this->engine_index_);
bool link_up = this->state_ != EngineState::CONNECTING;
bool cancel_sent = false;
if (!link_up) {
BluetoothLock lock;
// Handle check under the lock: a success completion can stamp it in
// the BTstack context right up to this point, and escalating past a
// live link would orphan it (the queued CONNECTED event is dropped
// by the state guard once fail_connection_ runs).
link_up = this->con_handle_ != HCI_CON_HANDLE_INVALID;
if (!link_up && connect_owner == this) {
// gap_connect_cancel is stack-global; only the engine whose
// create-connection is in flight may issue it. First timeout:
// cancel and give the completion a grace period. Second: the
// completion was lost, re-issue the cancel in case the procedure
// still runs (a no-op on an idle stack), then escalate.
gap_connect_cancel();
cancel_sent = !this->connect_cancel_attempted_;
}
this->connect_cancel_attempted_ = true;
}
if (link_up) {
// The link is up (stamped mid-timeout or MTU exchange stalled): tear
// it down properly so the controller frees its side; the
// DISCONNECTING safety net below reclaims state if the disconnection
// event is lost. Dropping engine state without gap_disconnect would
// leak the live link and this engine's GATT slot for the rest of the
// boot.
this->gatt_disconnect();
} else if (cancel_sent) {
// The cancel produces a connection-complete event with a failure
// status, which drives the normal failure path; restart the timer so
// a lost event escalates on the short cancel budget.
this->connect_started_ = now;
} else {
this->fail_connection_(HCI_REASON_CONNECTION_TIMEOUT);
}
}
} else if (this->state_ == EngineState::DISCONNECTING) {
if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
ESP_LOGW(TAG, "[%u] Disconnect timeout, forcing idle", this->engine_index_);
this->handle_disconnected_(HCI_REASON_CONNECTION_TIMEOUT);
}
} else if (this->state_ == EngineState::READY && this->op_type_ == OpType::WRITE_CHAR_NO_RSP &&
millis() - this->write_no_rsp_started_ > WRITE_NO_RSP_TIMEOUT_MS) {
// The can-send window never opened; report instead of hanging the op slot.
bool timed_out = false;
{
BluetoothLock lock;
// The trampoline may have just sent it; its queued result wins.
if (this->event_queue_.empty()) {
this->op_type_ = OpType::NONE;
timed_out = true;
}
}
if (timed_out) {
ESP_LOGW(TAG, "[%u] Deferred write timeout, handle=0x%04x", this->engine_index_, this->op_handle_);
this->listener_->on_write_result(this->op_handle_, GATT_CLIENT_BUSY);
}
} else if (this->state_ == EngineState::IDLE || (this->state_ == EngineState::READY && !this->op_in_flight_() &&
this->event_queue_.empty() && this->notify_queue_.empty())) {
// Nothing pending: the enqueue path re-arms the loop from any context.
this->disable_loop();
}
}
void RP2GattClient::handle_event_(const RP2GattEvent &event) {
switch (event.type) {
case RP2GattEvent::CONNECTED:
this->handle_connected_(event.status, event.value);
break;
case RP2GattEvent::DISCONNECTED:
this->handle_disconnected_(event.status);
break;
case RP2GattEvent::MTU_EXCHANGED:
if (this->state_ == EngineState::MTU_EXCHANGE) {
this->mtu_ = event.value;
ESP_LOGD(TAG, "[%u] MTU %u", this->engine_index_, this->mtu_);
this->state_ = EngineState::READY;
// Scanning resumes and runs alongside the established connection.
this->release_scan_inhibit_();
this->listener_->on_connection_state(true, this->mtu_, 0);
}
break;
case RP2GattEvent::QUERY_COMPLETE:
this->handle_query_complete_(event.status);
break;
case RP2GattEvent::WRITE_NO_RSP_DONE:
this->finish_write_no_rsp_(event.status);
break;
case RP2GattEvent::PAIRING_RESULT:
this->listener_->on_pairing_result(event.status);
break;
}
}
void RP2GattClient::can_write_no_rsp_trampoline(void *context) {
// BTstack context: this callback IS the can-send window, so the deferred
// write happens here; only the result is enqueued for the main loop.
auto *self = static_cast<RP2GattClient *>(context);
if (self->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
return;
}
uint8_t status = gatt_client_write_value_of_characteristic_without_response(self->con_handle_, self->op_handle_,
self->op_len_, self->op_buffer_);
if ((status == GATT_CLIENT_BUSY || status == BTSTACK_ACL_BUFFERS_FULL) &&
gatt_client_request_to_write_without_response(&self->can_write_registration_, self->con_handle_) == 0) {
return; // next window retries; a failed re-arm falls through as an error
}
self->enqueue_event_irq_(RP2GattEvent::WRITE_NO_RSP_DONE, status, 0);
}
void RP2GattClient::finish_write_no_rsp_(uint8_t status) {
if (this->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
return;
}
this->op_type_ = OpType::NONE;
this->listener_->on_write_result(this->op_handle_, status);
}
void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
if (this->state_ != EngineState::CONNECTING) {
return;
}
if (status != 0) {
ESP_LOGW(TAG, "[%u] Connect failed, status=0x%02x", this->engine_index_, status);
this->fail_connection_(status);
return;
}
if (this->cancel_requested_) {
// A disconnect request raced the connection complete and lost; finish
// the teardown instead of reporting a connection nobody wants.
this->con_handle_ = con_handle;
this->state_ = EngineState::DISCONNECTING;
this->disconnecting_started_ = millis();
// No more initiating: give the radio back to the scanner during teardown.
this->release_scan_inhibit_();
uint8_t disc_status;
{
BluetoothLock lock;
disc_status = gap_disconnect(this->con_handle_);
}
if (disc_status != 0) {
this->handle_disconnected_(HCI_REASON_CONNECTION_TIMEOUT);
}
return;
}
this->con_handle_ = con_handle;
this->state_ = EngineState::MTU_EXCHANGE;
ESP_LOGD(TAG, "[%u] Link up, handle=0x%04x, negotiating MTU", this->engine_index_, con_handle);
BluetoothLock lock;
// One wildcard listener covers notifications/indications for every
// characteristic on this connection; the CCCD writes come from the API
// client as plain descriptor writes.
gatt_client_listen_for_characteristic_value_updates(&this->notification_registration_,
&RP2GattClient::gatt_packet_handler, this->con_handle_, nullptr);
// Auto MTU negotiation is disabled (see rp2040_ble enable hooks), so the
// exchange is kicked explicitly; GATT_EVENT_MTU completes it. Without the
// explicit kick the MTU would only be exchanged on the first GATT query,
// which never happens on a V3_WITH_CACHE connection.
// Both registration calls above return void (BTstack 075a078, arduino-pico
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
// the connect timeout in loop().
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
}
void RP2GattClient::release_scan_inhibit_() {
if (this->holds_scan_inhibit_) {
this->holds_scan_inhibit_ = false;
this->parent_->release_scan_inhibit();
}
}
void RP2GattClient::fail_connection_(uint8_t reason) {
{
// Timeout escalation can fire with the completion event lost; release the
// stack-wide connect slot so pending engines can proceed. Until the old
// completion is processed, gap_connect answers any peer with DISALLOWED
// (the request-level guard in hci.c); a cancel idles that request
// immediately, and a late addressed completion from the old procedure is
// then dropped by the owner-peer cross-check in the handler.
BluetoothLock lock;
if (connect_owner == this) {
connect_owner = nullptr;
}
if (this->state_ == EngineState::CONNECTING && this->con_handle_ != HCI_CON_HANDLE_INVALID) {
// A success completion stamped the handle between the escalation
// decision and this lock: tear the link down before cleanup wipes the
// handle, or it leaks its pool block for the rest of the boot.
gap_disconnect(this->con_handle_);
}
}
this->cleanup_link_state_();
this->release_scan_inhibit_();
this->state_ = EngineState::IDLE;
this->listener_->on_connection_state(false, 0, reason);
}
void RP2GattClient::cleanup_link_state_() {
// Drop notifications queued behind the disconnect so they cannot emit
// against a freed slot (address 0) on the next loop.
RP2GattNotifyEvent *stale;
while ((stale = this->notify_queue_.pop()) != nullptr) {
this->notify_pool_.release(stale);
}
// con_handle_ may be stamped in the BTstack context before the main loop
// registers the listener, so a valid handle does not imply a registration;
// stop_listening on an unregistered entry is a benign no-op. One lock
// scope around check and reset so an IRQ stamp cannot land in between
// (unreachable today — ownership is released before cleanup — but the
// invariant lives three functions away).
{
BluetoothLock lock;
if (this->con_handle_ != HCI_CON_HANDLE_INVALID) {
gatt_client_stop_listening_for_characteristic_value_updates(&this->notification_registration_);
}
this->con_handle_ = HCI_CON_HANDLE_INVALID;
}
this->notify_subscription_count_ = 0;
this->cancel_requested_ = false;
this->op_type_ = OpType::NONE;
this->discovery_phase_ = DiscoveryPhase::NONE;
this->release_services();
}
void RP2GattClient::handle_disconnected_(uint8_t reason) {
if (this->state_ == EngineState::IDLE) {
return;
}
ESP_LOGD(TAG, "[%u] Disconnected, reason=0x%02x", this->engine_index_, reason);
this->fail_connection_(reason);
}
void RP2GattClient::handle_query_complete_(uint8_t att_status) {
// Stale completions cannot cross connections: the loop drains the whole
// event queue every iteration, teardown resets op/discovery state, and a
// new discovery is only issued after the new link's MTU event — which in
// this BTstack emits no QUERY_COMPLETE (the MTU state machine is separate
// from the query state machine). Completions with nothing in flight are
// dropped below.
if (this->op_type_ != OpType::NONE && this->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
OpType op = this->op_type_;
this->op_type_ = OpType::NONE;
switch (op) {
case OpType::READ_CHAR:
case OpType::READ_DESC:
// A value that is an exact multiple of MTU - 1 ends with a trailing
// blob request some peers refuse with INVALID_OFFSET; the read is
// complete, not failed.
if ((att_status == ATT_ERROR_INVALID_OFFSET || att_status == ATT_ERROR_ATTRIBUTE_NOT_LONG) &&
this->op_len_ > 0) {
att_status = 0;
}
this->listener_->on_read_result(this->op_handle_, this->op_buffer_, att_status == 0 ? this->op_len_ : 0,
att_status);
break;
case OpType::WRITE_CHAR:
case OpType::WRITE_DESC:
this->listener_->on_write_result(this->op_handle_, att_status);
break;
default:
break;
}
return;
}
if (this->discovery_phase_ != DiscoveryPhase::NONE) {
this->advance_discovery_(att_status);
}
}
// ---- Service discovery ----
int RP2GattClient::discover_services() {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
if (this->op_in_flight_()) {
return GATT_CLIENT_IN_WRONG_STATE;
}
if (this->arena_ == nullptr) {
// Transient: freed in release_services() right after the table streams
// to the API client (mirrors Bluedroid's own per-connection GATT DB
// lifetime on esp32). Checked: a fragmented heap must surface as a
// stack error the proxy can report, not a device reset.
RAMAllocator<ServiceArena> allocator(RAMAllocator<ServiceArena>::ALLOC_INTERNAL);
this->arena_ = allocator.allocate(1);
if (this->arena_ == nullptr) {
ESP_LOGE(TAG, "[%u] Service table allocation failed", this->engine_index_);
return ble_device_base::GATT_ERR_NO_MEMORY;
}
new (this->arena_) ServiceArena();
}
this->service_count_ = 0;
this->char_count_ = 0;
this->desc_count_ = 0;
this->truncated_ = false;
this->discovery_phase_ = DiscoveryPhase::SERVICES;
BluetoothLock lock;
uint8_t status = gatt_client_discover_primary_services(&RP2GattClient::gatt_packet_handler, this->con_handle_);
if (status != 0) {
this->discovery_phase_ = DiscoveryPhase::NONE;
this->release_services();
return status;
}
return 0;
}
int RP2GattClient::issue_characteristic_query_(uint16_t service_index) {
auto &service = this->arena_->services[service_index];
gatt_client_service_t btstack_service = {};
btstack_service.start_group_handle = service.start_handle;
btstack_service.end_group_handle = service.end_handle;
service.first_characteristic = this->char_count_;
BluetoothLock lock;
return gatt_client_discover_characteristics_for_service(&RP2GattClient::gatt_packet_handler, this->con_handle_,
&btstack_service);
}
int RP2GattClient::issue_descriptor_query_(uint16_t char_index) {
auto &chr = this->arena_->characteristics[char_index];
gatt_client_characteristic_t btstack_characteristic = {};
btstack_characteristic.value_handle = chr.value_handle;
btstack_characteristic.end_handle = chr.end_handle;
chr.first_descriptor = this->desc_count_;
BluetoothLock lock;
return gatt_client_discover_characteristic_descriptors(&RP2GattClient::gatt_packet_handler, this->con_handle_,
&btstack_characteristic);
}
void RP2GattClient::advance_discovery_(uint8_t att_status) {
if (this->arena_ == nullptr) {
// release_services() is publicly callable; a table freed mid-discovery
// must end the discovery instead of dereferencing a null arena.
this->finish_discovery_(GATT_ERR_NOT_CONNECTED);
return;
}
if (att_status != 0) {
this->finish_discovery_(att_status);
return;
}
switch (this->discovery_phase_) {
case DiscoveryPhase::SERVICES:
if (this->service_count_ == 0) {
this->finish_discovery_(0);
return;
}
this->discovery_phase_ = DiscoveryPhase::CHARACTERISTICS;
this->disc_service_cursor_ = 0;
if (int err = this->issue_characteristic_query_(0); err != 0) {
this->finish_discovery_(err);
}
break;
case DiscoveryPhase::CHARACTERISTICS: {
auto &service = this->arena_->services[this->disc_service_cursor_];
service.characteristic_count = this->char_count_ - service.first_characteristic;
this->disc_service_cursor_++;
if (this->disc_service_cursor_ < this->service_count_) {
if (int err = this->issue_characteristic_query_(this->disc_service_cursor_); err != 0) {
this->finish_discovery_(err);
}
return;
}
if (this->char_count_ == 0) {
this->finish_discovery_(0);
return;
}
this->discovery_phase_ = DiscoveryPhase::DESCRIPTORS;
this->disc_char_cursor_ = 0;
if (int err = this->issue_descriptor_query_(0); err != 0) {
this->finish_discovery_(err);
}
break;
}
case DiscoveryPhase::DESCRIPTORS: {
auto &chr = this->arena_->characteristics[this->disc_char_cursor_];
chr.descriptor_count = this->desc_count_ - chr.first_descriptor;
this->disc_char_cursor_++;
if (this->disc_char_cursor_ < this->char_count_) {
if (int err = this->issue_descriptor_query_(this->disc_char_cursor_); err != 0) {
this->finish_discovery_(err);
}
return;
}
this->finish_discovery_(0);
break;
}
default:
break;
}
}
void RP2GattClient::finish_discovery_(int error) {
this->discovery_phase_ = DiscoveryPhase::NONE;
ESP_LOGD(TAG, "[%u] Discovery done (err=%d): %u services, %u characteristics, %u descriptors", this->engine_index_,
error, this->service_count_, this->char_count_, this->desc_count_);
if (error == 0 && this->truncated_) {
// A partial table must not stream: V3 clients cache the database
// permanently, so an incomplete one would be wrong forever.
error = ATT_ERROR_INSUFFICIENT_RESOURCES;
}
if (error == 0) {
// Discovery no longer needs the fast interval; settle into the shared
// steady-state parameters (same lifecycle place as esp32). Status
// discarded: BTstack fails this only for an already-gone handle.
BluetoothLock lock;
gap_update_connection_parameters(this->con_handle_, MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0,
MEDIUM_CONN_TIMEOUT);
}
if (this->truncated_) {
ESP_LOGE(TAG, "Service table truncated (device exceeds %u services / %u characteristics / %u descriptors)",
RP2_GATT_MAX_SERVICES, RP2_GATT_MAX_CHARACTERISTICS, RP2_GATT_MAX_DESCRIPTORS);
}
if (error != 0) {
this->release_services();
}
this->listener_->on_service_discovery_done(error);
}
ble_device_base::GattServiceTable RP2GattClient::get_service_table() {
ble_device_base::GattServiceTable table;
if (this->arena_ != nullptr) {
table.services = this->arena_->services;
table.characteristics = this->arena_->characteristics;
table.descriptors = this->arena_->descriptors;
table.service_count = this->service_count_;
table.characteristic_count = this->char_count_;
table.descriptor_count = this->desc_count_;
}
return table;
}
void RP2GattClient::release_services() {
if (this->arena_ != nullptr) {
// Under BluetoothLock so a discovery result landing in the BTstack
// context cannot write into the arena mid-free.
BluetoothLock lock;
RAMAllocator<ServiceArena> allocator(RAMAllocator<ServiceArena>::ALLOC_INTERNAL);
this->arena_->~ServiceArena();
allocator.deallocate(this->arena_, 1);
this->arena_ = nullptr;
}
this->service_count_ = 0;
this->char_count_ = 0;
this->desc_count_ = 0;
this->truncated_ = false;
}
// ---- Connection control ----
int RP2GattClient::connect(uint64_t address, uint8_t addr_type) {
if (this->is_failed()) {
// setup() failed: nothing is registered for event routing and loop()
// never runs, so a connect could not complete or time out.
return GATT_ERR_NOT_CONNECTED;
}
if (this->state_ != EngineState::IDLE) {
return GATT_CLIENT_IN_WRONG_STATE;
}
if (!this->parent_->is_active()) {
return GATT_ERR_NOT_CONNECTED;
}
ble_device_base::uint64_to_mac_msb_first(address, this->peer_addr_);
// BLE_ADDR_TYPE_* code space: bit 0 distinguishes public from random
// (resolved RPA types 2/3 connect with the underlying kind).
this->peer_addr_type_ = (addr_type & 1) != 0 ? BD_ADDR_TYPE_LE_RANDOM : BD_ADDR_TYPE_LE_PUBLIC;
// Stop the shared radio's scan for the duration of the connect attempt
// (esp32 parity: initiating and scanning contend for the radio).
this->holds_scan_inhibit_ = true;
this->parent_->inhibit_scan();
this->connect_cancel_attempted_ = false;
this->cancel_requested_ = false;
// Bounds the queued wait; restarted when gap_connect is accepted so the
// radio attempt gets its full budget (HA's own ~20 s timeout arbitrates the
// sum via a disconnect request).
this->connect_started_ = millis();
if (int err = this->try_gap_connect_(); err != 0) {
this->release_scan_inhibit_();
return err;
}
this->enable_loop();
return 0;
}
// One outgoing LE create-connection exists stack-wide: issue it if no other
// engine owns it, otherwise park in CONNECT_PENDING for loop() to retry.
// Returns nonzero only for hard failures (state untouched; caller cleans up).
int RP2GattClient::try_gap_connect_() {
// Unlocked peek: single core, aligned pointer; a stale value costs one loop
// pass and the locked re-check below is authoritative. Keeps the per-loop
// pending retry from taking BluetoothLock just to find the radio busy.
if (connect_owner != nullptr) {
this->state_ = EngineState::CONNECT_PENDING;
return 0;
}
uint8_t status;
{
BluetoothLock lock;
if (connect_owner != nullptr) {
status = ERROR_CODE_COMMAND_DISALLOWED;
} else {
// esp32 parity: cached connections come up at MEDIUM already (nothing
// consumes the fast interval without a discovery phase), so there is no
// post-connect update procedure to race or silently lose; sustained
// FAST intervals also starve WiFi on the shared CYW43 radio.
// Without-cache runs FAST for discovery and steps down in
// finish_discovery_.
bool cached = this->connection_type_ == ble_device_base::ConnectionType::V3_WITH_CACHE;
gap_set_connection_parameters(CONN_SCAN_INTERVAL, CONN_SCAN_WINDOW,
cached ? MEDIUM_MIN_CONN_INTERVAL : FAST_MIN_CONN_INTERVAL,
cached ? MEDIUM_MAX_CONN_INTERVAL : FAST_MAX_CONN_INTERVAL, 0,
cached ? MEDIUM_CONN_TIMEOUT : FAST_CONN_TIMEOUT, CONN_CE_MIN, CONN_CE_MAX);
status = gap_connect(this->peer_addr_, this->peer_addr_type_);
if (status == 0) {
connect_owner = this;
// Still under the lock: a synthesized failure completion can fire in
// the BTstack context the instant it releases, and completion routing
// requires CONNECTING — set after the fact, the event is discarded
// and the engine burns its whole budget waiting for it.
this->state_ = EngineState::CONNECTING;
this->connect_started_ = millis();
}
}
}
if (status == 0) {
return 0;
}
if (status == ERROR_CODE_COMMAND_DISALLOWED) {
// Radio busy with another engine's connect; resolved from loop().
this->state_ = EngineState::CONNECT_PENDING;
return 0;
}
ESP_LOGW(TAG, "[%u] gap_connect failed, status=0x%02x", this->engine_index_, status);
return status;
}
int RP2GattClient::gatt_disconnect() {
switch (this->state_) {
case EngineState::IDLE:
return GATT_ERR_NOT_CONNECTED;
case EngineState::DISCONNECTING:
return 0; // already on its way down
case EngineState::CONNECT_PENDING:
// Nothing issued stack-side; the invalid handle takes the refused
// path below without touching the stack.
break;
case EngineState::CONNECTING: {
if (this->con_handle_ == HCI_CON_HANDLE_INVALID) {
// The cancel can lose the race against a successful connection
// complete; handle_connected_ checks this flag and finishes the
// teardown instead of proceeding. It also counts as the one cancel
// attempt, so a lost completion escalates on the next timeout tick.
this->cancel_requested_ = true;
this->connect_cancel_attempted_ = true;
// Grace period for the cancel completion: the client's disconnect
// often lands right at the engine's own deadline, and without the
// restart the loop timeout fires first and reports before the
// completion can finish the teardown cleanly.
this->connect_started_ = millis();
BluetoothLock lock;
// Owner: the cancel completes as a failed connection-complete. Not
// the owner (completion already resolved in the BTstack context): the
// queued event drives the same teardown, nothing to cancel.
if (connect_owner == this) {
gap_connect_cancel();
}
return 0;
}
break;
}
default:
break;
}
uint8_t status = ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER;
if (this->con_handle_ != HCI_CON_HANDLE_INVALID) {
{
BluetoothLock lock;
status = gap_disconnect(this->con_handle_);
}
if (status != 0) {
ESP_LOGW(TAG, "[%u] gap_disconnect failed, status=0x%02x", this->engine_index_, status);
}
}
if (status != 0) {
// Refused (handle already gone) or never issued (CONNECT_PENDING):
// complete via the event queue so the listener cannot re-enter
// disconnect mid-call. BluetoothLock stops the IRQ producer, so this
// main-loop push is SPSC-safe.
BluetoothLock lock;
this->enqueue_event_irq_(RP2GattEvent::DISCONNECTED, HCI_REASON_CONNECTION_TIMEOUT, 0);
}
this->state_ = EngineState::DISCONNECTING;
this->disconnecting_started_ = millis();
// No more initiating: give the radio back to the scanner during teardown.
this->release_scan_inhibit_();
this->enable_loop();
return 0;
}
// ---- GATT operations (single outstanding op) ----
int RP2GattClient::read_characteristic(uint16_t handle) {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
if (this->op_in_flight_()) {
return GATT_CLIENT_IN_WRONG_STATE;
}
this->op_type_ = OpType::READ_CHAR;
this->op_handle_ = handle;
this->op_len_ = 0;
BluetoothLock lock;
// Long variant: plain read first, blob continuations only past MTU - 1.
uint8_t status = gatt_client_read_long_value_of_characteristic_using_value_handle(&RP2GattClient::gatt_packet_handler,
this->con_handle_, handle);
if (status != 0) {
this->op_type_ = OpType::NONE;
return status;
}
return 0;
}
int RP2GattClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
if (len > RP2_GATT_MAX_ATTR_LEN) {
return ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LENGTH;
}
if (!response) {
// Synchronous in BTstack: the data is copied into the L2CAP buffer before
// the call returns, and no completion event exists — synthesize one so
// the wire behavior matches esp32 (which reports write-no-response too).
uint8_t status;
{
BluetoothLock lock;
if (this->op_type_ == OpType::WRITE_CHAR_NO_RSP) {
// A deferred write is parked; sending now would overtake it.
return GATT_CLIENT_BUSY;
}
status = gatt_client_write_value_of_characteristic_without_response(this->con_handle_, handle, len,
const_cast<uint8_t *>(data));
// BTSTACK_ACL_BUFFERS_FULL is the same transient flow control one layer
// down (L2CAP), so it defers identically.
if (status == GATT_CLIENT_BUSY || status == BTSTACK_ACL_BUFFERS_FULL) {
if (this->op_in_flight_()) {
// The op buffer is owned; bounce the busy to the caller as before.
return status;
}
// Stash the payload and send from the can-send callback.
memcpy(this->op_buffer_, data, len);
this->op_type_ = OpType::WRITE_CHAR_NO_RSP;
this->op_handle_ = handle;
this->op_len_ = len;
this->write_no_rsp_started_ = millis();
this->can_write_registration_.callback = &RP2GattClient::can_write_no_rsp_trampoline;
this->can_write_registration_.context = this;
uint8_t req = gatt_client_request_to_write_without_response(&this->can_write_registration_, this->con_handle_);
if (req != 0 && req != ERROR_CODE_COMMAND_DISALLOWED) {
this->op_type_ = OpType::NONE;
return req;
}
// COMMAND_DISALLOWED = still armed from a timed-out deferral; that
// registration sends the newly parked payload. Keep the loop running
// so the deadline below can fire on a stalled link.
this->enable_loop();
return 0;
}
}
if (status == 0) {
this->listener_->on_write_result(handle, 0);
}
return status;
}
if (this->op_in_flight_()) {
return GATT_CLIENT_IN_WRONG_STATE;
}
// BTstack keeps the caller's pointer until the request is sent; the payload
// must live in engine-owned storage across the async operation.
memcpy(this->op_buffer_, data, len);
this->op_type_ = OpType::WRITE_CHAR;
this->op_handle_ = handle;
BluetoothLock lock;
uint8_t status;
if (len <= this->mtu_ - 3) {
status = gatt_client_write_value_of_characteristic(&RP2GattClient::gatt_packet_handler, this->con_handle_, handle,
len, this->op_buffer_);
} else {
status = gatt_client_write_long_value_of_characteristic(&RP2GattClient::gatt_packet_handler, this->con_handle_,
handle, len, this->op_buffer_);
}
if (status != 0) {
this->op_type_ = OpType::NONE;
return status;
}
return 0;
}
int RP2GattClient::read_descriptor(uint16_t handle) {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
if (this->op_in_flight_()) {
return GATT_CLIENT_IN_WRONG_STATE;
}
this->op_type_ = OpType::READ_DESC;
this->op_handle_ = handle;
this->op_len_ = 0;
BluetoothLock lock;
uint8_t status = gatt_client_read_long_characteristic_descriptor_using_descriptor_handle(
&RP2GattClient::gatt_packet_handler, this->con_handle_, handle);
if (status != 0) {
this->op_type_ = OpType::NONE;
return status;
}
return 0;
}
int RP2GattClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
if (this->op_in_flight_()) {
return GATT_CLIENT_IN_WRONG_STATE;
}
if (len > RP2_GATT_MAX_ATTR_LEN) {
return ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LENGTH;
}
memcpy(this->op_buffer_, data, len);
this->op_type_ = OpType::WRITE_DESC;
this->op_handle_ = handle;
BluetoothLock lock;
uint8_t status = gatt_client_write_characteristic_descriptor_using_descriptor_handle(
&RP2GattClient::gatt_packet_handler, this->con_handle_, handle, len, this->op_buffer_);
if (status != 0) {
this->op_type_ = OpType::NONE;
return status;
}
return 0;
}
int RP2GattClient::pair() {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
BluetoothLock lock;
sm_request_pairing(this->con_handle_); // void API; completion via SM events
return 0;
}
int RP2GattClient::notify_characteristic(uint16_t handle, bool enable) {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
// The CCCD write arrives separately as a descriptor write (V3 semantics);
// this call only gates local delivery via the subscription list.
if (enable) {
if (!this->notify_subscribed_(handle)) {
if (this->notify_subscription_count_ >= RP2_GATT_MAX_NOTIFY_SUBSCRIPTIONS) {
return GATT_ERR_NO_MEMORY;
}
this->notify_subscriptions_[this->notify_subscription_count_++] = handle;
}
} else {
for (uint8_t i = 0; i < this->notify_subscription_count_; i++) {
if (this->notify_subscriptions_[i] == handle) {
this->notify_subscriptions_[i] = this->notify_subscriptions_[--this->notify_subscription_count_];
break;
}
}
}
this->listener_->on_notify_state(handle, enable, 0);
return 0;
}
bool RP2GattClient::notify_subscribed_(uint16_t handle) const {
for (uint8_t i = 0; i < this->notify_subscription_count_; i++) {
if (this->notify_subscriptions_[i] == handle) {
return true;
}
}
return false;
}
int RP2GattClient::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout) {
if (this->state_ != EngineState::READY) {
return GATT_ERR_NOT_CONNECTED;
}
BluetoothLock lock;
return gap_update_connection_parameters(this->con_handle_, min_interval, max_interval, latency, timeout);
}
conn_err_t unpair_device(uint64_t address) {
uint8_t mac[MAC_ADDRESS_SIZE];
ble_device_base::uint64_to_mac_msb_first(address, mac);
bool found = false;
BluetoothLock lock;
// Exhaustive: the db keys on (type, address), so stale entries can share
// the same address bytes under different types.
for (int i = 0; i < le_device_db_max_count(); i++) {
int addr_type = 0;
bd_addr_t addr;
le_device_db_info(i, &addr_type, addr, nullptr);
if (addr_type != BD_ADDR_TYPE_UNKNOWN && memcmp(addr, mac, sizeof(bd_addr_t)) == 0) {
le_device_db_remove(i);
found = true;
}
}
if (found) {
return CONN_OK;
}
// No bond for this address; the shared error domain has no closer code
// (esp32 parity: its remove-bond call also errors for an unknown address).
return GATT_NOT_CONNECTED;
}
} // namespace esphome::bluetooth_connection
#endif // USE_RP2040_BLE && USE_BLE_GATT_CLIENT