ESPHome 2025.6.3
Loading...
Searching...
No Matches
ble_event.h
Go to the documentation of this file.
1#pragma once
2
3#ifdef USE_ESP32
4
5#include <cstddef> // for offsetof
6#include <vector>
7
8#include <esp_gap_ble_api.h>
9#include <esp_gattc_api.h>
10#include <esp_gatts_api.h>
11
12#include "ble_scan_result.h"
13
14namespace esphome {
15namespace esp32_ble {
16
17// Compile-time verification that ESP-IDF scan complete events only contain a status field
18// This ensures our reinterpret_cast in ble.cpp is safe
19static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param) == sizeof(esp_bt_status_t),
20 "ESP-IDF scan_param_cmpl structure has unexpected size");
21static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
22 "ESP-IDF scan_start_cmpl structure has unexpected size");
23static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
24 "ESP-IDF scan_stop_cmpl structure has unexpected size");
25
26// Verify the status field is at offset 0 (first member)
27static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0,
28 "status must be first member of scan_param_cmpl");
29static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0,
30 "status must be first member of scan_start_cmpl");
31static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0,
32 "status must be first member of scan_stop_cmpl");
33
34// Compile-time verification for advertising complete events
35static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
36 "ESP-IDF adv_data_cmpl structure has unexpected size");
37static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
38 "ESP-IDF scan_rsp_data_cmpl structure has unexpected size");
39static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t),
40 "ESP-IDF adv_data_raw_cmpl structure has unexpected size");
41static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
42 "ESP-IDF adv_start_cmpl structure has unexpected size");
43static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
44 "ESP-IDF adv_stop_cmpl structure has unexpected size");
45
46// Verify the status field is at offset 0 for advertising events
47static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0,
48 "status must be first member of adv_data_cmpl");
49static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0,
50 "status must be first member of scan_rsp_data_cmpl");
51static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0,
52 "status must be first member of adv_data_raw_cmpl");
53static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0,
54 "status must be first member of adv_start_cmpl");
55static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0,
56 "status must be first member of adv_stop_cmpl");
57
58// Compile-time verification for RSSI complete event structure
59static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0,
60 "status must be first member of read_rssi_cmpl");
61static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t),
62 "rssi must immediately follow status in read_rssi_cmpl");
63static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
64 "remote_addr must follow rssi in read_rssi_cmpl");
65
66// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
67// This class stores each event with minimal memory usage.
68// GAP events (99% of traffic) don't have the vector overhead.
69// GATTC/GATTS events use heap allocation for their param and data.
70//
71// Event flow:
72// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
73// 2. The handlers create a BLEEvent instance, copying only the data we need
74// 3. The event is pushed to a thread-safe queue
75// 4. In the main loop(), events are popped from the queue and processed
76// 5. The event destructor cleans up any external allocations
77//
78// Thread safety:
79// - GAP events: We copy only the fields we need directly into the union
80// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
81// the data remains valid even after the BLE callback returns. The original
82// param pointer from ESP-IDF is only valid during the callback.
83//
84// CRITICAL DESIGN NOTE:
85// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety.
86// DO NOT attempt to optimize by removing these allocations or storing pointers
87// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime
88// than our event processing, and accessing it after the callback returns would
89// result in use-after-free bugs and crashes.
90class BLEEvent {
91 public:
92 // NOLINTNEXTLINE(readability-identifier-naming)
93 enum ble_event_t : uint8_t {
97 };
98
99 // Type definitions for cleaner method signatures
101 esp_bt_status_t status;
102 };
103
105 esp_bt_status_t status;
106 int8_t rssi;
107 esp_bd_addr_t remote_addr;
108 };
109
110 // Constructor for GAP events - no external allocations needed
111 BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
112 this->type_ = GAP;
113 this->init_gap_data_(e, p);
114 }
115
116 // Constructor for GATTC events - uses heap allocation
117 // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
118 // The param pointer from ESP-IDF is only valid during the callback execution.
119 // Since BLE events are processed asynchronously in the main loop, we must create
120 // our own copy to ensure the data remains valid until the event is processed.
121 BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
122 this->type_ = GATTC;
123 this->init_gattc_data_(e, i, p);
124 }
125
126 // Constructor for GATTS events - uses heap allocation
127 // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
128 // The param pointer from ESP-IDF is only valid during the callback execution.
129 // Since BLE events are processed asynchronously in the main loop, we must create
130 // our own copy to ensure the data remains valid until the event is processed.
131 BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
132 this->type_ = GATTS;
133 this->init_gatts_data_(e, i, p);
134 }
135
136 // Destructor to clean up heap allocations
138
139 // Default constructor for pre-allocation in pool
141
142 // Clean up any heap-allocated data
144 if (this->type_ == GAP) {
145 return;
146 }
147 if (this->type_ == GATTC) {
148 delete this->event_.gattc.gattc_param;
149 delete this->event_.gattc.data;
150 this->event_.gattc.gattc_param = nullptr;
151 this->event_.gattc.data = nullptr;
152 return;
153 }
154 if (this->type_ == GATTS) {
155 delete this->event_.gatts.gatts_param;
156 delete this->event_.gatts.data;
157 this->event_.gatts.gatts_param = nullptr;
158 this->event_.gatts.data = nullptr;
159 }
160 }
161
162 // Load new event data for reuse (replaces previous event data)
163 void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
164 this->cleanup_heap_data();
165 this->type_ = GAP;
166 this->init_gap_data_(e, p);
167 }
168
169 void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
170 this->cleanup_heap_data();
171 this->type_ = GATTC;
172 this->init_gattc_data_(e, i, p);
173 }
174
175 void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
176 this->cleanup_heap_data();
177 this->type_ = GATTS;
178 this->init_gatts_data_(e, i, p);
179 }
180
181 // Disable copy to prevent double-delete
182 BLEEvent(const BLEEvent &) = delete;
183 BLEEvent &operator=(const BLEEvent &) = delete;
184
185 union {
186 // NOLINTNEXTLINE(readability-identifier-naming)
187 struct gap_event {
188 esp_gap_ble_cb_event_t gap_event;
189 union {
190 BLEScanResult scan_result; // 73 bytes - Used by: esp32_ble_tracker
191 // This matches ESP-IDF's scan complete event structures
192 // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout
193 // Used by: esp32_ble_tracker
195 // Advertising complete events all have same structure
196 // Used by: esp32_ble_beacon, esp32_ble server components
197 // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP
199 // RSSI complete event
200 // Used by: ble_client (ble_rssi_sensor component)
202 // Security events - we store the full security union
203 // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client
204 esp_ble_sec_t security; // Variable size, but fits within scan_result size
205 };
206 } gap; // 80 bytes total
207
208 // NOLINTNEXTLINE(readability-identifier-naming)
209 struct gattc_event {
210 esp_gattc_cb_event_t gattc_event;
211 esp_gatt_if_t gattc_if;
212 esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated
213 std::vector<uint8_t> *data; // Heap-allocated
214 } gattc; // 16 bytes (pointers only)
215
216 // NOLINTNEXTLINE(readability-identifier-naming)
217 struct gatts_event {
218 esp_gatts_cb_event_t gatts_event;
219 esp_gatt_if_t gatts_if;
220 esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated
221 std::vector<uint8_t> *data; // Heap-allocated
222 } gatts; // 16 bytes (pointers only)
223 } event_; // 80 bytes
224
226
227 // Helper methods to access event data
228 ble_event_t type() const { return type_; }
229 esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
230 const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
231 esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
232 esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; }
233 const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; }
234 const esp_ble_sec_t &security() const { return event_.gap.security; }
235
236 private:
237 // Initialize GAP event data
238 void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
239 this->event_.gap.gap_event = e;
240
241 if (p == nullptr) {
242 return; // Invalid event, but we can't log in header file
243 }
244
245 // Copy data based on event type
246 switch (e) {
247 case ESP_GAP_BLE_SCAN_RESULT_EVT:
248 memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
249 this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
250 this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
251 this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
252 this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
253 this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
254 memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
255 ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
256 break;
257
258 case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
259 this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
260 break;
261
262 case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
263 this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
264 break;
265
266 case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
267 this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
268 break;
269
270 // Advertising complete events - all have same structure with just status
271 // Used by: esp32_ble_beacon, esp32_ble server components
272 case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
273 this->event_.gap.adv_complete.status = p->adv_data_cmpl.status;
274 break;
275 case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
276 this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status;
277 break;
278 case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon
279 this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status;
280 break;
281 case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon
282 this->event_.gap.adv_complete.status = p->adv_start_cmpl.status;
283 break;
284 case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon
285 this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status;
286 break;
287
288 // RSSI complete event
289 // Used by: ble_client (ble_rssi_sensor)
290 case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
291 this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status;
292 this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi;
293 memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t));
294 break;
295
296 // Security events - copy the entire security union
297 // Used by: ble_client, bluetooth_proxy, esp32_ble_client
298 case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client
299 case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client
300 case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation
301 case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation
302 case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation
303 memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t));
304 break;
305
306 default:
307 // We only store data for GAP events that components currently use
308 // Unknown events still get queued and logged in ble.cpp:375 as
309 // "Unhandled GAP event type in loop" - this helps identify new events
310 // that components might need in the future
311 break;
312 }
313 }
314
315 // Initialize GATTC event data
316 void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
317 this->event_.gattc.gattc_event = e;
318 this->event_.gattc.gattc_if = i;
319
320 if (p == nullptr) {
321 this->event_.gattc.gattc_param = nullptr;
322 this->event_.gattc.data = nullptr;
323 return; // Invalid event, but we can't log in header file
324 }
325
326 // Heap-allocate param and data
327 // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
328 // while GAP events (99%) are stored inline to minimize memory usage
329 // IMPORTANT: This heap allocation provides clear ownership semantics:
330 // - The BLEEvent owns the allocated memory for its lifetime
331 // - The data remains valid from the BLE callback context until processed in the main loop
332 // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
333 this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
334
335 // Copy data for events that need it
336 // The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
337 // We must copy this data to ensure it remains valid when the event is processed later.
338 switch (e) {
339 case ESP_GATTC_NOTIFY_EVT:
340 this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
341 this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
342 break;
343 case ESP_GATTC_READ_CHAR_EVT:
344 case ESP_GATTC_READ_DESCR_EVT:
345 this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
346 this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
347 break;
348 default:
349 this->event_.gattc.data = nullptr;
350 break;
351 }
352 }
353
354 // Initialize GATTS event data
355 void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
356 this->event_.gatts.gatts_event = e;
357 this->event_.gatts.gatts_if = i;
358
359 if (p == nullptr) {
360 this->event_.gatts.gatts_param = nullptr;
361 this->event_.gatts.data = nullptr;
362 return; // Invalid event, but we can't log in header file
363 }
364
365 // Heap-allocate param and data
366 // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
367 // while GAP events (99%) are stored inline to minimize memory usage
368 // IMPORTANT: This heap allocation provides clear ownership semantics:
369 // - The BLEEvent owns the allocated memory for its lifetime
370 // - The data remains valid from the BLE callback context until processed in the main loop
371 // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
372 this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
373
374 // Copy data for events that need it
375 // The param struct contains pointers (e.g., write.value) that point to temporary buffers.
376 // We must copy this data to ensure it remains valid when the event is processed later.
377 switch (e) {
378 case ESP_GATTS_WRITE_EVT:
379 this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
380 this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
381 break;
382 default:
383 this->event_.gatts.data = nullptr;
384 break;
385 }
386 }
387};
388
389// Verify the gap_event struct hasn't grown beyond expected size
390// The gap member in the union should be 80 bytes (including the gap_event enum)
391static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
392
393// Verify esp_ble_sec_t fits within our union
394static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");
395
396// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
397
398} // namespace esp32_ble
399} // namespace esphome
400
401#endif
struct esphome::esp32_ble::BLEEvent::@77::gattc_event gattc
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p)
Definition ble_event.h:121
BLEEvent(const BLEEvent &)=delete
struct esphome::esp32_ble::BLEEvent::@77::gap_event gap
esp_bt_status_t adv_complete_status() const
Definition ble_event.h:232
StatusOnlyData scan_complete
Definition ble_event.h:194
esp_ble_gatts_cb_param_t * gatts_param
Definition ble_event.h:220
const esp_ble_sec_t & security() const
Definition ble_event.h:234
void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p)
Definition ble_event.h:163
esp_bt_status_t scan_complete_status() const
Definition ble_event.h:231
ble_event_t type() const
Definition ble_event.h:228
BLEEvent & operator=(const BLEEvent &)=delete
esp_gatts_cb_event_t gatts_event
Definition ble_event.h:218
struct esphome::esp32_ble::BLEEvent::@77::gatts_event gatts
esp_gap_ble_cb_event_t gap_event
Definition ble_event.h:188
std::vector< uint8_t > * data
Definition ble_event.h:213
esp_gattc_cb_event_t gattc_event
Definition ble_event.h:210
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p)
Definition ble_event.h:111
union esphome::esp32_ble::BLEEvent::@77 event_
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p)
Definition ble_event.h:131
esp_gap_ble_cb_event_t gap_event_type() const
Definition ble_event.h:229
void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p)
Definition ble_event.h:175
void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p)
Definition ble_event.h:169
StatusOnlyData adv_complete
Definition ble_event.h:198
RSSICompleteData read_rssi_complete
Definition ble_event.h:201
const RSSICompleteData & read_rssi_complete() const
Definition ble_event.h:233
esp_ble_gattc_cb_param_t * gattc_param
Definition ble_event.h:212
const BLEScanResult & scan_result() const
Definition ble_event.h:230
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7