ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
bluetooth_proxy.h
Go to the documentation of this file.
1#pragma once
2
4
5#ifdef USE_BLUETOOTH_PROXY
6
7#include <array>
8
14
16
18
20
22
23// The connection-domain types live in the bluetooth_connection component;
24// re-exported here so the proxy code reads unqualified.
25using bluetooth_connection::CONN_OK;
27using bluetooth_connection::GATT_NOT_CONNECTED;
28using bluetooth_connection::DONE_SENDING_SERVICES;
29using bluetooth_connection::INIT_SENDING_SERVICES;
30using bluetooth_connection::SERVICES_DONE_PENDING;
31
32#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
33using bluetooth_connection::BluetoothConnection;
35#endif
36
37// Legacy versions:
38// Version 1: Initial version without active connections
39// Version 2: Support for active connections
40// Version 3: New connection API
41// Version 4: Pairing support
42// Version 5: Cache clear support
43static constexpr uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5;
44static constexpr uint32_t LEGACY_ACTIVE_NO_CACHE_CLEAR_VERSION = 4;
45static constexpr uint32_t LEGACY_ACTIVE_NO_PAIRING_VERSION = 3;
46static constexpr uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1;
47
58
62
63#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
67 public:
68 constexpr void set(uint64_t address, conn_err_t error) {
69 // Mask: the address originates from the client, and a stray high bit
70 // must not corrupt the reason.
71 this->word_ = (address & ADDRESS_MASK) | (static_cast<uint64_t>(static_cast<uint16_t>(error)) << 48);
72 }
73 constexpr void clear() { this->word_ = 0; }
74 // Whole-word test: only (address 0, error 0) reads back as nothing owed.
75 // A zero-address failure still latches, which is correct - that reply is
76 // owed too. Neither backend can unpair address 0 successfully.
77 constexpr bool empty() const { return this->word_ == 0; }
78 // Masked like set(), so a stray high bit cannot defeat the pool lookups.
79 constexpr bool matches(uint64_t address) const { return this->address() == (address & ADDRESS_MASK); }
80 constexpr uint64_t address() const { return this->word_ & ADDRESS_MASK; }
81 constexpr conn_err_t error() const { return static_cast<int16_t>(this->word_ >> 48); }
82
83 private:
84 static constexpr uint64_t ADDRESS_MASK = 0x0000FFFFFFFFFFFFULL;
85 uint64_t word_{0};
86};
87// Pin the packing at compile time: mask and sign round-trip for every
88// reachable shape (negative, GATT status, ESP_ERR_* range, stray high bit).
89constexpr bool pending_reply_round_trips(uint64_t address, uint64_t expected_address, conn_err_t error) {
91 p.set(address, error);
92 return p.address() == expected_address && p.error() == error && !p.empty() && p.matches(address);
93}
94static_assert(pending_reply_round_trips(0x0000112233445566ULL, 0x0000112233445566ULL, -1));
95static_assert(pending_reply_round_trips(0x0000FFFFFFFFFFFFULL, 0x0000FFFFFFFFFFFFULL, 0x8F));
96static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x0000112233445566ULL, 0x110));
97static_assert(PendingReply{}.empty());
98#endif
99
100#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
115 void *instance{nullptr};
116 bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr};
118 bool is_set() const { return this->fn != nullptr; }
119 bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); }
120};
121#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
122
123class BluetoothProxy final : public Component {
124#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
125 // Allow the connection to update connections_free_response_
127#endif
128 public:
130 void set_ble_hub(ble_device_base::BLEHub *hub) { this->hub_ = hub; }
131 void dump_config() override;
132 void setup() override;
133 void loop() override;
134
135#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
137#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
138#ifndef USE_ESP32
139 // Run after the hub's setup() (the trackers use AFTER_WIFI): setup() below
140 // snapshots scan_active()/scan_running() and installs the raw callback, and
141 // the BLEHub contract does not promise those are settled any earlier than
142 // the hub's own setup().
143 float get_setup_priority() const override { return setup_priority::AFTER_WIFI - 1.0f; }
144#endif // !USE_ESP32
145
146#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
155#endif
156
162 return this->api_connection_ != nullptr && this->api_connection_->client_supports_api_version(1, 12);
163 }
164
165#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
169 bool send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, conn_err_t error = CONN_OK);
171 void send_connections_free(api::APIConnection *api_connection);
173 bool send_gatt_services_done(uint64_t address);
175 bool send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error);
176 void send_device_pairing(uint64_t address, bool paired, conn_err_t error = CONN_OK);
179 void send_device_unpairing(uint64_t address, bool success, conn_err_t error);
180 void send_device_clear_cache(uint64_t address, bool success, conn_err_t error = CONN_OK);
181#endif
182
183 void bluetooth_scanner_set_mode(bool active);
184
185 void set_active(bool active) { this->active_ = active; }
186 bool has_active() { return this->active_; }
187
188#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
191#endif
192
194 if (!this->active_) {
195 return LEGACY_PASSIVE_ONLY_VERSION;
196 }
197 // Legacy clients (which predate the feature flags) map versions to
198 // capability sets: 5 adds cache clearing, 4 adds pairing, 3 is active
199 // connections only.
200 if (bluetooth_connection::SUPPORTS_CACHE_CLEARING) {
201 return LEGACY_ACTIVE_CONNECTIONS_VERSION;
202 }
203 return bluetooth_connection::SUPPORTS_PAIRING ? LEGACY_ACTIVE_NO_CACHE_CLEAR_VERSION
204 : LEGACY_ACTIVE_NO_PAIRING_VERSION;
205 }
206
208 uint32_t flags = 0;
211#ifdef USE_ESP32
213#else
214 // Advertise mode switching only where the hub honors request_scan_mode();
215 // scan_mode_switch is the capability bit for exactly that (#18079) —
216 // active_scan alone is not enough, a hub may support active scanning yet
217 // refuse the runtime switch.
218 if (ble_device_base::BLEHub::get_capabilities().scan_mode_switch) {
220 }
221#endif
222 if (this->active_) {
223 // REMOTE_CACHING is mandatory for active connections: API clients
224 // refuse to connect without it (it selects which V3 connect request
225 // they send, not device-side caching).
229 if (bluetooth_connection::SUPPORTS_PAIRING) {
231 }
232 if (bluetooth_connection::SUPPORTS_CACHE_CLEARING) {
234 }
235 }
236
237 return flags;
238 }
239
240 void get_bluetooth_mac_address_pretty(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> output) {
241 uint8_t mac[MAC_ADDRESS_SIZE] = {};
242 this->hub_->get_adapter_mac(mac);
243 // Unavailable -> empty string: some hubs (rp2040's BTstack) only learn
244 // the address once the link layer is up, and report all-zero until then.
245 if (mac_address_is_valid(mac)) {
246 format_mac_addr_upper(mac, output.data());
247 } else {
248 output[0] = '\0';
249 }
250 }
251
252 protected:
254#ifdef USE_BLE_SCANNER_STATE_CALLBACK
256#else
258#endif
260
263 if (this->response_.advertisements_len == 0)
264 return;
265 // Perishable and the highest-frequency send here: a drop only reports at
266 // V, anything louder would be the flood the batch pacing exists to avoid.
267 [[maybe_unused]] bool sent = this->api_connection_->send_message(this->response_);
268#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
269 this->log_advertisement_flush_(sent);
270#endif
272 }
273 void log_advertisement_flush_(bool sent);
274
275#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
276 BluetoothConnection *get_connection_(uint64_t address, bool reserve);
278 void log_connection_info_(BluetoothConnection *connection, const char *message);
279 void log_not_connected_gatt_(const char *action, const char *type);
280 void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
281
285 void update_address_slot_(uint64_t old_address, uint64_t new_address) {
286 auto &resp = this->connections_free_response_;
287 if (new_address == 0 && old_address != 0) {
288 if (resp.free < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
289 resp.free++;
290 } else {
292 }
293 this->replace_allocated_slot_(old_address, 0);
294 } else if (new_address != 0 && old_address == 0) {
295 if (resp.free > 0) {
296 resp.free--;
297 } else {
299 }
300 this->replace_allocated_slot_(0, new_address);
301 }
302 }
303 void replace_allocated_slot_(uint64_t find_value, uint64_t set_value);
310 void reset_connection_slot_(BluetoothConnection *connection, conn_err_t reason);
317 void send_device_disconnected_(uint64_t address, conn_err_t error = CONN_OK);
323 void latch_pending_disconnection_(uint64_t address, conn_err_t error);
324#endif
325
330 void reset_owed_replies_();
331#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
333 void log_reply_dropped_(const char *what, uint64_t address);
335 void log_reply_deferred_(const char *what, uint64_t address);
337 void log_reply_displaced_(const char *what, uint64_t owed, uint64_t address);
338#endif
339
340 // Memory optimized layout for 32-bit systems
341 // Group 1: Pointers (4 bytes each, naturally aligned)
343
344#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
345 // Group 2: Fixed-size array of connection pointers
346 std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
347 // Address-keyed pool of owed freed-slot notifications; loop() resends.
348 // Proxy-only state, kept off BluetoothConnection; entries are not tied to
349 // slot indices.
350 std::array<PendingReply, BLUETOOTH_PROXY_MAX_CONNECTIONS> pending_disconnections_{};
351 // Owed unpair reply. The bond is already gone when the send is refused, so
352 // a retry is told the unpair failed when it succeeded. One slot: a second
353 // refused unpair displaces the first, as happened to both before this.
355#endif
357 // Group 3: 4-byte types; paired with hub_ so the 8-aligned messages below
358 // start on an even word, closing two alignment holes.
360
361#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
363#endif
364
365 // BLE advertisement batching
367
368#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
369 // Pre-allocated response message - always ready to send
371#endif
372
373 // Group 4: 1-byte types grouped together
375#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
376 // A dropped send (full TCP buffer) would leave the API client with a stale
377 // slot state forever; the cached response is current by construction, so
378 // retrying it from loop() is an idempotent resync.
381#endif
382 bool configured_scan_active_{false}; // Configured scan mode from YAML
383#ifdef USE_WIFI
387 bool adv_flush_toggle_{false};
388#endif
389#ifdef USE_BLE_SCANNER_STATE_CALLBACK
390 // A dropped push (full TX buffer) is re-queried from the hub and resent
391 // from loop(); the hub's current state is idempotent by construction.
393#else
394 bool last_scan_running_{false}; // Last scanner state reported to the subscriber
395#endif
396};
397
398extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
399
400} // namespace esphome::bluetooth_proxy
401
402#endif // USE_BLUETOOTH_PROXY
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
bool client_supports_api_version(uint8_t major, uint8_t minor) const
bool send_message(const T &msg)
Returns false as soon as the TCP buffer is full.
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)
void set_advertisement_filter(AdvertisementFilter filter)
One subscriber; a later call replaces an earlier one.
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 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 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 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 set_ble_hub(ble_device_base::BLEHub *hub)
void flush_pending_advertisements_()
Caller must ensure api_connection_ is non-null and API server is connected.
bool client_supports_efficient_uuids() const
Whether the subscribed API client understands 16/32-bit UUID fields.
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 flags
uint8_t type
bool state
Definition fan.h:2
ScannerState
Scanner lifecycle, wire-value aligned with the api enum so consumers cast directly (pinned by static_...
Definition ble_hub.h:53
ESPHOME_BLE_HUB_TYPE BLEHub
constexpr bool pending_reply_round_trips(uint64_t address, uint64_t expected_address, conn_err_t error)
BluetoothProxy * global_bluetooth_proxy
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
bool mac_address_is_valid(const uint8_t *mac)
Check if the MAC address is not all zeros or all ones.
Definition helpers.cpp:865
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:1676
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
Predicate slot letting an external component drop advertisements before they are queued for the API.
bool should_forward(const ble_device_base::RawAdvertisement &adv) const
bool(* fn)(void *instance, const ble_device_base::RawAdvertisement &adv)
bool is_set() const
A default-constructed slot is "no filter"; the proxy guards on this.
spi_device_handle_t handle