ESPHome 2026.10.2
Loading...
Searching...
No Matches
bluetooth_proxy.cpp
Go to the documentation of this file.
1#include "bluetooth_proxy.h"
2
3#ifdef USE_BLUETOOTH_PROXY
4
7#include "esphome/core/log.h"
10#include <algorithm>
11#include <cinttypes>
12#include <cstring>
13#include <limits>
14
16
17static const char *const TAG = "bluetooth_proxy";
18
19// BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation
20// It sets the batch size for BLE advertisements to maximize WiFi efficiency
21
22// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
23static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
24 "BLE advertisement data array size mismatch");
25
27
28// The neutral enum's values are the wire values.
30static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STARTING) ==
32static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::RUNNING) ==
34static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::FAILED) ==
36static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STOPPING) ==
38static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STOPPED) ==
40
53
54#ifdef USE_BLE_SCANNER_STATE_CALLBACK
56 // False only on a refused frame, so the latch arms only when a retry is owed.
58}
59#else
61 // One read feeds both the frame and the change detector; the detector only
62 // advances if the frame was accepted, so a dropped send (WOULD_BLOCK on a
63 // full TX buffer) is retried from loop() instead of leaving a stale state.
64 const bool running = this->hub_->scan_running();
67 this->last_scan_running_ = running;
68 }
69}
70#endif // USE_BLE_SCANNER_STATE_CALLBACK
71
73#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
74 this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
75 this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
76#endif
77
78 // Capture the configured scan mode from YAML before any API changes
79 this->configured_scan_active_ = this->hub_->scan_active();
80
81 this->hub_->set_raw_advertisement_callback({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
82 static_cast<BluetoothProxy *>(self)->on_raw_advertisement_(adv);
83 }});
84#ifdef USE_BLE_SCANNER_STATE_CALLBACK
85 // Only push hubs compile the slot; elsewhere loop() polls scan_running().
86 this->hub_->set_scanner_state_callback({this, [](void *self, ble_device_base::ScannerState state) {
87 static_cast<BluetoothProxy *>(self)->send_scanner_state_(state);
88 }});
89#endif
90}
91
92// The hub delivers raw advertisements on the ESPHome main loop.
94 if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
95 return;
96
97#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
98 // Ask the filter before the packet is queued, so a dropped advertisement never
99 // reaches the batch or the network.
100 if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) {
101 ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address);
102 return;
103 }
104#endif
105
106 auto &adv = this->response_.advertisements[this->response_.advertisements_len];
107 adv.address = raw.address;
108 adv.rssi = raw.rssi;
109 adv.address_type = raw.addr_type;
110 uint8_t length = raw.data_len > sizeof(adv.data) ? sizeof(adv.data) : static_cast<uint8_t>(raw.data_len);
111 adv.data_len = length;
112 std::memcpy(adv.data, raw.data, length);
113
115
116 ESP_LOGVV(TAG, "Queuing raw packet from %012" PRIX64 ", length %d. RSSI: %d dB", raw.address, length, raw.rssi);
117
118 // Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE
119 if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) {
121 }
122}
123
124#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
126 ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
128}
129
131 ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message);
132}
133#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
134
135#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
136void BluetoothProxy::log_reply_dropped_(const char *what, uint64_t address) {
137 ESP_LOGW(TAG, "%s reply for %012" PRIX64 " dropped, TCP buffer full", what, address);
138}
139
140void BluetoothProxy::log_reply_deferred_(const char *what, uint64_t address) {
141 ESP_LOGW(TAG, "%s reply for %012" PRIX64 " deferred, TCP buffer full", what, address);
142}
143
144void BluetoothProxy::log_reply_displaced_(const char *what, uint64_t owed, uint64_t address) {
145 ESP_LOGW(TAG, "%s reply for %012" PRIX64 " dropped, displaced by %012" PRIX64, what, owed, address);
146}
147
148void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
149 ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type);
150}
151
152void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
153 const char *type) {
154 this->log_not_connected_gatt_(action, type);
155 if (!this->send_gatt_error(address, handle, GATT_NOT_CONNECTED)) {
156 // No connection, so nothing to latch against; the client's timeout arbitrates.
157 this->log_reply_dropped_("Not-connected", address);
158 }
159}
160#endif
161
163 if (sent) {
164 // VV: one line per flush drowns a verbose log in any busy environment.
165 ESP_LOGVV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len);
166 } else {
167 // The rare congestion signal stays at V.
168 ESP_LOGV(TAG, "Batch of %u BLE advertisements dropped, TCP buffer full", this->response_.advertisements_len);
169 }
170}
171
173 // Print configured facts. dump_config runs right after setup, before the
174 // radio is up, so live scan state would always read "stopped" here — the
175 // loop's BluetoothScannerStateResponse carries the changing value instead.
176 char mac_str[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
178 const char *mac_out = mac_str[0] != '\0' ? mac_str : "unavailable (adapter not up yet)";
179 const char *scan_mode = this->configured_scan_active_ ? "active" : "passive";
180#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
181 ESP_LOGCONFIG(TAG,
182 "Bluetooth Proxy:\n"
183 " Active: %s\n"
184 " Connections: %d\n"
185 " Configured scan: %s\n"
186 " Adapter MAC: %s",
187 YESNO(this->active_), this->connection_count_, scan_mode, mac_out);
188#else
189 ESP_LOGCONFIG(TAG,
190 "Bluetooth Proxy:\n"
191 " Mode: advertisement-only (no GATT connections)\n"
192 " Configured scan: %s\n"
193 " Adapter MAC: %s",
194 scan_mode, mac_out);
195#endif
196#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
197 ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set()));
198#endif
199}
200
201#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
202
204 if (this->connection_count_ >= BLUETOOTH_PROXY_MAX_CONNECTIONS) {
205 // Cannot happen with codegen-sized registration; a silent drop would
206 // surface later as a null proxy_ dereference, so refuse loudly.
207 ESP_LOGE(TAG, "Connection registry full, dropping registration");
208 return;
209 }
210 // The hub wrapper has no Component lifecycle, so the index is assigned here.
211 connection->connection_index_ = this->connection_count_;
212 this->connections_[this->connection_count_++] = connection;
213 connection->proxy_ = this;
214}
215
216void BluetoothProxy::log_slot_accounting_mismatch_() { ESP_LOGW(TAG, "Connection slot free-count mismatch, clamped"); }
217
218void BluetoothProxy::replace_allocated_slot_(uint64_t find_value, uint64_t set_value) {
219 for (auto &slot : this->connections_free_response_.allocated) {
220 if (slot == find_value) {
221 slot = set_value;
222 return;
223 }
224 }
225 // The accounting arrays are only mutated here and sized to the slot count,
226 // so a miss means the bookkeeping already drifted — say so.
227 ESP_LOGW(TAG, "Connection slot accounting mismatch (find 0x%llx)", (unsigned long long) find_value);
228}
229
231 // Match before free entry so one address never occupies two pool slots.
232 PendingReply *free_entry = nullptr;
233 for (uint8_t i = 0; i < this->connection_count_; i++) {
234 auto &owed = this->pending_disconnections_[i];
235 if (owed.matches(address)) {
236 owed.set(address, error);
237 return;
238 }
239 if (free_entry == nullptr && owed.empty()) {
240 free_entry = &owed;
241 }
242 }
243 if (free_entry != nullptr) {
244 this->log_reply_deferred_("Disconnect", address);
245 free_entry->set(address, error);
246 return;
247 }
248 // Every entry is owed: evict the first so the newest loss is not silent too.
249 this->log_reply_displaced_("Disconnect", this->pending_disconnections_[0].address(), address);
250 this->pending_disconnections_[0].set(address, error);
251}
252
254 // A reconnect supersedes the owed disconnect; a late resend would shadow
255 // the new connection.
256 for (uint8_t i = 0; i < this->connection_count_; i++) {
257 if (this->pending_disconnections_[i].matches(address)) {
258 this->pending_disconnections_[i].clear();
259 return; // latch_pending_disconnection_ keeps at most one entry per address
260 }
261 }
262}
263
265 if (this->send_device_connection(address, false)) {
266 // A landed answer satisfies any owed notification for the address; a
267 // drained duplicate would follow it otherwise.
268 this->clear_pending_disconnection_(address);
269 return;
270 }
271 // Not latched: the client's own request timeout arbitrates, and pooling
272 // these would let a request retry loop displace an unsolicited disconnect.
273 this->log_reply_dropped_("Disconnect", address);
274}
275
277 if (this->send_device_connection(address, false, 0, error)) {
278 // A later disconnect landing for an address that still has one owed would
279 // otherwise have the drain repeat it.
280 this->clear_pending_disconnection_(address);
281 return;
282 }
283 // A dropped disconnect leaves the client believing the link is live, so
284 // every GATT operation on it times out until something else corrects it.
285 // latch_pending_disconnection_() reports the leading edge.
286 this->latch_pending_disconnection_(address, error);
287}
288
290 // The client has no other way to learn of an unsolicited disconnect.
291 this->send_device_disconnected_(connection->get_address(), reason);
292 connection->set_address(0);
293 connection->send_service_ = INIT_SENDING_SERVICES;
294 this->send_connections_free();
295}
296
298 // Finish the scan before reserving: a free slot earlier in the array must
299 // not win over a later slot that already holds the address, or one device
300 // ends up on two slots with a second connection attempt racing the first.
301 BluetoothConnection *free_slot = nullptr;
302 for (uint8_t i = 0; i < this->connection_count_; i++) {
303 auto *connection = this->connections_[i];
304 uint64_t conn_addr = connection->get_address();
305
306 if (conn_addr == address) {
307 // A connect request supersedes an owed disconnect.
308 if (reserve) {
309 this->clear_pending_disconnection_(address);
310 }
311 return connection;
312 }
313
314 if (free_slot == nullptr && conn_addr == 0)
315 free_slot = connection;
316 }
317 if (!reserve || free_slot == nullptr)
318 return nullptr;
319 this->clear_pending_disconnection_(address);
320 free_slot->send_service_ = INIT_SENDING_SERVICES;
321 free_slot->set_address(address);
322 // All connections must start at INIT
323 // We only set the state if we allocate the connection
324 // to avoid a race where multiple connection attempts
325 // are made.
326 free_slot->set_state(ClientState::INIT);
327 return free_slot;
328}
329
331 switch (msg.request_type) {
334 auto *connection = this->get_connection_(msg.address, true);
335 if (connection == nullptr) {
336 ESP_LOGW(TAG, "No free connections available");
338 return;
339 }
340 if (!msg.has_address_type) {
341 ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
342 connection->address_str());
344 return;
345 }
346 if (connection->state() == ClientState::CONNECTED || connection->state() == ClientState::ESTABLISHED) {
347 this->log_connection_request_ignored_(connection, connection->state());
348 connection->send_connected_reply_();
349 this->send_connections_free();
350 return;
351 } else if (connection->state() == ClientState::DISCONNECTING && connection->cancel_teardown()) {
352 ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
353 connection->get_connection_index(), connection->address_str());
354 return;
355 } else if (connection->state() != ClientState::INIT) {
356 // Covers CONNECTING too: a repeat request during a connect attempt is
357 // ignored the same way.
358 this->log_connection_request_ignored_(connection, connection->state());
359 return;
360 }
362 connection->set_connection_type(ble_device_base::ConnectionType::V3_WITH_CACHE);
363 this->log_connection_info_(connection, "v3 with cache");
364 } else { // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE
365 connection->set_connection_type(ble_device_base::ConnectionType::V3_WITHOUT_CACHE);
366 this->log_connection_info_(connection, "v3 without cache");
367 }
368 connection->initiate_connection(static_cast<uint8_t>(msg.address_type));
369 this->send_connections_free();
370 break;
371 }
373 auto *connection = this->get_connection_(msg.address, false);
374 if (connection == nullptr) {
376 this->send_connections_free();
377 return;
378 }
379 if (connection->state() != ClientState::IDLE) {
380 connection->disconnect();
381 } else {
382 connection->set_address(0);
384 this->send_connections_free();
385 }
386 break;
387 }
389 // The connection wrapper exposes the pairing surface; success is
390 // reported when the platform's pairing completion arrives.
391 auto *connection = this->get_connection_(msg.address, false);
392 if (connection != nullptr) {
393 if (!connection->is_paired()) {
394 auto err = connection->pair();
395 if (err != CONN_OK) {
396 this->send_device_pairing(msg.address, false, err);
397 }
398 } else {
399 this->send_device_pairing(msg.address, true);
400 }
401 } else {
402 // Answer instead of leaving the client to time out.
403 this->send_device_pairing(msg.address, false, GATT_NOT_CONNECTED);
404 }
405 break;
406 }
409 if (ret == CONN_OK) {
410 // The bond is gone; a live connection must not short-circuit the
411 // next PAIR as already paired.
412 auto *connection = this->get_connection_(msg.address, false);
413 if (connection != nullptr) {
414 connection->set_unpaired();
415 }
416 }
417 this->send_device_unpairing(msg.address, ret == CONN_OK, ret);
418 break;
419 }
422 this->send_device_clear_cache(msg.address, ret == CONN_OK, ret);
423 break;
424 }
426 ESP_LOGE(TAG, "V1 connections removed");
428 break;
429 }
430 }
431}
432
434 auto *connection = this->get_connection_(msg.address, false);
435 if (connection == nullptr) {
436 this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic");
437 return;
438 }
439
440 auto err = connection->read_characteristic(msg.handle);
441 if (err != CONN_OK) {
442 connection->send_gatt_error_(msg.handle, err);
443 }
444}
445
447 auto *connection = this->get_connection_(msg.address, false);
448 if (connection == nullptr) {
449 this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic");
450 return;
451 }
452
453 auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response);
454 if (err != CONN_OK) {
455 connection->send_gatt_error_(msg.handle, err);
456 }
457}
458
460 auto *connection = this->get_connection_(msg.address, false);
461 if (connection == nullptr) {
462 this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor");
463 return;
464 }
465
466 auto err = connection->read_descriptor(msg.handle);
467 if (err != CONN_OK) {
468 connection->send_gatt_error_(msg.handle, err);
469 }
470}
471
473 auto *connection = this->get_connection_(msg.address, false);
474 if (connection == nullptr) {
475 this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor");
476 return;
477 }
478
479 auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true);
480 if (err != CONN_OK) {
481 connection->send_gatt_error_(msg.handle, err);
482 }
483}
484
486 auto *connection = this->get_connection_(msg.address, false);
487 if (connection == nullptr || !connection->connected()) {
488 this->handle_gatt_not_connected_(msg.address, 0, "get", "services");
489 return;
490 }
491 if (!connection->has_gatt_services()) {
492 ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->get_connection_index(), connection->address_str());
493 // Through the retrying sender: a drop must not leave discovery hanging.
494 // Re-entry does not depend on the cursor - this branch is gated on
495 // has_gatt_services() alone, so no restore is needed.
496 connection->send_services_done_();
497 return;
498 }
499 if (connection->send_service_ > 0) {
500 // A request mid-stream restarts from the top so the requester always
501 // gets the full list. No duplicate risk: the client accumulates batches
502 // per request, and a same-session re-request only happens after the
503 // previous request timed out and discarded its partial list.
504 ESP_LOGD(TAG, "[%d] [%s] GetServices mid-stream, restarting", connection->get_connection_index(),
505 connection->address_str());
506 connection->send_service_ = 0;
507 return;
508 }
509 if (connection->send_service_ == SERVICES_DONE_PENDING) {
510 // A new request supersedes an owed done: the client accumulates batches
511 // per request, so its fresh, empty accumulator plus a bare done would
512 // cache as an empty database. The table is freed; the client's timeout
513 // arbitrates.
514 ESP_LOGW(TAG, "[%d] [%s] GetServices superseded an undelivered done; client timeout will retry",
515 connection->get_connection_index(), connection->address_str());
516 connection->send_service_ = DONE_SENDING_SERVICES;
517 return;
518 }
519 if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet
520 connection->send_service_ = 0;
521}
522
524 auto *connection = this->get_connection_(msg.address, false);
525 if (connection == nullptr) {
526 this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic");
527 return;
528 }
529
530 auto err = connection->notify_characteristic(msg.handle, msg.enable);
531 if (err != CONN_OK) {
532 connection->send_gatt_error_(msg.handle, err);
533 }
534}
535
537 if (this->api_connection_ == nullptr)
538 return;
539 // Not latched (esp32 parity): the request is idempotent, so a drop resolves
540 // via the client timeout and a retry gives the same answer. Still reported.
541
542 auto *connection = this->get_connection_(msg.address, false);
544 resp.address = msg.address;
545
546 if (connection == nullptr || !connection->connected()) {
547 ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected",
548 connection ? static_cast<int>(connection->get_connection_index()) : -1,
549 connection ? connection->address_str() : "unknown");
550 resp.error = GATT_NOT_CONNECTED;
551 if (!this->api_connection_->send_message(resp)) {
552 this->log_reply_dropped_("Connection-params", msg.address);
553 }
554 return;
555 }
556
557 // Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values;
558 // clamp to uint16_t since the current BLE spec defines these as 16-bit.
559 constexpr uint32_t max_val = std::numeric_limits<uint16_t>::max();
560 resp.error = connection->update_connection_params(static_cast<uint16_t>(std::min(msg.min_interval, max_val)),
561 static_cast<uint16_t>(std::min(msg.max_interval, max_val)),
562 static_cast<uint16_t>(std::min(msg.latency, max_val)),
563 static_cast<uint16_t>(std::min(msg.timeout, max_val)));
564 if (!this->api_connection_->send_message(resp)) {
565 this->log_reply_dropped_("Connection-params", msg.address);
566 }
567}
568
569#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
570
571#ifdef USE_ESP32
572
574 // esp32 only: BLEHub is the concrete tracker here, so these calls reach
575 // tracker-native methods beyond the neutral contract.
576 if (this->hub_->get_scan_active() == active) {
577 return;
578 }
579 ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
580 this->hub_->set_scan_active(active);
581 this->hub_->stop_scan();
582 this->hub_->set_scan_continuous(
583 true); // Set this to true to automatically start scanning again when it has cleaned up.
584}
585
586#else // !USE_ESP32
587
589 if (this->hub_->scan_active() != active) {
590 ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
591 if (!this->hub_->request_scan_mode(active)) {
592 // Passive-only controller asked for active scanning; the state report
593 // below carries the real, unchanged mode so the subscriber does not
594 // assume the change happened.
595 ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive");
596 }
597 }
598#ifndef USE_BLE_SCANNER_STATE_CALLBACK
599 if (this->api_connection_ != nullptr) {
600 // Reports the mode change; the sender also refreshes last_scan_running_, so
601 // a failed restart (scan_running_ dropped by the tracker) is not reported
602 // again by loop() on the next tick. A push hub reports the restart's
603 // transitions (mode rides along) instead.
605 }
606#endif
607}
608
609#endif // USE_ESP32
610
612#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
613 // Stream pending service-discovery batches every iteration; the streamer
614 // handles a vanished API connection itself.
615 for (uint8_t i = 0; i < this->connection_count_; i++) {
616 this->connections_[i]->process_pending_services();
617 }
618#endif
619
620 // Run advertisement flush / scanner-state poll every 100ms
622 if (now - this->last_advertisement_flush_time_ < 100)
623 return;
625
626#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
627 if (this->connections_free_pending_ && this->api_connection_ != nullptr) {
628 // Resend a dropped slot-state update, paced by the 100 ms gate so the
629 // retry does not hammer the congestion it exists to survive. Every build
630 // sends this at subscribe time (api_connection.cpp), so the drain
631 // compiles on every proxy build.
632 this->connections_free_pending_ = false;
634 }
635#endif
636
637 if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
638 // The API subscriber is gone: tear down any connections it left behind.
640 return;
641 }
642
643#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
644 // Paced retries of owed per-slot notifications; subscriber swaps clear
645 // stale latches before this runs.
646 for (uint8_t i = 0; i < this->connection_count_; i++) {
647 this->connections_[i]->flush_owed_replies_();
648 }
649 // Address-keyed, not slot-keyed, so it gets its own loop; bounded by
650 // connection_count_ like the latch and clear helpers. Not pre-cleared:
651 // the sender clears on success and re-latches on refusal, keeping the
652 // latch's leading-edge warn honest (same shape as the unpair drain).
653 for (uint8_t i = 0; i < this->connection_count_; i++) {
654 auto &owed = this->pending_disconnections_[i];
655 if (owed.empty())
656 continue;
657 this->send_device_disconnected_(owed.address(), owed.error());
658 }
659
660 // An owed unpair reply. Not pre-cleared: the sender clears on success and
661 // re-latches on refusal, keeping its leading-edge warn guard honest.
662 if (!this->pending_unpairing_.empty()) {
663 conn_err_t error = this->pending_unpairing_.error();
664 this->send_device_unpairing(this->pending_unpairing_.address(), error == CONN_OK, error);
665 }
666#endif
667
668#ifdef USE_BLE_SCANNER_STATE_CALLBACK
669 // Resend a dropped scanner-state push (see scanner_state_pending_).
670 if (this->scanner_state_pending_) {
671 this->send_scanner_state_(this->hub_->get_scanner_state());
672 }
673#else
674 // This hub doesn't push scanner-state transitions; poll and report on
675 // change. A hub gaining push emits the define and drops this poll.
676 if (this->hub_->scan_running() != this->last_scan_running_) {
678 }
679#endif
680
681#ifdef USE_WIFI
682 // Wi-Fi (or a coexistence build that can fall back to it): every other
683 // non-empty 100 ms tick (~200 ms) gives partial batches time to fill
684 // toward BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE, so the air gets fewer,
685 // fuller frames. Full batches still ship immediately from the queueing
686 // path, and the owed-reply drains above keep the 100 ms cadence.
687 if (this->response_.advertisements_len != 0) {
688 if (this->adv_flush_toggle_) {
690 }
692 } else {
693 // Nothing pending (idle, or a full batch just shipped inline): arm so
694 // the next batch ships on the next tick.
695 this->adv_flush_toggle_ = true;
696 }
697#else
698 // No Wi-Fi in the build (ethernet): no airtime worth trading latency for,
699 // so partial batches flush every tick.
701#endif
702}
703
705#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
706 this->connections_free_pending_ = false;
707#endif
708#ifdef USE_BLE_SCANNER_STATE_CALLBACK
709 // Owed on unsubscribe; on subscribe the trailing send_scanner_state_()
710 // re-drives it from the hub, so clearing it there is free.
711 this->scanner_state_pending_ = false;
712#else
713 // Force a poll-arm mismatch: a frame refused at subscribe time could
714 // otherwise match the stale detector and never be retried. Inert on
715 // unsubscribe: loop() returns at the no-subscriber gate before the
716 // detector runs, and a re-subscribe re-arms this anyway.
717 this->last_scan_running_ = !this->hub_->scan_running();
718#endif
719#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
721 this->pending_disconnections_.fill({});
722 for (uint8_t i = 0; i < this->connection_count_; i++) {
723 // Neither a partial stream's tail nor an owed done belongs to the next
724 // session; silence (the client's timeout) arbitrates.
725 auto *connection = this->connections_[i];
726 connection->park_service_stream_();
727 connection->clear_owed_flags_();
728 }
729#endif
730}
731
733#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
734 for (uint8_t i = 0; i < this->connection_count_; i++) {
735 auto *connection = this->connections_[i];
736 if (connection->get_address() != 0) {
737 connection->disconnect();
738 }
739 }
740#endif
741}
742
744 if (api_connection != this->api_connection_) {
745 if (this->api_connection_ != nullptr) {
746 // A previous subscriber still holds the slot. This is almost always a
747 // stale connection from a client that dropped without a clean disconnect
748 // and has not yet hit the keepalive timeout; rejecting the new
749 // subscriber would silently starve it of advertisements until it
750 // reconnects, so the newest subscriber wins instead.
751 char old_peername[socket::SOCKADDR_STR_LEN];
752 char new_peername[socket::SOCKADDR_STR_LEN];
753 ESP_LOGW(TAG, "Subscription from %s (%s) replaces %s (%s)", api_connection->get_name(),
754 api_connection->get_peername_to(new_peername), this->api_connection_->get_name(),
755 this->api_connection_->get_peername_to(old_peername));
756 }
757 // Stale retry latches belong to the previous subscriber's session; a
758 // re-subscribe by the current one keeps what it is still owed.
759 this->reset_owed_replies_();
760 this->api_connection_ = api_connection;
761 // Held links belong to a client that no longer gets their events; free them so idle-only
762 // advertisers are seen again. After the swap, so a sync slot free reports to the new client.
764 }
765#ifdef USE_BLE_SCANNER_STATE_CALLBACK
766 // get_scanner_state() is part of the push-hub surface (see BLEHubContract).
767 this->send_scanner_state_(this->hub_->get_scanner_state());
768#else
770#endif
771}
772
774 if (this->api_connection_ != api_connection) {
775 ESP_LOGV(TAG, "API connection is not subscribed");
776 return;
777 }
778 this->api_connection_ = nullptr;
779 this->reset_owed_replies_();
780}
781
782#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
784 if (this->api_connection_ != nullptr) {
786 }
787}
788
790 // Latch only for the current subscriber: loop() resends to api_connection_.
791 if (!api_connection->send_message(this->connections_free_response_) && api_connection == this->api_connection_) {
792 // V like the api layer's own buffer-full log: a D would ride the same
793 // full connection.
794 ESP_LOGV(TAG, "Connections-free update deferred, TCP buffer full");
795 this->connections_free_pending_ = true;
796 }
797}
798
799bool BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, conn_err_t error) {
800 if (this->api_connection_ == nullptr)
801 return true; // Nobody subscribed: nothing owed
804 call.connected = connected;
805 call.mtu = mtu;
806 call.error = error;
807 return this->api_connection_->send_message(call);
808}
809
811 if (this->api_connection_ == nullptr)
812 return true; // Nobody subscribed: nothing is owed, only a refused frame reports false
815 return this->api_connection_->send_message(call);
816}
817
819 if (this->api_connection_ == nullptr)
820 return true; // Nobody subscribed: nothing is owed, only a refused frame reports false
823 call.handle = handle;
824 call.error = error;
825 return this->api_connection_->send_message(call);
826}
827
828void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, conn_err_t error) {
829 if (this->api_connection_ == nullptr)
830 return;
833 call.paired = paired;
834 call.error = error;
835
836 if (!this->api_connection_->send_message(call)) {
837 // Not latched: a retried PAIR is answered from is_paired(), so the client
838 // recovers on its own. Still worth saying it happened.
839 this->log_reply_dropped_("Pairing", address);
840 }
841}
842
843void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, conn_err_t error) {
844 if (this->api_connection_ == nullptr)
845 return;
846 // An owed success is the authoritative answer: a later attempt for the
847 // same address fails only because the first already removed the bond.
848 if (!this->pending_unpairing_.empty() && this->pending_unpairing_.matches(address) &&
849 this->pending_unpairing_.error() == CONN_OK) {
850 success = true;
851 error = CONN_OK;
852 }
855 call.success = success;
856 call.error = error;
857
858 if (this->api_connection_->send_message(call)) {
859 // A later unpair landing for an address that still has one owed would
860 // otherwise have the drain repeat it.
861 if (this->pending_unpairing_.matches(address)) {
863 }
864 return;
865 }
866 if (this->pending_unpairing_.empty()) {
867 this->log_reply_deferred_("Unpair", address);
868 } else if (!this->pending_unpairing_.matches(address)) {
869 this->log_reply_displaced_("Unpair", this->pending_unpairing_.address(), address);
870 }
871 this->pending_unpairing_.set(address, error);
872}
873
874// GATT arm only: the advertisement-only arm no longer dispatches CLEAR_CACHE,
875// so its response encoder would be dead weight there.
876void BluetoothProxy::send_device_clear_cache(uint64_t address, bool success, conn_err_t error) {
877 if (this->api_connection_ == nullptr)
878 return;
881 call.success = success;
882 call.error = error;
883
884 if (!this->api_connection_->send_message(call)) {
885 // Not latched: clear-cache is idempotent, so a retry gives the same answer.
886 this->log_reply_dropped_("Clear-cache", address);
887 }
888}
889#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
890
891BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
892
893} // namespace esphome::bluetooth_proxy
894
895#endif // USE_BLUETOOTH_PROXY
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
const char * get_peername_to(std::span< char, socket::SOCKADDR_STR_LEN > buf) const
Get peer name (IP address) into caller-provided buffer, returns buf for convenience.
const char * get_name() const
bool send_message(const T &msg)
Returns false as soon as the TCP buffer is full.
std::array< uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS > allocated
Definition api_pb2.h:2714
enums::BluetoothDeviceRequestType request_type
Definition api_pb2.h:2401
std::array< BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE > advertisements
Definition api_pb2.h:2377
enums::BluetoothScannerMode mode
Definition api_pb2.h:2867
enums::BluetoothScannerState state
Definition api_pb2.h:2866
enums::BluetoothScannerMode configured_mode
Definition api_pb2.h:2868
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg)
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg)
void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type)
void log_reply_deferred_(const char *what, uint64_t address)
A latched reply's leading edge; the drain's re-refusals stay quiet.
void get_bluetooth_mac_address_pretty(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > output)
void on_raw_advertisement_(const ble_device_base::RawAdvertisement &raw)
bool adv_flush_toggle_
Wi-Fi only: flush on every other non-empty tick (~200 ms) so partial batches fill; an idle tick re-ar...
void log_not_connected_gatt_(const char *action, const char *type)
void send_scanner_state_(ble_device_base::ScannerState state)
void answer_device_disconnected_(uint64_t address)
Answer a request with connected=false.
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg)
void send_device_unpairing(uint64_t address, bool success, conn_err_t error)
No default error: the drain rebuilds success as (error == CONN_OK), so a caller that omitted it would...
void replace_allocated_slot_(uint64_t find_value, uint64_t set_value)
bool send_gatt_services_done(uint64_t address)
Same convention as send_device_connection: false only on a refused frame.
void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg)
std::array< PendingReply, BLUETOOTH_PROXY_MAX_CONNECTIONS > pending_disconnections_
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags)
void disconnect_all_connections_()
Tear down every held link; repeat calls are no-ops.
void send_device_clear_cache(uint64_t address, bool success, conn_err_t error=CONN_OK)
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 unsubscribe_api_connection(api::APIConnection *api_connection)
void register_connection(BluetoothConnection *connection)
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.
api::BluetoothLERawAdvertisementsResponse response_
void send_device_disconnected_(uint64_t address, conn_err_t error=CONN_OK)
Send connected=false and pool it for the paced drain if refused.
void log_connection_info_(BluetoothConnection *connection, const char *message)
void reset_owed_replies_()
Drop everything the ending session was 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.
void log_reply_displaced_(const char *what, uint64_t owed, uint64_t address)
A latched reply lost to a newer one for a different address.
BluetoothConnection * get_connection_(uint64_t address, bool reserve)
void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg)
bool send_bluetooth_scanner_state_(ble_device_base::ScannerState state)
void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg)
void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg)
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg)
void log_reply_dropped_(const char *what, uint64_t address)
Report a reply we deliberately do not latch, so no drop is silent.
void flush_pending_advertisements_()
Caller must ensure api_connection_ is non-null and API server is connected.
std::array< BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS > connections_
void latch_pending_disconnection_(uint64_t address, conn_err_t error)
Pool a refused freed-slot notification for the paced drain.
api::BluetoothConnectionsFreeResponse connections_free_response_
void log_connection_request_ignored_(BluetoothConnection *connection, ClientState state)
void clear_pending_disconnection_(uint64_t address)
Drop any owed freed-slot notification for this address (client reconnected).
One owed address-keyed reply in a single word: 48-bit address low, 16-bit error on top.
constexpr uint64_t address() const
constexpr bool matches(uint64_t address) const
constexpr void set(uint64_t address, conn_err_t error)
constexpr conn_err_t error() const
const LogString * message
Definition component.cpp:35
uint16_t type
uint16_t flags
bool state
Definition fan.h:2
int ret
@ BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR
Definition api_pb2.h:238
@ BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE
Definition api_pb2.h:239
@ BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT
Definition api_pb2.h:235
@ BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR
Definition api_pb2.h:237
@ BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE
Definition api_pb2.h:240
@ BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE
Definition api_pb2.h:241
@ BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT
Definition api_pb2.h:236
@ BLUETOOTH_SCANNER_MODE_PASSIVE
Definition api_pb2.h:254
@ BLUETOOTH_SCANNER_MODE_ACTIVE
Definition api_pb2.h:255
@ BLUETOOTH_SCANNER_STATE_STOPPED
Definition api_pb2.h:251
@ BLUETOOTH_SCANNER_STATE_STARTING
Definition api_pb2.h:247
@ BLUETOOTH_SCANNER_STATE_STOPPING
Definition api_pb2.h:250
@ BLUETOOTH_SCANNER_STATE_RUNNING
Definition api_pb2.h:248
@ BLUETOOTH_SCANNER_STATE_FAILED
Definition api_pb2.h:249
APIServer * global_api_server
const char * client_state_to_string(ClientState state)
ScannerState
Scanner lifecycle, wire-value aligned with the api enum so consumers cast directly (pinned by static_...
Definition ble_hub.h:53
conn_err_t unpair_device(uint64_t address)
conn_err_t clear_gatt_cache(uint64_t address)
BluetoothProxy * global_bluetooth_proxy
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
One raw advertisement as delivered by the controller — a borrowed view, valid only for the duration o...
Definition ble_hub.h:24
bool is_set() const
A default-constructed slot is "no filter"; the proxy guards on this.
uint16_t length
Definition tt21100.cpp:0
spi_device_handle_t handle