ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
bluetooth_connection_hub.cpp
Go to the documentation of this file.
1// The proxy's per-slot connection wrapper, shared by every platform.
2//
3// SERVICE STREAMING HAZARD - read before touching the streaming code here or
4// in the platform streamers (bluetooth_connection_bluedroid.cpp).
5//
6// A V3 client caches the service list it receives as the device's complete,
7// permanent database. Nothing on the wire marks a list as partial, so a
8// stream that is truncated, has a skipped batch, or is terminated early
9// would be cached whole and poison every later session with the device.
10//
11// The rule: it is always better to send nothing and let the client time out
12// than to let services-done follow an incomplete stream. Concretely:
13// - a refused batch rewinds the cursor and is retried, never skipped;
14// - services-done is sent only after every batch was accepted;
15// - every interruption (subscriber lost or swapped, backend abort,
16// bounds-check failure) parks or aborts WITHOUT services-done and drops
17// any owed done;
18// - a new GetServices supersedes an owed done, so a stale done can never
19// land on a fresh request's empty accumulator and cache it as empty.
20// The client only caches a list terminated by services-done within the same
21// request; timeouts, disconnects and errors raise instead of caching.
23
24#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
25
28#include "esphome/core/hal.h"
30#include "esphome/core/log.h"
31
33
34static const char *const TAG = "bluetooth_connection";
35
37 // Keep the proxy's pre-allocated connections-free message in step
38 this->proxy_->update_address_slot_(this->address_, address);
39 // Slot changing hands: anything owed belonged to the old address. The
40 // choke point for every reassignment, not just reset_connection_()'s path.
41 this->clear_owed_flags_();
42 this->address_ = address;
43 if (address == 0) {
44 this->address_str_[0] = '\0';
45 return;
46 }
47 uint8_t mac[MAC_ADDRESS_SIZE];
50}
51
52void BluetoothConnection::initiate_connection(uint8_t address_type) {
53 // No connect timeout here: the API client's own timeout or the api-gone
54 // sweep drives disconnect().
55 this->state_ = ClientState::CONNECTING;
56 int err = this->backend_->connect(this->address_, address_type);
57 if (err != 0) {
58 ESP_LOGW(TAG, "[%d] [%s] connect failed, err=%d", this->connection_index_, this->address_str_, err);
59 this->reset_connection_(err);
60 }
61}
62
64 // Idempotent: the proxy's teardown loop calls this every 100 ms while the
65 // API subscriber is gone, and a repeat call reaching the backend would
66 // re-arm its teardown timer so the safety timeout never fires.
67 if (this->state_ == ClientState::IDLE || this->state_ == ClientState::DISCONNECTING) {
68 return;
69 }
70 int err = this->backend_->gatt_disconnect();
71 if (err != 0) {
72 // Nonzero means nothing to tear down (both backends): free the slot.
73 // Accepted teardowns always reach a terminal report.
74 ESP_LOGW(TAG, "[%d] [%s] disconnect while backend idle, err=%d", this->connection_index_, this->address_str_, err);
75 this->reset_connection_(err);
76 return;
77 }
78 this->state_ = ClientState::DISCONNECTING;
79}
80
82 if (this->address_ == 0) {
83 // A drop before completion already answered: reset_connection_slot_ sends
84 // the connection response, which the client's pair watcher raises on.
85 return;
86 }
87 this->paired_ = status == 0;
88 this->proxy_->send_device_pairing(this->address_, status == 0, status);
89}
90
92 if (this->pending_error_ != 0) {
93 reason = this->pending_error_;
94 this->pending_error_ = 0;
95 }
96 this->state_ = ClientState::IDLE;
97 this->services_discovered_ = false;
98 this->paired_ = false;
99 // Link gone: the slot may hold a different device before the drain runs.
100 this->clear_owed_flags_();
101 this->backend_->release_services();
102 this->proxy_->reset_connection_slot_(this, reason);
103}
104
105// ---- backend event listener ----
106
107void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int error) {
108 if (connected && this->address_ == 0) {
109 // Late completion for a slot that was already freed: nothing to report,
110 // and the api-gone sweep or a new reservation owns the slot now.
111 // Return ignored: nonzero just means the backend was already idle, and
112 // re-arming a freed slot could clobber a new reservation.
113 this->backend_->gatt_disconnect();
114 return;
115 }
116 if (connected && this->state_ == ClientState::DISCONNECTING) {
117 // The link came up after a disconnect request won the race; finish the
118 // teardown instead of reporting a connection the client no longer wants.
119 int err = this->backend_->gatt_disconnect();
120 if (err != 0) {
121 // Nothing left to tear down after all.
122 this->reset_connection_(err);
123 }
124 return;
125 }
126 if (connected) {
127 this->mtu_ = mtu;
128 if (this->connection_type_ == ConnectionType::V3_WITH_CACHE) {
129 // The API client has the services cached; never discover them. No
130 // discovery phase needs the fast interval, so settle straight into the
131 // shared steady-state parameters. Both backends already open cached
132 // connections with these values (esp32 prefer-params, rp2 initiating
133 // params), so this request is normally redundant - kept as a backstop
134 // in case the initial parameters were negotiated away.
135 this->state_ = ClientState::ESTABLISHED;
136 // The one D-level line for a cached connect; the uncached path narrates
137 // through "Discovery finished" instead.
138 ESP_LOGD(TAG, "[%d] [%s] Connected with cached services, sending connected (mtu=%u)", this->connection_index_,
139 this->address_str_, mtu);
140 int param_err = this->backend_->update_connection_params(ble_device_base::MEDIUM_MIN_CONN_INTERVAL,
141 ble_device_base::MEDIUM_MAX_CONN_INTERVAL, 0,
142 ble_device_base::MEDIUM_CONN_TIMEOUT);
143 if (param_err != 0) {
144 // Survivable: the link just stays on the fast interval.
145 ESP_LOGW(TAG, "[%d] [%s] conn param update failed, err=%d", this->connection_index_, this->address_str_,
146 param_err);
147 }
148 this->send_connected_reply_();
150 return;
151 }
152 // V3_WITHOUT_CACHE: discover services first — the connected response is
153 // sent when discovery completes (MTU + services before the response).
154 this->state_ = ClientState::CONNECTED;
155 int err = this->backend_->discover_services();
156 if (err != 0) {
157 ESP_LOGW(TAG, "[%d] [%s] discover_services failed, err=%d", this->connection_index_, this->address_str_, err);
158 // Latch the real cause for the disconnect report.
159 this->latch_pending_error_(err);
160 this->disconnect();
161 }
162 return;
163 }
164 // Disconnected, connect failed, or teardown complete
165 if (this->address_ == 0) {
166 return; // Slot already freed
167 }
168 ESP_LOGD(TAG, "[%d] [%s] Disconnected, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
169 error);
170 this->reset_connection_(error);
171}
172
174 if (error != 0) {
175 ESP_LOGW(TAG, "[%d] [%s] Service discovery failed, err=%d", this->connection_index_, this->address_str_, error);
176 // Carry the GATT error into the disconnection report so the client sees
177 // the real cause instead of a generic HCI reason.
178 this->latch_pending_error_(error);
179 this->disconnect();
180 return;
181 }
182 ESP_LOGD(TAG, "[%d] [%s] Discovery finished, sending connected (mtu=%u)", this->connection_index_, this->address_str_,
183 this->mtu_);
184 this->state_ = ClientState::ESTABLISHED;
185 this->services_discovered_ = true;
186 this->send_connected_reply_();
188}
189
191 // Connected first: the client should never see services-done or an ack for
192 // a link it has not been told is up. Structural, not size-dependent: a
193 // still-owed connected reply defers the smaller sends to the next tick.
194 if (this->connected_reply_owed_) {
195 this->send_connected_reply_();
196 if (this->connected_reply_owed_) {
197 // The retry limits are wall-clock windows: age the deferred budgets so
198 // a reply cannot outlive the window it was sized for.
199 if (this->send_service_ == SERVICES_DONE_PENDING) {
200 this->age_services_done_();
201 }
202 if (this->has_pending_ack_()) {
203 this->age_pending_ack_();
204 }
205 return;
206 }
207 }
208 if (this->send_service_ == SERVICES_DONE_PENDING) {
209 this->send_services_done_();
210 }
211 if (this->has_pending_ack_()) {
212 this->flush_pending_ack_();
213 }
214}
215
217 if (this->proxy_->send_device_connection(this->address_, true, this->mtu_)) {
218 this->connected_reply_owed_ = false;
219 return;
220 }
221 // Warn on the leading edge only, as elsewhere: the drop must be visible but
222 // must not add traffic to the connection that just refused a frame.
223 if (!this->connected_reply_owed_) {
224 ESP_LOGW(TAG, "[%d] [%s] Connected reply deferred, TCP buffer full", this->connection_index_, this->address_str_);
225 this->connected_reply_owed_ = true;
226 }
227}
228
229void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, int status) {
230 ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str_,
231 operation, handle, status);
232}
233
235 if (this->batch_stalled_)
236 return;
237 this->batch_stalled_ = true;
238 ESP_LOGW(TAG, "[%d] [%s] Service batch deferred, TCP buffer full; retrying", this->connection_index_,
239 this->address_str_);
240}
241
243template<typename Response>
244static bool send_handle_reply(api::APIConnection *api_connection, uint64_t address, uint16_t handle) {
245 Response resp;
246 resp.address = address;
247 resp.handle = handle;
248 return api_connection->send_message(resp);
249}
250
253 if (kind == PendingAck::PENDING_ACK_ERROR) {
254 // Proxy owns the error reply and reports a refusal the same way.
255 return this->proxy_->send_gatt_error(this->address_, handle, error);
256 }
257 auto *api_connection = this->proxy_->get_api_connection();
258 if (api_connection == nullptr)
259 return true; // Nobody subscribed: nothing is owed
260 switch (kind) {
262 return send_handle_reply<api::BluetoothGATTWriteResponse>(api_connection, this->address_, handle);
264 return send_handle_reply<api::BluetoothGATTNotifyResponse>(api_connection, this->address_, handle);
266 case PendingAck::PENDING_ACK_ERROR: // returned above
267 return true;
268 }
269 // No default label above, so a new enumerator is a -Wswitch warning rather
270 // than a silent notify reply. This return only satisfies -Wreturn-type.
271 return true;
272}
273
275 if (this->try_send_ack_(kind, handle, error))
276 return;
277 // Report a newly owed reply and a displaced one; displacing is the case
278 // that loses a reply. Re-refusing the same one stays quiet.
279 if (!this->has_pending_ack_()) {
280 ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X deferred, TCP buffer full", this->connection_index_,
281 this->address_str_, handle);
282 } else if (this->pending_ack_handle_ != handle || this->pending_ack_ != kind) {
283 ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X dropped for handle 0x%04X", this->connection_index_,
284 this->address_str_, this->pending_ack_handle_, handle);
285 }
286 this->latch_pending_ack_(kind, handle, error);
287}
288
290 if (!this->has_pending_ack_())
291 return;
292 if (this->try_send_ack_(this->pending_ack_, this->pending_ack_handle_, this->pending_ack_error_)) {
293 this->clear_pending_ack_();
294 return;
295 }
296 this->age_pending_ack_();
297}
298
300 if (++this->pending_ack_retries_ >= PENDING_ACK_RETRY_LIMIT) {
301 // Undeliverable: past here the client has given up and may have re-asked,
302 // and a late reply would answer the new request instead of this one.
303 ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X undeliverable, abandoning", this->connection_index_,
304 this->address_str_, this->pending_ack_handle_);
305 this->clear_pending_ack_();
306 }
307}
308
309void BluetoothConnection::on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {
310 // Late completion for a freed slot; nothing to report.
311 if (this->address_ == 0)
312 return;
313 if (error != 0) {
314 this->log_gatt_operation_error_("reading char/descriptor", handle, error);
315 this->send_gatt_error_(handle, error);
316 return;
317 }
318 auto *api_connection = this->proxy_->get_api_connection();
319 if (api_connection == nullptr)
320 return;
322 resp.address = this->address_;
323 resp.handle = handle;
324 resp.set_data(data, len);
325 if (!api_connection->send_message(resp)) {
326 // Not latched: would mean holding the payload through the congestion
327 // that refused it. The client's read timeout arbitrates.
328 ESP_LOGW(TAG, "[%d] [%s] Failed to send read response", this->connection_index_, this->address_str_);
329 }
330}
331
333 if (this->address_ == 0)
334 return;
335 if (error != 0) {
336 this->log_gatt_operation_error_("writing char/descriptor", handle, error);
337 this->send_gatt_error_(handle, error);
338 return;
339 }
341}
342
343void BluetoothConnection::on_notify_state(uint16_t handle, bool enabled, int error) {
344 if (this->address_ == 0)
345 return;
346 if (error != 0) {
347 this->log_gatt_operation_error_(enabled ? "registering notifications" : "unregistering notifications", handle,
348 error);
349 this->send_gatt_error_(handle, error);
350 return;
351 }
353}
354
355void BluetoothConnection::on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {
356 if (this->address_ == 0)
357 return;
358 ESP_LOGV(TAG, "[%d] [%s] Notify: handle=0x%2X", this->connection_index_, this->address_str_, handle);
359 auto *api_connection = this->proxy_->get_api_connection();
360 if (api_connection == nullptr)
361 return;
363 resp.address = this->address_;
364 resp.handle = handle;
365 resp.set_data(data, len);
366 if (!api_connection->send_message(resp)) {
367 // Not latched, same reason as the read reply. Notify data is lossy: the
368 // peripheral will not resend it.
369 ESP_LOGW(TAG, "[%d] [%s] Failed to send notify data response", this->connection_index_, this->address_str_);
370 }
371}
372
373// ---- GATT operations ----
374
375conn_err_t BluetoothConnection::check_connected_op_(const char *action, const char *type) const {
376 if (this->connected()) {
377 return CONN_OK;
378 }
379 ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str_, action,
380 type);
381 return GATT_NOT_CONNECTED;
382}
383
386 if (conn_err_t err = this->check_connected_op_("read", "characteristic"); err != CONN_OK)
387 return err;
388 ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
389 return this->backend_->read_characteristic(handle);
390}
391
393 bool response) {
395 if (conn_err_t err = this->check_connected_op_("write", "characteristic"); err != CONN_OK)
396 return err;
397 ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
398 return this->backend_->write_characteristic(handle, data, static_cast<uint16_t>(length), response);
399}
400
403 if (conn_err_t err = this->check_connected_op_("read", "descriptor"); err != CONN_OK)
404 return err;
405 ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
406 return this->backend_->read_descriptor(handle);
407}
408
409// The neutral backend contract performs descriptor writes acknowledged, so
410// the response flag is intentionally ignored (esp32 maps it to RSP/NO_RSP).
411conn_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length,
412 bool /*response*/) {
414 if (conn_err_t err = this->check_connected_op_("write", "descriptor"); err != CONN_OK)
415 return err;
416 ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
417 return this->backend_->write_descriptor(handle, data, static_cast<uint16_t>(length));
418}
419
422 if (conn_err_t err = this->check_connected_op_("notify", "characteristic"); err != CONN_OK)
423 return err;
424 ESP_LOGV(TAG, "[%d] [%s] %s GATT characteristic notifications handle %d", this->connection_index_, this->address_str_,
425 enable ? "Registering for" : "Unregistering for", handle);
426 return this->backend_->notify_characteristic(handle, enable);
427}
428
429conn_err_t BluetoothConnection::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
430 uint16_t timeout) {
431 if (conn_err_t err = this->check_connected_op_("update params of", "connection"); err != CONN_OK)
432 return err;
433 return this->backend_->update_connection_params(min_interval, max_interval, latency, timeout);
434}
435
436// ---- Service streaming ----
437
439 if (this->proxy_->send_gatt_services_done(this->address_)) {
440 // Sent, or subscriber gone (park silently; its timeout arbitrates).
441 this->send_service_ = DONE_SENDING_SERVICES;
442 return;
443 }
444 if (this->send_service_ != SERVICES_DONE_PENDING) {
445 // Warn on the transition only; retries stay silent.
446 ESP_LOGW(TAG, "[%d] [%s] Failed to send services done, retrying", this->connection_index_, this->address_str_);
447 this->services_done_retries_ = 0;
448 this->send_service_ = SERVICES_DONE_PENDING;
449 } else {
450 this->age_services_done_();
451 }
452}
453
455 if (++this->services_done_retries_ >= SERVICES_DONE_RETRY_LIMIT) {
456 // Undeliverable (see SERVICES_DONE_RETRY_LIMIT); silence arbitrates.
457 ESP_LOGW(TAG, "[%d] [%s] Services done undeliverable, abandoning", this->connection_index_, this->address_str_);
458 this->send_service_ = DONE_SENDING_SERVICES;
459 }
460}
461
463 auto table = this->backend_->get_service_table();
464 if (this->send_service_ >= table.service_count) {
465 this->backend_->release_services();
466 this->send_services_done_();
467 return;
468 }
469
470 // The subscriber vanished mid-stream; the api-gone sweep tears the
471 // connection down anyway.
472 auto *api_conn = this->proxy_->get_api_connection();
473 if (api_conn == nullptr) {
474 ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", this->connection_index_,
475 this->address_str_);
476 this->park_service_stream_();
477 return;
478 }
479
480 // Check if client supports efficient UUIDs
481 bool use_efficient_uuids = this->proxy_->client_supports_efficient_uuids();
482
483 // Prepare response
485 resp.address = this->address_;
486
487 // Dynamic batching based on actual size, same contract as the esp32 streamer
488 size_t current_size = resp.calculate_size();
489 int16_t batch_start = this->send_service_;
490
491 while (this->send_service_ < table.service_count) {
492 const auto &service = table.services[this->send_service_];
493
494 // If this service likely won't fit, send current batch (unless it's the first)
495 size_t estimated_size = estimate_service_size(service.characteristic_count, use_efficient_uuids);
496 if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
497 break;
498 }
499
500 resp.services.emplace_back();
501 auto &service_resp = resp.services.back();
502 fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service.uuid, use_efficient_uuids);
503 service_resp.handle = service.start_handle;
504
505 // Bounds-check the backend's index ranges against the table totals rather
506 // than trusting its discovery bookkeeping blindly. A miscounted non-empty
507 // range must not stream a truncated database as authoritative (V3 clients
508 // cache it permanently): abort and tear the connection down; the client
509 // times out and retries. Empty ranges are tolerated regardless of index.
510 uint16_t char_count = service.characteristic_count;
511 if (char_count != 0 && service.first_characteristic + char_count > table.characteristic_count) {
512 ESP_LOGE(TAG, "[%d] [%s] Characteristic range out of bounds (service %d), aborting stream",
513 this->connection_index_, this->address_str_, this->send_service_);
514 this->abort_service_stream(ble_device_base::GATT_ERR_UNLIKELY);
515 return;
516 }
517 if (char_count > 0) {
518 service_resp.characteristics.init(char_count);
519 for (uint16_t ci = 0; ci < char_count; ci++) {
520 const auto &chr = table.characteristics[service.first_characteristic + ci];
521 service_resp.characteristics.emplace_back();
522 auto &characteristic_resp = service_resp.characteristics.back();
523 fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, chr.uuid, use_efficient_uuids);
524 characteristic_resp.handle = chr.value_handle;
525 characteristic_resp.properties = chr.properties;
526 uint16_t desc_count = chr.descriptor_count;
527 if (desc_count != 0 && chr.first_descriptor + desc_count > table.descriptor_count) {
528 ESP_LOGE(TAG, "[%d] [%s] Descriptor range out of bounds (service %d), aborting stream",
529 this->connection_index_, this->address_str_, this->send_service_);
530 this->abort_service_stream(ble_device_base::GATT_ERR_UNLIKELY);
531 return;
532 }
533 if (desc_count == 0) {
534 continue;
535 }
536 characteristic_resp.descriptors.init(desc_count);
537 for (uint16_t di = 0; di < desc_count; di++) {
538 const auto &desc = table.descriptors[chr.first_descriptor + di];
539 characteristic_resp.descriptors.emplace_back();
540 auto &descriptor_resp = characteristic_resp.descriptors.back();
541 fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc.uuid, use_efficient_uuids);
542 descriptor_resp.handle = desc.handle;
543 }
544 }
545 }
546
547 if (close_service_batch(resp, current_size, this->send_service_, this->connection_index_, this->address_str_) !=
549 break;
550 }
551 }
552
553 // Send the message with dynamically batched services; on a failed send,
554 // rewind the cursor so the batch is retried instead of silently skipped
555 // (bounded: a subscriber that stays gone ends streaming via the api-lost
556 // rewind above).
557 if (!api_conn->send_message(resp)) {
558 this->note_batch_stalled_();
559 this->send_service_ = batch_start;
560 return;
561 }
562 this->batch_stalled_ = false;
563}
564
565} // namespace esphome::bluetooth_connection
566
567#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
uint8_t address
Definition bl0906.h:4
uint8_t status
Definition bl0942.h:8
bool send_message(const T &msg)
Returns false as soon as the TCP buffer is full.
std::vector< BluetoothGATTService > services
Definition api_pb2.h:2108
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:2255
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:2160
bool try_send_ack_(PendingAck kind, uint16_t handle, conn_err_t error)
Sole construction site for these replies, shared by send and retry.
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override
void initiate_connection(uint8_t address_type)
Start connecting with the API address type (BLE_ADDR_TYPE_* code space).
void latch_pending_ack_(PendingAck kind, uint16_t handle, conn_err_t error=0)
Latch a refused reply for the proxy drain.
conn_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response)
bool connected_reply_owed_
An owed connected=true reply; the proxy's paced drain re-offers it.
void supersede_pending_ack_(uint16_t handle, PendingAck kind)
Drop an owed reply this re-ask makes stale.
void latch_pending_error_(conn_err_t err)
First cause wins: a later, less specific error must not overwrite it.
void abort_service_stream(conn_err_t err)
Streamer abort: latch the GATT cause, park the cursor, tear down.
void age_pending_ack_()
Advance the retry budget and abandon at the limit, without sending.
void send_gatt_error_(uint16_t handle, conn_err_t error)
Report a rejected request.
conn_err_t check_connected_op_(const char *action, const char *type) const
void age_services_done_()
Advance the retry budget and abandon at the limit, without sending.
void log_gatt_operation_error_(const char *operation, uint16_t handle, int status)
void on_write_result(uint16_t handle, int error) override
bool batch_stalled_
Set while a refused batch is retrying, so only the first one warns.
void flush_pending_ack_()
Re-offer the owed reply; clears on success, stays owed on a refusal.
void on_notify_state(uint16_t handle, bool enabled, int error) override
conn_err_t notify_characteristic(uint16_t handle, bool enable)
void on_connection_state(bool connected, uint16_t mtu, int error) override
void send_services_done_()
Send services-done and settle the cursor: DONE when it lands (or no subscriber), SERVICES_DONE_PENDIN...
void park_service_stream_()
Park the stream without services-done and free any held table: an interrupted stream must never be de...
void flush_owed_replies_()
Re-offer everything this slot owes.
void note_batch_stalled_()
Warn on the stall's leading edge only.
conn_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response)
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override
void send_ack_(PendingAck kind, uint16_t handle, conn_err_t error=0)
First attempt: send, and latch it for the drain if the API refuses.
void send_connected_reply_()
Send the connected=true reply, latching it if the API refuses.
conn_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout)
void clear_owed_flags_()
Drop everything this slot owes, in one write to the shared tail byte.
bool send_gatt_services_done(uint64_t address)
Same convention as send_device_connection: false only on a refused frame.
void reset_connection_slot_(BluetoothConnection *connection, conn_err_t reason)
Free a connection slot after teardown: notify the API client and reset the streaming cursor.
void update_address_slot_(uint64_t old_address, uint64_t new_address)
Keep the pre-allocated connections-free message in step when a connection slot changes address (0 = f...
void send_device_pairing(uint64_t address, bool paired, conn_err_t error=CONN_OK)
bool send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error)
False only when the API refused the frame, so the reply is still owed.
bool send_device_connection(uint64_t address, bool connected, uint16_t mtu=0, conn_err_t error=CONN_OK)
False only when a subscriber refused the frame; true = delivered or nobody subscribed.
bool client_supports_efficient_uuids() const
Whether the subscribed API client understands 16/32-bit UUID fields.
uint16_t type
void uint64_to_mac_msb_first(uint64_t address, uint8_t out[6])
Unpack a uint64 BLE address into printable (MSB-first) byte order — the order bd_addr_t / esp_bd_addr...
Definition ble_device.h:169
size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids)
Estimate the wire size of a service (service overhead + its characteristics, assuming 128-bit UUIDs a...
BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size_t &current_size, int16_t &send_service, uint8_t connection_index, const char *address_str)
Close out the service just packed into resp (account its actual wire size, advance the cursor) and de...
PendingAck
A refused GATT reply owed to the current subscriber.
void fill_gatt_uuid(std::array< uint64_t, 2 > &uuid_128, uint32_t &short_uuid, const ble_device_base::ESPBTUUID &uuid, bool use_efficient_uuids)
Fill the UUID in the appropriate wire format based on client support and UUID type (128-bit array for...
const void size_t len
Definition hal.h:64
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1493
uint16_t length
Definition tt21100.cpp:0
spi_device_handle_t handle