ESPHome 2026.9.0-dev
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, and so does a
279 // fresh deferral for the handle already warned about: a congested bulk
280 // transfer re-asks the same handle every cycle and each ack would warn.
281 if (!this->has_pending_ack_()) {
282 if (!this->ack_deferred_warned_ || this->pending_ack_handle_ != handle) {
283 ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X deferred, TCP buffer full", this->connection_index_,
284 this->address_str_, handle);
285 this->ack_deferred_warned_ = true;
286 }
287 } else if (this->pending_ack_handle_ != handle || this->pending_ack_ != kind) {
288 ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X dropped for handle 0x%04X", this->connection_index_,
289 this->address_str_, this->pending_ack_handle_, handle);
290 }
291 this->latch_pending_ack_(kind, handle, error);
292}
293
295 if (!this->has_pending_ack_())
296 return;
297 if (this->try_send_ack_(this->pending_ack_, this->pending_ack_handle_, this->pending_ack_error_)) {
298 this->clear_pending_ack_();
299 return;
300 }
301 this->age_pending_ack_();
302}
303
305 if (++this->pending_ack_retries_ >= PENDING_ACK_RETRY_LIMIT) {
306 // Undeliverable: past here the client has given up and may have re-asked,
307 // and a late reply would answer the new request instead of this one.
308 ESP_LOGW(TAG, "[%d] [%s] GATT reply for handle 0x%04X undeliverable, abandoning", this->connection_index_,
309 this->address_str_, this->pending_ack_handle_);
310 this->clear_pending_ack_();
311 }
312}
313
314void BluetoothConnection::on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {
315 // Late completion for a freed slot; nothing to report.
316 if (this->address_ == 0)
317 return;
318 if (error != 0) {
319 this->log_gatt_operation_error_("reading char/descriptor", handle, error);
320 this->send_gatt_error_(handle, error);
321 return;
322 }
323 auto *api_connection = this->proxy_->get_api_connection();
324 if (api_connection == nullptr)
325 return;
327 resp.address = this->address_;
328 resp.handle = handle;
329 resp.set_data(data, len);
330 if (!api_connection->send_message(resp)) {
331 // Not latched: would mean holding the payload through the congestion
332 // that refused it. The client's read timeout arbitrates.
333 ESP_LOGW(TAG, "[%d] [%s] Failed to send read response", this->connection_index_, this->address_str_);
334 }
335}
336
338 if (this->address_ == 0)
339 return;
340 if (error != 0) {
341 this->log_gatt_operation_error_("writing char/descriptor", handle, error);
342 this->send_gatt_error_(handle, error);
343 return;
344 }
346}
347
348void BluetoothConnection::on_notify_state(uint16_t handle, bool enabled, int error) {
349 if (this->address_ == 0)
350 return;
351 if (error != 0) {
352 this->log_gatt_operation_error_(enabled ? "registering notifications" : "unregistering notifications", handle,
353 error);
354 this->send_gatt_error_(handle, error);
355 return;
356 }
358}
359
360void BluetoothConnection::on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {
361 if (this->address_ == 0)
362 return;
363 ESP_LOGV(TAG, "[%d] [%s] Notify: handle=0x%2X", this->connection_index_, this->address_str_, handle);
364 auto *api_connection = this->proxy_->get_api_connection();
365 if (api_connection == nullptr)
366 return;
368 resp.address = this->address_;
369 resp.handle = handle;
370 resp.set_data(data, len);
371 if (!api_connection->send_message(resp)) {
372 // Not latched, same reason as the read reply. Notify data is lossy: the
373 // peripheral will not resend it. Warn on the first drop only; a congested
374 // link drops a whole stream and one line per notify floods the log.
375 if (!this->notify_drop_warned_) {
376 ESP_LOGW(TAG, "[%d] [%s] Failed to send notify data response, handle 0x%04X", this->connection_index_,
377 this->address_str_, handle);
378 this->notify_drop_warned_ = true;
379 } else {
380 ESP_LOGV(TAG, "[%d] [%s] Failed to send notify data response, handle 0x%04X", this->connection_index_,
381 this->address_str_, handle);
382 }
383 }
384}
385
386// ---- GATT operations ----
387
388conn_err_t BluetoothConnection::check_connected_op_(const char *action, const char *type) const {
389 if (this->connected()) {
390 return CONN_OK;
391 }
392 ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str_, action,
393 type);
394 return GATT_NOT_CONNECTED;
395}
396
399 if (conn_err_t err = this->check_connected_op_("read", "characteristic"); err != CONN_OK)
400 return err;
401 ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
402 return this->backend_->read_characteristic(handle);
403}
404
406 bool response) {
408 if (conn_err_t err = this->check_connected_op_("write", "characteristic"); err != CONN_OK)
409 return err;
410 ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
411 return this->backend_->write_characteristic(handle, data, static_cast<uint16_t>(length), response);
412}
413
416 if (conn_err_t err = this->check_connected_op_("read", "descriptor"); err != CONN_OK)
417 return err;
418 ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
419 return this->backend_->read_descriptor(handle);
420}
421
422// The neutral backend contract performs descriptor writes acknowledged, so
423// the response flag is intentionally ignored (esp32 maps it to RSP/NO_RSP).
424conn_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length,
425 bool /*response*/) {
427 if (conn_err_t err = this->check_connected_op_("write", "descriptor"); err != CONN_OK)
428 return err;
429 ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
430 return this->backend_->write_descriptor(handle, data, static_cast<uint16_t>(length));
431}
432
435 if (conn_err_t err = this->check_connected_op_("notify", "characteristic"); err != CONN_OK)
436 return err;
437 ESP_LOGV(TAG, "[%d] [%s] %s GATT characteristic notifications handle %d", this->connection_index_, this->address_str_,
438 enable ? "Registering for" : "Unregistering for", handle);
439 return this->backend_->notify_characteristic(handle, enable);
440}
441
442conn_err_t BluetoothConnection::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
443 uint16_t timeout) {
444 if (conn_err_t err = this->check_connected_op_("update params of", "connection"); err != CONN_OK)
445 return err;
446 return this->backend_->update_connection_params(min_interval, max_interval, latency, timeout);
447}
448
449// ---- Service streaming ----
450
452 if (this->proxy_->send_gatt_services_done(this->address_)) {
453 // Sent, or subscriber gone (park silently; its timeout arbitrates).
454 this->send_service_ = DONE_SENDING_SERVICES;
455 return;
456 }
457 if (this->send_service_ != SERVICES_DONE_PENDING) {
458 // Warn on the transition only; retries stay silent.
459 ESP_LOGW(TAG, "[%d] [%s] Failed to send services done, retrying", this->connection_index_, this->address_str_);
460 this->services_done_retries_ = 0;
461 this->send_service_ = SERVICES_DONE_PENDING;
462 } else {
463 this->age_services_done_();
464 }
465}
466
468 if (++this->services_done_retries_ >= SERVICES_DONE_RETRY_LIMIT) {
469 // Undeliverable (see SERVICES_DONE_RETRY_LIMIT); silence arbitrates.
470 ESP_LOGW(TAG, "[%d] [%s] Services done undeliverable, abandoning", this->connection_index_, this->address_str_);
471 this->send_service_ = DONE_SENDING_SERVICES;
472 }
473}
474
476 auto table = this->backend_->get_service_table();
477 if (this->send_service_ >= table.service_count) {
478 this->backend_->release_services();
479 this->send_services_done_();
480 return;
481 }
482
483 // The subscriber vanished mid-stream; the api-gone sweep tears the
484 // connection down anyway.
485 auto *api_conn = this->proxy_->get_api_connection();
486 if (api_conn == nullptr) {
487 ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", this->connection_index_,
488 this->address_str_);
489 this->park_service_stream_();
490 return;
491 }
492
493 // Check if client supports efficient UUIDs
494 bool use_efficient_uuids = this->proxy_->client_supports_efficient_uuids();
495
496 // Prepare response
498 resp.address = this->address_;
499
500 // Dynamic batching based on actual size, same contract as the esp32 streamer
501 size_t current_size = resp.calculate_size();
502 int16_t batch_start = this->send_service_;
503
504 while (this->send_service_ < table.service_count) {
505 const auto &service = table.services[this->send_service_];
506
507 // If this service likely won't fit, send current batch (unless it's the first)
508 size_t estimated_size = estimate_service_size(service.characteristic_count, use_efficient_uuids);
509 if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
510 break;
511 }
512
513 resp.services.emplace_back();
514 auto &service_resp = resp.services.back();
515 fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service.uuid, use_efficient_uuids);
516 service_resp.handle = service.start_handle;
517
518 // Bounds-check the backend's index ranges against the table totals rather
519 // than trusting its discovery bookkeeping blindly. A miscounted non-empty
520 // range must not stream a truncated database as authoritative (V3 clients
521 // cache it permanently): abort and tear the connection down; the client
522 // times out and retries. Empty ranges are tolerated regardless of index.
523 uint16_t char_count = service.characteristic_count;
524 if (char_count != 0 && service.first_characteristic + char_count > table.characteristic_count) {
525 ESP_LOGE(TAG, "[%d] [%s] Characteristic range out of bounds (service %d), aborting stream",
526 this->connection_index_, this->address_str_, this->send_service_);
527 this->abort_service_stream(ble_device_base::GATT_ERR_UNLIKELY);
528 return;
529 }
530 if (char_count > 0) {
531 service_resp.characteristics.init(char_count);
532 for (uint16_t ci = 0; ci < char_count; ci++) {
533 const auto &chr = table.characteristics[service.first_characteristic + ci];
534 service_resp.characteristics.emplace_back();
535 auto &characteristic_resp = service_resp.characteristics.back();
536 fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, chr.uuid, use_efficient_uuids);
537 characteristic_resp.handle = chr.value_handle;
538 characteristic_resp.properties = chr.properties;
539 uint16_t desc_count = chr.descriptor_count;
540 if (desc_count != 0 && chr.first_descriptor + desc_count > table.descriptor_count) {
541 ESP_LOGE(TAG, "[%d] [%s] Descriptor range out of bounds (service %d), aborting stream",
542 this->connection_index_, this->address_str_, this->send_service_);
543 this->abort_service_stream(ble_device_base::GATT_ERR_UNLIKELY);
544 return;
545 }
546 if (desc_count == 0) {
547 continue;
548 }
549 characteristic_resp.descriptors.init(desc_count);
550 for (uint16_t di = 0; di < desc_count; di++) {
551 const auto &desc = table.descriptors[chr.first_descriptor + di];
552 characteristic_resp.descriptors.emplace_back();
553 auto &descriptor_resp = characteristic_resp.descriptors.back();
554 fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc.uuid, use_efficient_uuids);
555 descriptor_resp.handle = desc.handle;
556 }
557 }
558 }
559
560 if (close_service_batch(resp, current_size, this->send_service_, this->connection_index_, this->address_str_) !=
562 break;
563 }
564 }
565
566 // Send the message with dynamically batched services; on a failed send,
567 // rewind the cursor so the batch is retried instead of silently skipped
568 // (bounded: a subscriber that stays gone ends streaming via the api-lost
569 // rewind above).
570 if (!api_conn->send_message(resp)) {
571 this->note_batch_stalled_();
572 this->send_service_ = batch_start;
573 return;
574 }
575 this->batch_stalled_ = false;
576}
577
578} // namespace esphome::bluetooth_connection
579
580#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:2130
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:2277
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:2182
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)
bool notify_drop_warned_
Set on the first dropped notify; later drops log at verbose only.
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...
bool ack_deferred_warned_
Set once the deferred warn fired; with an unchanged pending_ack_handle_ it keeps re-deferrals of the ...
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:1505
uint16_t length
Definition tt21100.cpp:0
spi_device_handle_t handle