esphome 2025.6.0b2__py3-none-any.whl → 2025.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. esphome/components/api/api_pb2.cpp +2 -0
  2. esphome/components/api/api_pb2.h +1 -0
  3. esphome/components/esp32_ble/ble.cpp +108 -46
  4. esphome/components/esp32_ble/ble.h +2 -0
  5. esphome/components/esp32_ble/ble_event.h +242 -75
  6. esphome/components/esp32_ble/ble_event_pool.h +72 -0
  7. esphome/components/esp32_ble/queue.h +14 -11
  8. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +1 -0
  9. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +4 -0
  10. esphome/components/i2s_audio/i2s_audio.cpp +1 -1
  11. esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +37 -22
  12. esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +2 -0
  13. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +28 -10
  14. esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +1 -0
  15. esphome/components/light/light_state.h +15 -15
  16. esphome/components/nextion/nextion.cpp +2 -10
  17. esphome/components/openthread/__init__.py +5 -5
  18. esphome/components/openthread/openthread.cpp +3 -3
  19. esphome/components/openthread/tlv.py +7 -0
  20. esphome/components/spi/spi_arduino.cpp +22 -9
  21. esphome/components/switch/switch.h +13 -7
  22. esphome/config_validation.py +44 -1
  23. esphome/const.py +1 -1
  24. esphome/yaml_util.py +2 -1
  25. {esphome-2025.6.0b2.dist-info → esphome-2025.6.1.dist-info}/METADATA +1 -1
  26. {esphome-2025.6.0b2.dist-info → esphome-2025.6.1.dist-info}/RECORD +30 -29
  27. {esphome-2025.6.0b2.dist-info → esphome-2025.6.1.dist-info}/WHEEL +0 -0
  28. {esphome-2025.6.0b2.dist-info → esphome-2025.6.1.dist-info}/entry_points.txt +0 -0
  29. {esphome-2025.6.0b2.dist-info → esphome-2025.6.1.dist-info}/licenses/LICENSE +0 -0
  30. {esphome-2025.6.0b2.dist-info → esphome-2025.6.1.dist-info}/top_level.txt +0 -0
@@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
516
516
  return "VOICE_ASSISTANT_TTS_STREAM_START";
517
517
  case enums::VOICE_ASSISTANT_TTS_STREAM_END:
518
518
  return "VOICE_ASSISTANT_TTS_STREAM_END";
519
+ case enums::VOICE_ASSISTANT_INTENT_PROGRESS:
520
+ return "VOICE_ASSISTANT_INTENT_PROGRESS";
519
521
  default:
520
522
  return "UNKNOWN";
521
523
  }
@@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t {
208
208
  VOICE_ASSISTANT_STT_VAD_END = 12,
209
209
  VOICE_ASSISTANT_TTS_STREAM_START = 98,
210
210
  VOICE_ASSISTANT_TTS_STREAM_END = 99,
211
+ VOICE_ASSISTANT_INTENT_PROGRESS = 100,
211
212
  };
212
213
  enum VoiceAssistantTimerEvent : uint32_t {
213
214
  VOICE_ASSISTANT_TIMER_STARTED = 0,
@@ -1,6 +1,7 @@
1
1
  #ifdef USE_ESP32
2
2
 
3
3
  #include "ble.h"
4
+ #include "ble_event_pool.h"
4
5
 
5
6
  #include "esphome/core/application.h"
6
7
  #include "esphome/core/log.h"
@@ -23,9 +24,6 @@ namespace esp32_ble {
23
24
 
24
25
  static const char *const TAG = "esp32_ble";
25
26
 
26
- static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
27
- RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
28
-
29
27
  void ESP32BLE::setup() {
30
28
  global_ble = this;
31
29
  ESP_LOGCONFIG(TAG, "Running setup");
@@ -326,32 +324,77 @@ void ESP32BLE::loop() {
326
324
  }
327
325
  case BLEEvent::GAP: {
328
326
  esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
329
- if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) {
330
- // Use the new scan event handler - no memcpy!
331
- for (auto *scan_handler : this->gap_scan_event_handlers_) {
332
- scan_handler->gap_scan_event_handler(ble_event->scan_result());
333
- }
334
- } else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT ||
335
- gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT ||
336
- gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) {
337
- // All three scan complete events have the same structure with just status
338
- // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
339
- // This is verified at compile-time by static_assert checks in ble_event.h
340
- // The struct already contains our copy of the status (copied in BLEEvent constructor)
341
- ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
342
- for (auto *gap_handler : this->gap_event_handlers_) {
343
- gap_handler->gap_event_handler(
344
- gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
345
- }
327
+ switch (gap_event) {
328
+ case ESP_GAP_BLE_SCAN_RESULT_EVT:
329
+ // Use the new scan event handler - no memcpy!
330
+ for (auto *scan_handler : this->gap_scan_event_handlers_) {
331
+ scan_handler->gap_scan_event_handler(ble_event->scan_result());
332
+ }
333
+ break;
334
+
335
+ // Scan complete events
336
+ case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
337
+ case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
338
+ case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
339
+ // All three scan complete events have the same structure with just status
340
+ // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
341
+ // This is verified at compile-time by static_assert checks in ble_event.h
342
+ // The struct already contains our copy of the status (copied in BLEEvent constructor)
343
+ ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
344
+ for (auto *gap_handler : this->gap_event_handlers_) {
345
+ gap_handler->gap_event_handler(
346
+ gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
347
+ }
348
+ break;
349
+
350
+ // Advertising complete events
351
+ case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
352
+ case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
353
+ case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
354
+ case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
355
+ case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
356
+ // All advertising complete events have the same structure with just status
357
+ ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
358
+ for (auto *gap_handler : this->gap_event_handlers_) {
359
+ gap_handler->gap_event_handler(
360
+ gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
361
+ }
362
+ break;
363
+
364
+ // RSSI complete event
365
+ case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
366
+ ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
367
+ for (auto *gap_handler : this->gap_event_handlers_) {
368
+ gap_handler->gap_event_handler(
369
+ gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
370
+ }
371
+ break;
372
+
373
+ // Security events
374
+ case ESP_GAP_BLE_AUTH_CMPL_EVT:
375
+ case ESP_GAP_BLE_SEC_REQ_EVT:
376
+ case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
377
+ case ESP_GAP_BLE_PASSKEY_REQ_EVT:
378
+ case ESP_GAP_BLE_NC_REQ_EVT:
379
+ ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
380
+ for (auto *gap_handler : this->gap_event_handlers_) {
381
+ gap_handler->gap_event_handler(
382
+ gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
383
+ }
384
+ break;
385
+
386
+ default:
387
+ // Unknown/unhandled event
388
+ ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event);
389
+ break;
346
390
  }
347
391
  break;
348
392
  }
349
393
  default:
350
394
  break;
351
395
  }
352
- // Destructor will clean up external allocations for GATTC/GATTS
353
- ble_event->~BLEEvent();
354
- EVENT_ALLOCATOR.deallocate(ble_event, 1);
396
+ // Return the event to the pool
397
+ this->ble_event_pool_.release(ble_event);
355
398
  ble_event = this->ble_events_.pop();
356
399
  }
357
400
  if (this->advertising_ != nullptr) {
@@ -359,37 +402,41 @@ void ESP32BLE::loop() {
359
402
  }
360
403
 
361
404
  // Log dropped events periodically
362
- size_t dropped = this->ble_events_.get_and_reset_dropped_count();
405
+ uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
363
406
  if (dropped > 0) {
364
- ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped);
407
+ ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
365
408
  }
366
409
  }
367
410
 
411
+ // Helper function to load new event data based on type
412
+ void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
413
+ event->load_gap_event(e, p);
414
+ }
415
+
416
+ void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
417
+ event->load_gattc_event(e, i, p);
418
+ }
419
+
420
+ void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
421
+ event->load_gatts_event(e, i, p);
422
+ }
423
+
368
424
  template<typename... Args> void enqueue_ble_event(Args... args) {
369
- // Check if queue is full before allocating
370
- if (global_ble->ble_events_.full()) {
371
- // Queue is full, drop the event
425
+ // Allocate an event from the pool
426
+ BLEEvent *event = global_ble->ble_event_pool_.allocate();
427
+ if (event == nullptr) {
428
+ // No events available - queue is full or we're out of memory
372
429
  global_ble->ble_events_.increment_dropped_count();
373
430
  return;
374
431
  }
375
432
 
376
- BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
377
- if (new_event == nullptr) {
378
- // Memory too fragmented to allocate new event. Can only drop it until memory comes back
379
- global_ble->ble_events_.increment_dropped_count();
380
- return;
381
- }
382
- new (new_event) BLEEvent(args...);
383
-
384
- // Push the event - since we're the only producer and we checked full() above,
385
- // this should always succeed unless we have a bug
386
- if (!global_ble->ble_events_.push(new_event)) {
387
- // This should not happen in SPSC queue with single producer
388
- ESP_LOGE(TAG, "BLE queue push failed unexpectedly");
389
- new_event->~BLEEvent();
390
- EVENT_ALLOCATOR.deallocate(new_event, 1);
391
- }
392
- } // NOLINT(clang-analyzer-unix.Malloc)
433
+ // Load new event data (replaces previous event)
434
+ load_ble_event(event, args...);
435
+
436
+ // Push the event to the queue
437
+ global_ble->ble_events_.push(event);
438
+ // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
439
+ }
393
440
 
394
441
  // Explicit template instantiations for the friend function
395
442
  template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
@@ -398,11 +445,26 @@ template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gat
398
445
 
399
446
  void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
400
447
  switch (event) {
401
- // Only queue the 4 GAP events we actually handle
448
+ // Queue GAP events that components need to handle
449
+ // Scanning events - used by esp32_ble_tracker
402
450
  case ESP_GAP_BLE_SCAN_RESULT_EVT:
403
451
  case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
404
452
  case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
405
453
  case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
454
+ // Advertising events - used by esp32_ble_beacon and esp32_ble server
455
+ case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
456
+ case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
457
+ case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
458
+ case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
459
+ case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
460
+ // Connection events - used by ble_client
461
+ case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
462
+ // Security events - used by ble_client and bluetooth_proxy
463
+ case ESP_GAP_BLE_AUTH_CMPL_EVT:
464
+ case ESP_GAP_BLE_SEC_REQ_EVT:
465
+ case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
466
+ case ESP_GAP_BLE_PASSKEY_REQ_EVT:
467
+ case ESP_GAP_BLE_NC_REQ_EVT:
406
468
  enqueue_ble_event(event, param);
407
469
  return;
408
470
 
@@ -12,6 +12,7 @@
12
12
  #include "esphome/core/helpers.h"
13
13
 
14
14
  #include "ble_event.h"
15
+ #include "ble_event_pool.h"
15
16
  #include "queue.h"
16
17
 
17
18
  #ifdef USE_ESP32
@@ -148,6 +149,7 @@ class ESP32BLE : public Component {
148
149
  BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
149
150
 
150
151
  LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
152
+ BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
151
153
  BLEAdvertising *advertising_{};
152
154
  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
153
155
  uint32_t advertising_cycle_time_{};
@@ -24,16 +24,45 @@ static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == si
24
24
  "ESP-IDF scan_stop_cmpl structure has unexpected size");
25
25
 
26
26
  // Verify the status field is at offset 0 (first member)
27
- static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) ==
28
- offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl),
27
+ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0,
29
28
  "status must be first member of scan_param_cmpl");
30
- static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) ==
31
- offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl),
29
+ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0,
32
30
  "status must be first member of scan_start_cmpl");
33
- static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
34
- offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl),
31
+ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0,
35
32
  "status must be first member of scan_stop_cmpl");
36
33
 
34
+ // Compile-time verification for advertising complete events
35
+ static_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");
37
+ static_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");
39
+ static_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");
41
+ static_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");
43
+ static_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
47
+ static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0,
48
+ "status must be first member of adv_data_cmpl");
49
+ static_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");
51
+ static_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");
53
+ static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0,
54
+ "status must be first member of adv_start_cmpl");
55
+ static_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
59
+ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0,
60
+ "status must be first member of read_rssi_cmpl");
61
+ static_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");
63
+ static_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
+
37
66
  // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
38
67
  // This class stores each event with minimal memory usage.
39
68
  // GAP events (99% of traffic) don't have the vector overhead.
@@ -51,6 +80,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
51
80
  // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
52
81
  // the data remains valid even after the BLE callback returns. The original
53
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.
54
90
  class BLEEvent {
55
91
  public:
56
92
  // NOLINTNEXTLINE(readability-identifier-naming)
@@ -60,19 +96,155 @@ class BLEEvent {
60
96
  GATTS,
61
97
  };
62
98
 
99
+ // Type definitions for cleaner method signatures
100
+ struct StatusOnlyData {
101
+ esp_bt_status_t status;
102
+ };
103
+
104
+ struct RSSICompleteData {
105
+ esp_bt_status_t status;
106
+ int8_t rssi;
107
+ esp_bd_addr_t remote_addr;
108
+ };
109
+
63
110
  // Constructor for GAP events - no external allocations needed
64
111
  BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
65
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
137
+ ~BLEEvent() { this->cleanup_heap_data(); }
138
+
139
+ // Default constructor for pre-allocation in pool
140
+ BLEEvent() : type_(GAP) {}
141
+
142
+ // Clean up any heap-allocated data
143
+ void cleanup_heap_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
194
+ StatusOnlyData scan_complete; // 1 byte
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
198
+ StatusOnlyData adv_complete; // 1 byte
199
+ // RSSI complete event
200
+ // Used by: ble_client (ble_rssi_sensor component)
201
+ RSSICompleteData read_rssi_complete; // 8 bytes
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
+
225
+ ble_event_t type_;
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) {
66
239
  this->event_.gap.gap_event = e;
67
240
 
68
241
  if (p == nullptr) {
69
242
  return; // Invalid event, but we can't log in header file
70
243
  }
71
244
 
72
- // Only copy the data we actually use for each GAP event type
245
+ // Copy data based on event type
73
246
  switch (e) {
74
247
  case ESP_GAP_BLE_SCAN_RESULT_EVT:
75
- // Copy only the fields we use from scan results
76
248
  memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
77
249
  this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
78
250
  this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
@@ -95,16 +267,53 @@ class BLEEvent {
95
267
  this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
96
268
  break;
97
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
+
98
306
  default:
99
- // We only handle 4 GAP event types, others are dropped
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
100
311
  break;
101
312
  }
102
313
  }
103
314
 
104
- // Constructor for GATTC events - uses heap allocation
105
- // Creates a copy of the param struct since the original is only valid during the callback
106
- BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
107
- this->type_ = GATTC;
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) {
108
317
  this->event_.gattc.gattc_event = e;
109
318
  this->event_.gattc.gattc_if = i;
110
319
 
@@ -117,9 +326,15 @@ class BLEEvent {
117
326
  // Heap-allocate param and data
118
327
  // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
119
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
120
333
  this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
121
334
 
122
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.
123
338
  switch (e) {
124
339
  case ESP_GATTC_NOTIFY_EVT:
125
340
  this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
@@ -136,10 +351,8 @@ class BLEEvent {
136
351
  }
137
352
  }
138
353
 
139
- // Constructor for GATTS events - uses heap allocation
140
- // Creates a copy of the param struct since the original is only valid during the callback
141
- BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
142
- this->type_ = GATTS;
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) {
143
356
  this->event_.gatts.gatts_event = e;
144
357
  this->event_.gatts.gatts_if = i;
145
358
 
@@ -152,9 +365,15 @@ class BLEEvent {
152
365
  // Heap-allocate param and data
153
366
  // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
154
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
155
372
  this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
156
373
 
157
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.
158
377
  switch (e) {
159
378
  case ESP_GATTS_WRITE_EVT:
160
379
  this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
@@ -165,66 +384,14 @@ class BLEEvent {
165
384
  break;
166
385
  }
167
386
  }
387
+ };
168
388
 
169
- // Destructor to clean up heap allocations
170
- ~BLEEvent() {
171
- switch (this->type_) {
172
- case GATTC:
173
- delete this->event_.gattc.gattc_param;
174
- delete this->event_.gattc.data;
175
- break;
176
- case GATTS:
177
- delete this->event_.gatts.gatts_param;
178
- delete this->event_.gatts.data;
179
- break;
180
- default:
181
- break;
182
- }
183
- }
184
-
185
- // Disable copy to prevent double-delete
186
- BLEEvent(const BLEEvent &) = delete;
187
- BLEEvent &operator=(const BLEEvent &) = delete;
188
-
189
- union {
190
- // NOLINTNEXTLINE(readability-identifier-naming)
191
- struct gap_event {
192
- esp_gap_ble_cb_event_t gap_event;
193
- union {
194
- BLEScanResult scan_result; // 73 bytes
195
- // This matches ESP-IDF's scan complete event structures
196
- // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout
197
- struct {
198
- esp_bt_status_t status;
199
- } scan_complete; // 1 byte
200
- };
201
- } gap; // 80 bytes total
202
-
203
- // NOLINTNEXTLINE(readability-identifier-naming)
204
- struct gattc_event {
205
- esp_gattc_cb_event_t gattc_event;
206
- esp_gatt_if_t gattc_if;
207
- esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated
208
- std::vector<uint8_t> *data; // Heap-allocated
209
- } gattc; // 16 bytes (pointers only)
210
-
211
- // NOLINTNEXTLINE(readability-identifier-naming)
212
- struct gatts_event {
213
- esp_gatts_cb_event_t gatts_event;
214
- esp_gatt_if_t gatts_if;
215
- esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated
216
- std::vector<uint8_t> *data; // Heap-allocated
217
- } gatts; // 16 bytes (pointers only)
218
- } event_; // 80 bytes
219
-
220
- ble_event_t type_;
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)
391
+ static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
221
392
 
222
- // Helper methods to access event data
223
- ble_event_t type() const { return type_; }
224
- esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
225
- const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
226
- esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
227
- };
393
+ // Verify esp_ble_sec_t fits within our union
394
+ static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");
228
395
 
229
396
  // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
230
397