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