ESPHome 2026.8.0b4
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
34using ClientState = ble_device_base::ClientState;
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
100class BluetoothProxy final : public Component {
101#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
102 // Allow the connection to update connections_free_response_
104#endif
105 public:
107 void set_ble_hub(ble_device_base::BLEHub *hub) { this->hub_ = hub; }
108 void dump_config() override;
109 void setup() override;
110 void loop() override;
111
112#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
114#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
115#ifndef USE_ESP32
116 // Run after the hub's setup() (the trackers use AFTER_WIFI): setup() below
117 // snapshots scan_active()/scan_running() and installs the raw callback, and
118 // the BLEHub contract does not promise those are settled any earlier than
119 // the hub's own setup().
120 float get_setup_priority() const override { return setup_priority::AFTER_WIFI - 1.0f; }
121#endif // !USE_ESP32
122
123#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
132#endif
133
139 return this->api_connection_ != nullptr && this->api_connection_->client_supports_api_version(1, 12);
140 }
141
142#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
146 bool send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, conn_err_t error = CONN_OK);
148 void send_connections_free(api::APIConnection *api_connection);
150 bool send_gatt_services_done(uint64_t address);
152 bool send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error);
153 void send_device_pairing(uint64_t address, bool paired, conn_err_t error = CONN_OK);
156 void send_device_unpairing(uint64_t address, bool success, conn_err_t error);
157 void send_device_clear_cache(uint64_t address, bool success, conn_err_t error = CONN_OK);
158#endif
159
160 void bluetooth_scanner_set_mode(bool active);
161
162 void set_active(bool active) { this->active_ = active; }
163 bool has_active() { return this->active_; }
164
166 if (!this->active_) {
167 return LEGACY_PASSIVE_ONLY_VERSION;
168 }
169 // Legacy clients (which predate the feature flags) map versions to
170 // capability sets: 5 adds cache clearing, 4 adds pairing, 3 is active
171 // connections only.
172 if (bluetooth_connection::SUPPORTS_CACHE_CLEARING) {
173 return LEGACY_ACTIVE_CONNECTIONS_VERSION;
174 }
175 return bluetooth_connection::SUPPORTS_PAIRING ? LEGACY_ACTIVE_NO_CACHE_CLEAR_VERSION
176 : LEGACY_ACTIVE_NO_PAIRING_VERSION;
177 }
178
180 uint32_t flags = 0;
183#ifdef USE_ESP32
185#else
186 // Advertise mode switching only where the hub honors request_scan_mode();
187 // scan_mode_switch is the capability bit for exactly that (#18079) —
188 // active_scan alone is not enough, a hub may support active scanning yet
189 // refuse the runtime switch.
190 if (ble_device_base::BLEHub::get_capabilities().scan_mode_switch) {
192 }
193#endif
194 if (this->active_) {
195 // REMOTE_CACHING is mandatory for active connections: API clients
196 // refuse to connect without it (it selects which V3 connect request
197 // they send, not device-side caching).
201 if (bluetooth_connection::SUPPORTS_PAIRING) {
203 }
204 if (bluetooth_connection::SUPPORTS_CACHE_CLEARING) {
206 }
207 }
208
209 return flags;
210 }
211
212 void get_bluetooth_mac_address_pretty(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> output) {
213 uint8_t mac[MAC_ADDRESS_SIZE] = {};
214 this->hub_->get_adapter_mac(mac);
215 // Unavailable -> empty string: some hubs (rp2040's BTstack) only learn
216 // the address once the link layer is up, and report all-zero until then.
217 if (mac_address_is_valid(mac)) {
218 format_mac_addr_upper(mac, output.data());
219 } else {
220 output[0] = '\0';
221 }
222 }
223
224 protected:
226#ifdef USE_BLE_SCANNER_STATE_CALLBACK
228#else
230#endif
232
235 if (this->response_.advertisements_len == 0)
236 return;
237 // Perishable and the highest-frequency send here: a drop only reports at
238 // V, anything louder would be the flood the batch pacing exists to avoid.
239 [[maybe_unused]] bool sent = this->api_connection_->send_message(this->response_);
240#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
241 this->log_advertisement_flush_(sent);
242#endif
244 }
245 void log_advertisement_flush_(bool sent);
246
247#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
248 BluetoothConnection *get_connection_(uint64_t address, bool reserve);
249 void log_connection_request_ignored_(BluetoothConnection *connection, ClientState state);
250 void log_connection_info_(BluetoothConnection *connection, const char *message);
251 void log_not_connected_gatt_(const char *action, const char *type);
252 void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
253
257 void update_address_slot_(uint64_t old_address, uint64_t new_address) {
258 auto &resp = this->connections_free_response_;
259 if (new_address == 0 && old_address != 0) {
260 if (resp.free < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
261 resp.free++;
262 } else {
264 }
265 this->replace_allocated_slot_(old_address, 0);
266 } else if (new_address != 0 && old_address == 0) {
267 if (resp.free > 0) {
268 resp.free--;
269 } else {
271 }
272 this->replace_allocated_slot_(0, new_address);
273 }
274 }
275 void replace_allocated_slot_(uint64_t find_value, uint64_t set_value);
282 void reset_connection_slot_(BluetoothConnection *connection, conn_err_t reason);
289 void send_device_disconnected_(uint64_t address, conn_err_t error = CONN_OK);
295 void latch_pending_disconnection_(uint64_t address, conn_err_t error);
296#endif
297
302 void reset_owed_replies_();
303#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
305 void log_reply_dropped_(const char *what, uint64_t address);
307 void log_reply_deferred_(const char *what, uint64_t address);
309 void log_reply_displaced_(const char *what, uint64_t owed, uint64_t address);
310#endif
311
312 // Memory optimized layout for 32-bit systems
313 // Group 1: Pointers (4 bytes each, naturally aligned)
315
316#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
317 // Group 2: Fixed-size array of connection pointers
318 std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
319 // Address-keyed pool of owed freed-slot notifications; loop() resends.
320 // Proxy-only state, kept off BluetoothConnection; entries are not tied to
321 // slot indices.
322 std::array<PendingReply, BLUETOOTH_PROXY_MAX_CONNECTIONS> pending_disconnections_{};
323 // Owed unpair reply. The bond is already gone when the send is refused, so
324 // a retry is told the unpair failed when it succeeded. One slot: a second
325 // refused unpair displaces the first, as happened to both before this.
327#endif
329 // Group 3: 4-byte types; paired with hub_ so the 8-aligned messages below
330 // start on an even word, closing two alignment holes.
332
333 // BLE advertisement batching
335
336#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
337 // Pre-allocated response message - always ready to send
339#endif
340
341 // Group 4: 1-byte types grouped together
343#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
344 // A dropped send (full TCP buffer) would leave the API client with a stale
345 // slot state forever; the cached response is current by construction, so
346 // retrying it from loop() is an idempotent resync.
349#endif
350 bool configured_scan_active_{false}; // Configured scan mode from YAML
351#ifdef USE_WIFI
355 bool adv_flush_toggle_{false};
356#endif
357#ifdef USE_BLE_SCANNER_STATE_CALLBACK
358 // A dropped push (full TX buffer) is re-queried from the hub and resent
359 // from loop(); the hub's current state is idempotent by construction.
361#else
362 bool last_scan_running_{false}; // Last scanner state reported to the subscriber
363#endif
364};
365
366extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
367
368} // namespace esphome::bluetooth_proxy
369
370#endif // USE_BLUETOOTH_PROXY
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
bool send_message(const T &msg)
Returns false as soon as the TCP buffer is full.
bool client_supports_api_version(uint16_t major, uint16_t minor) const
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 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 type
uint16_t flags
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:827
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
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
spi_device_handle_t handle