esphome 2025.6.0b1__py3-none-any.whl → 2025.6.0b2__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 (71) hide show
  1. esphome/components/api/api_connection.cpp +53 -33
  2. esphome/components/api/api_connection.h +24 -25
  3. esphome/components/api/api_pb2.cpp +1 -0
  4. esphome/components/api/api_pb2.h +65 -226
  5. esphome/components/api/client.py +1 -3
  6. esphome/components/api/proto.h +1 -1
  7. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +2 -2
  8. esphome/components/bluetooth_proxy/bluetooth_proxy.h +1 -1
  9. esphome/components/bme280_base/bme280_base.cpp +2 -3
  10. esphome/components/datetime/date_entity.cpp +5 -5
  11. esphome/components/datetime/datetime_base.h +0 -5
  12. esphome/components/datetime/datetime_entity.cpp +8 -8
  13. esphome/components/datetime/time_entity.cpp +4 -4
  14. esphome/components/esp32/__init__.py +30 -3
  15. esphome/components/esp32_ble/ble.cpp +90 -45
  16. esphome/components/esp32_ble/ble.h +24 -5
  17. esphome/components/esp32_ble/ble_event.h +172 -32
  18. esphome/components/esp32_ble/ble_scan_result.h +24 -0
  19. esphome/components/esp32_ble/queue.h +53 -27
  20. esphome/components/esp32_ble_tracker/__init__.py +1 -0
  21. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +95 -66
  22. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +16 -16
  23. esphome/components/esp32_camera/esp32_camera.cpp +1 -1
  24. esphome/components/fan/fan.cpp +26 -17
  25. esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +12 -9
  26. esphome/components/kmeteriso/kmeteriso.cpp +2 -3
  27. esphome/components/logger/logger.cpp +2 -15
  28. esphome/components/logger/logger.h +1 -2
  29. esphome/components/mqtt/mqtt_component.cpp +1 -1
  30. esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +1 -1
  31. esphome/components/nextion/nextion_upload_arduino.cpp +12 -9
  32. esphome/components/nextion/nextion_upload_idf.cpp +11 -9
  33. esphome/components/nextion/sensor/nextion_sensor.cpp +1 -1
  34. esphome/components/nextion/text_sensor/nextion_textsensor.cpp +1 -1
  35. esphome/components/number/number.cpp +1 -1
  36. esphome/components/number/number.h +0 -4
  37. esphome/components/prometheus/__init__.py +0 -1
  38. esphome/components/select/select.cpp +1 -1
  39. esphome/components/select/select.h +0 -4
  40. esphome/components/sensor/sensor.cpp +8 -4
  41. esphome/components/sensor/sensor.h +3 -6
  42. esphome/components/status_led/light/status_led_light.cpp +2 -2
  43. esphome/components/status_led/light/status_led_light.h +1 -1
  44. esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +14 -9
  45. esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +1 -0
  46. esphome/components/text/text.cpp +1 -1
  47. esphome/components/text/text.h +0 -4
  48. esphome/components/text_sensor/text_sensor.cpp +8 -4
  49. esphome/components/text_sensor/text_sensor.h +6 -6
  50. esphome/components/update/update_entity.cpp +1 -1
  51. esphome/components/update/update_entity.h +0 -3
  52. esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +1 -1
  53. esphome/components/web_server_idf/__init__.py +0 -2
  54. esphome/components/web_server_idf/web_server_idf.cpp +6 -2
  55. esphome/components/web_server_idf/web_server_idf.h +7 -0
  56. esphome/components/weikai/weikai.cpp +1 -1
  57. esphome/const.py +1 -1
  58. esphome/core/application.cpp +14 -8
  59. esphome/core/application.h +7 -7
  60. esphome/core/component.cpp +36 -15
  61. esphome/core/component.h +30 -13
  62. esphome/core/entity_base.cpp +4 -16
  63. esphome/core/entity_base.h +27 -13
  64. esphome/core/helpers.h +1 -1
  65. esphome/wizard.py +0 -16
  66. {esphome-2025.6.0b1.dist-info → esphome-2025.6.0b2.dist-info}/METADATA +2 -2
  67. {esphome-2025.6.0b1.dist-info → esphome-2025.6.0b2.dist-info}/RECORD +71 -70
  68. {esphome-2025.6.0b1.dist-info → esphome-2025.6.0b2.dist-info}/WHEEL +0 -0
  69. {esphome-2025.6.0b1.dist-info → esphome-2025.6.0b2.dist-info}/entry_points.txt +0 -0
  70. {esphome-2025.6.0b1.dist-info → esphome-2025.6.0b2.dist-info}/licenses/LICENSE +0 -0
  71. {esphome-2025.6.0b1.dist-info → esphome-2025.6.0b2.dist-info}/top_level.txt +0 -0
@@ -2,92 +2,232 @@
2
2
 
3
3
  #ifdef USE_ESP32
4
4
 
5
+ #include <cstddef> // for offsetof
5
6
  #include <vector>
6
7
 
7
8
  #include <esp_gap_ble_api.h>
8
9
  #include <esp_gattc_api.h>
9
10
  #include <esp_gatts_api.h>
10
11
 
12
+ #include "ble_scan_result.h"
13
+
11
14
  namespace esphome {
12
15
  namespace 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
19
+ static_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");
21
+ static_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");
23
+ static_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)
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),
29
+ "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),
32
+ "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),
35
+ "status must be first member of scan_stop_cmpl");
36
+
13
37
  // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
14
- // This class stores each event in a single type.
38
+ // This class stores each event with minimal memory usage.
39
+ // GAP events (99% of traffic) don't have the vector overhead.
40
+ // GATTC/GATTS events use heap allocation for their param and data.
41
+ //
42
+ // Event flow:
43
+ // 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
44
+ // 2. The handlers create a BLEEvent instance, copying only the data we need
45
+ // 3. The event is pushed to a thread-safe queue
46
+ // 4. In the main loop(), events are popped from the queue and processed
47
+ // 5. The event destructor cleans up any external allocations
48
+ //
49
+ // Thread safety:
50
+ // - GAP events: We copy only the fields we need directly into the union
51
+ // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
52
+ // the data remains valid even after the BLE callback returns. The original
53
+ // param pointer from ESP-IDF is only valid during the callback.
15
54
  class BLEEvent {
16
55
  public:
56
+ // NOLINTNEXTLINE(readability-identifier-naming)
57
+ enum ble_event_t : uint8_t {
58
+ GAP,
59
+ GATTC,
60
+ GATTS,
61
+ };
62
+
63
+ // Constructor for GAP events - no external allocations needed
17
64
  BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
18
- this->event_.gap.gap_event = e;
19
- memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
20
65
  this->type_ = GAP;
21
- };
66
+ this->event_.gap.gap_event = e;
67
+
68
+ if (p == nullptr) {
69
+ return; // Invalid event, but we can't log in header file
70
+ }
71
+
72
+ // Only copy the data we actually use for each GAP event type
73
+ switch (e) {
74
+ case ESP_GAP_BLE_SCAN_RESULT_EVT:
75
+ // Copy only the fields we use from scan results
76
+ memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
77
+ this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
78
+ this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
79
+ this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
80
+ this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
81
+ this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
82
+ memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
83
+ ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
84
+ break;
85
+
86
+ case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
87
+ this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
88
+ break;
89
+
90
+ case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
91
+ this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
92
+ break;
93
+
94
+ case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
95
+ this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
96
+ break;
97
+
98
+ default:
99
+ // We only handle 4 GAP event types, others are dropped
100
+ break;
101
+ }
102
+ }
22
103
 
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
23
106
  BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
107
+ this->type_ = GATTC;
24
108
  this->event_.gattc.gattc_event = e;
25
109
  this->event_.gattc.gattc_if = i;
26
- memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
27
- // Need to also make a copy of relevant event data.
110
+
111
+ if (p == nullptr) {
112
+ this->event_.gattc.gattc_param = nullptr;
113
+ this->event_.gattc.data = nullptr;
114
+ return; // Invalid event, but we can't log in header file
115
+ }
116
+
117
+ // Heap-allocate param and data
118
+ // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
119
+ // while GAP events (99%) are stored inline to minimize memory usage
120
+ this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
121
+
122
+ // Copy data for events that need it
28
123
  switch (e) {
29
124
  case ESP_GATTC_NOTIFY_EVT:
30
- this->data.assign(p->notify.value, p->notify.value + p->notify.value_len);
31
- this->event_.gattc.gattc_param.notify.value = this->data.data();
125
+ this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
126
+ this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
32
127
  break;
33
128
  case ESP_GATTC_READ_CHAR_EVT:
34
129
  case ESP_GATTC_READ_DESCR_EVT:
35
- this->data.assign(p->read.value, p->read.value + p->read.value_len);
36
- this->event_.gattc.gattc_param.read.value = this->data.data();
130
+ this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
131
+ this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
37
132
  break;
38
133
  default:
134
+ this->event_.gattc.data = nullptr;
39
135
  break;
40
136
  }
41
- this->type_ = GATTC;
42
- };
137
+ }
43
138
 
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
44
141
  BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
142
+ this->type_ = GATTS;
45
143
  this->event_.gatts.gatts_event = e;
46
144
  this->event_.gatts.gatts_if = i;
47
- memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
48
- // Need to also make a copy of relevant event data.
145
+
146
+ if (p == nullptr) {
147
+ this->event_.gatts.gatts_param = nullptr;
148
+ this->event_.gatts.data = nullptr;
149
+ return; // Invalid event, but we can't log in header file
150
+ }
151
+
152
+ // Heap-allocate param and data
153
+ // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
154
+ // while GAP events (99%) are stored inline to minimize memory usage
155
+ this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
156
+
157
+ // Copy data for events that need it
49
158
  switch (e) {
50
159
  case ESP_GATTS_WRITE_EVT:
51
- this->data.assign(p->write.value, p->write.value + p->write.len);
52
- this->event_.gatts.gatts_param.write.value = this->data.data();
160
+ this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
161
+ this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
53
162
  break;
54
163
  default:
164
+ this->event_.gatts.data = nullptr;
55
165
  break;
56
166
  }
57
- this->type_ = GATTS;
58
- };
167
+ }
168
+
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;
59
188
 
60
189
  union {
61
190
  // NOLINTNEXTLINE(readability-identifier-naming)
62
191
  struct gap_event {
63
192
  esp_gap_ble_cb_event_t gap_event;
64
- esp_ble_gap_cb_param_t gap_param;
65
- } gap;
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
66
202
 
67
203
  // NOLINTNEXTLINE(readability-identifier-naming)
68
204
  struct gattc_event {
69
205
  esp_gattc_cb_event_t gattc_event;
70
206
  esp_gatt_if_t gattc_if;
71
- esp_ble_gattc_cb_param_t gattc_param;
72
- } gattc;
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)
73
210
 
74
211
  // NOLINTNEXTLINE(readability-identifier-naming)
75
212
  struct gatts_event {
76
213
  esp_gatts_cb_event_t gatts_event;
77
214
  esp_gatt_if_t gatts_if;
78
- esp_ble_gatts_cb_param_t gatts_param;
79
- } gatts;
80
- } event_;
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
81
219
 
82
- std::vector<uint8_t> data{};
83
- // NOLINTNEXTLINE(readability-identifier-naming)
84
- enum ble_event_t : uint8_t {
85
- GAP,
86
- GATTC,
87
- GATTS,
88
- } type_;
220
+ ble_event_t type_;
221
+
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; }
89
227
  };
90
228
 
229
+ // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
230
+
91
231
  } // namespace esp32_ble
92
232
  } // namespace esphome
93
233
 
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ #ifdef USE_ESP32
4
+
5
+ #include <esp_gap_ble_api.h>
6
+
7
+ namespace esphome {
8
+ namespace esp32_ble {
9
+
10
+ // Structure for BLE scan results - only fields we actually use
11
+ struct __attribute__((packed)) BLEScanResult {
12
+ esp_bd_addr_t bda;
13
+ uint8_t ble_addr_type;
14
+ int8_t rssi;
15
+ uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX];
16
+ uint8_t adv_data_len;
17
+ uint8_t scan_rsp_len;
18
+ uint8_t search_evt;
19
+ }; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t
20
+
21
+ } // namespace esp32_ble
22
+ } // namespace esphome
23
+
24
+ #endif
@@ -2,52 +2,78 @@
2
2
 
3
3
  #ifdef USE_ESP32
4
4
 
5
- #include <mutex>
6
- #include <queue>
7
-
8
- #include <freertos/FreeRTOS.h>
9
- #include <freertos/semphr.h>
5
+ #include <atomic>
6
+ #include <cstddef>
10
7
 
11
8
  /*
12
9
  * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
13
- * than trying to deal with various locking strategies, all incoming GAP and GATT
14
- * events will simply be placed on a semaphore guarded queue. The next time the
15
- * component runs loop(), these events are popped off the queue and handed at
16
- * this safer time.
10
+ * than using mutex-based locking, this lock-free queue allows the BLE
11
+ * task to enqueue events without blocking. The main loop() then processes
12
+ * these events at a safer time.
13
+ *
14
+ * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
15
+ * The BLE task is the only producer, and the main loop() is the only consumer.
17
16
  */
18
17
 
19
18
  namespace esphome {
20
19
  namespace esp32_ble {
21
20
 
22
- template<class T> class Queue {
21
+ template<class T, size_t SIZE> class LockFreeQueue {
23
22
  public:
24
- Queue() { m_ = xSemaphoreCreateMutex(); }
23
+ LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
25
24
 
26
- void push(T *element) {
25
+ bool push(T *element) {
27
26
  if (element == nullptr)
28
- return;
29
- // It is not called from main loop. Thus it won't block main thread.
30
- xSemaphoreTake(m_, portMAX_DELAY);
31
- q_.push(element);
32
- xSemaphoreGive(m_);
27
+ return false;
28
+
29
+ size_t current_tail = tail_.load(std::memory_order_relaxed);
30
+ size_t next_tail = (current_tail + 1) % SIZE;
31
+
32
+ if (next_tail == head_.load(std::memory_order_acquire)) {
33
+ // Buffer full
34
+ dropped_count_.fetch_add(1, std::memory_order_relaxed);
35
+ return false;
36
+ }
37
+
38
+ buffer_[current_tail] = element;
39
+ tail_.store(next_tail, std::memory_order_release);
40
+ return true;
33
41
  }
34
42
 
35
43
  T *pop() {
36
- T *element = nullptr;
37
-
38
- if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
39
- if (!q_.empty()) {
40
- element = q_.front();
41
- q_.pop();
42
- }
43
- xSemaphoreGive(m_);
44
+ size_t current_head = head_.load(std::memory_order_relaxed);
45
+
46
+ if (current_head == tail_.load(std::memory_order_acquire)) {
47
+ return nullptr; // Empty
44
48
  }
49
+
50
+ T *element = buffer_[current_head];
51
+ head_.store((current_head + 1) % SIZE, std::memory_order_release);
45
52
  return element;
46
53
  }
47
54
 
55
+ size_t size() const {
56
+ size_t tail = tail_.load(std::memory_order_acquire);
57
+ size_t head = head_.load(std::memory_order_acquire);
58
+ return (tail - head + SIZE) % SIZE;
59
+ }
60
+
61
+ size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
62
+
63
+ void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
64
+
65
+ bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
66
+
67
+ bool full() const {
68
+ size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
69
+ return next_tail == head_.load(std::memory_order_acquire);
70
+ }
71
+
48
72
  protected:
49
- std::queue<T *> q_;
50
- SemaphoreHandle_t m_;
73
+ T *buffer_[SIZE];
74
+ std::atomic<size_t> head_;
75
+ std::atomic<size_t> tail_;
76
+ std::atomic<size_t> dropped_count_;
51
77
  };
52
78
 
53
79
  } // namespace esp32_ble
@@ -268,6 +268,7 @@ async def to_code(config):
268
268
 
269
269
  parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
270
270
  cg.add(parent.register_gap_event_handler(var))
271
+ cg.add(parent.register_gap_scan_event_handler(var))
271
272
  cg.add(parent.register_gattc_event_handler(var))
272
273
  cg.add(parent.register_ble_status_event_handler(var))
273
274
  cg.add(var.set_parent(parent))
@@ -50,17 +50,15 @@ void ESP32BLETracker::setup() {
50
50
  ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
51
51
  return;
52
52
  }
53
- ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param> allocator(
54
- ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param>::ALLOW_FAILURE);
55
- this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE);
53
+ RAMAllocator<BLEScanResult> allocator;
54
+ this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
56
55
 
57
- if (this->scan_result_buffer_ == nullptr) {
58
- ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!");
56
+ if (this->scan_ring_buffer_ == nullptr) {
57
+ ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
59
58
  this->mark_failed();
60
59
  }
61
60
 
62
61
  global_esp32_ble_tracker = this;
63
- this->scan_result_lock_ = xSemaphoreCreateMutex();
64
62
 
65
63
  #ifdef USE_OTA
66
64
  ota::get_global_ota_callback()->add_on_state_callback(
@@ -120,27 +118,31 @@ void ESP32BLETracker::loop() {
120
118
  }
121
119
  bool promote_to_connecting = discovered && !searching && !connecting;
122
120
 
123
- if (this->scanner_state_ == ScannerState::RUNNING &&
124
- this->scan_result_index_ && // if it looks like we have a scan result we will take the lock
125
- xSemaphoreTake(this->scan_result_lock_, 0)) {
126
- uint32_t index = this->scan_result_index_;
127
- if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
128
- ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
129
- }
121
+ // Process scan results from lock-free SPSC ring buffer
122
+ // Consumer side: This runs in the main loop thread
123
+ if (this->scanner_state_ == ScannerState::RUNNING) {
124
+ // Load our own index with relaxed ordering (we're the only writer)
125
+ size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
130
126
 
131
- if (this->raw_advertisements_) {
132
- for (auto *listener : this->listeners_) {
133
- listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
134
- }
135
- for (auto *client : this->clients_) {
136
- client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
127
+ // Load producer's index with acquire to see their latest writes
128
+ size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
129
+
130
+ while (read_idx != write_idx) {
131
+ // Process one result at a time directly from ring buffer
132
+ BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx];
133
+
134
+ if (this->raw_advertisements_) {
135
+ for (auto *listener : this->listeners_) {
136
+ listener->parse_devices(&scan_result, 1);
137
+ }
138
+ for (auto *client : this->clients_) {
139
+ client->parse_devices(&scan_result, 1);
140
+ }
137
141
  }
138
- }
139
142
 
140
- if (this->parse_advertisements_) {
141
- for (size_t i = 0; i < index; i++) {
143
+ if (this->parse_advertisements_) {
142
144
  ESPBTDevice device;
143
- device.parse_scan_rst(this->scan_result_buffer_[i]);
145
+ device.parse_scan_rst(scan_result);
144
146
 
145
147
  bool found = false;
146
148
  for (auto *listener : this->listeners_) {
@@ -161,9 +163,19 @@ void ESP32BLETracker::loop() {
161
163
  this->print_bt_device_info(device);
162
164
  }
163
165
  }
166
+
167
+ // Move to next entry in ring buffer
168
+ read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
169
+
170
+ // Store with release to ensure reads complete before index update
171
+ this->ring_read_index_.store(read_idx, std::memory_order_release);
172
+ }
173
+
174
+ // Log dropped results periodically
175
+ size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
176
+ if (dropped > 0) {
177
+ ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
164
178
  }
165
- this->scan_result_index_ = 0;
166
- xSemaphoreGive(this->scan_result_lock_);
167
179
  }
168
180
  if (this->scanner_state_ == ScannerState::STOPPED) {
169
181
  this->end_of_scan_(); // Change state to IDLE
@@ -370,9 +382,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
370
382
 
371
383
  void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
372
384
  switch (event) {
373
- case ESP_GAP_BLE_SCAN_RESULT_EVT:
374
- this->gap_scan_result_(param->scan_rst);
375
- break;
376
385
  case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
377
386
  this->gap_scan_set_param_complete_(param->scan_param_cmpl);
378
387
  break;
@@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
385
394
  default:
386
395
  break;
387
396
  }
397
+ // Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
388
398
  for (auto *client : this->clients_) {
389
399
  client->gap_event_handler(event, param);
390
400
  }
391
401
  }
392
402
 
403
+ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
404
+ ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
405
+
406
+ if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
407
+ // Lock-free SPSC ring buffer write (Producer side)
408
+ // This runs in the ESP-IDF Bluetooth stack callback thread
409
+ // IMPORTANT: Only this thread writes to ring_write_index_
410
+
411
+ // Load our own index with relaxed ordering (we're the only writer)
412
+ size_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
413
+ size_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
414
+
415
+ // Load consumer's index with acquire to see their latest updates
416
+ size_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
417
+
418
+ // Check if buffer is full
419
+ if (next_write_idx != read_idx) {
420
+ // Write to ring buffer
421
+ this->scan_ring_buffer_[write_idx] = scan_result;
422
+
423
+ // Store with release to ensure the write is visible before index update
424
+ this->ring_write_index_.store(next_write_idx, std::memory_order_release);
425
+ } else {
426
+ // Buffer full, track dropped results
427
+ this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
428
+ }
429
+ } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
430
+ // Scan finished on its own
431
+ if (this->scanner_state_ != ScannerState::RUNNING) {
432
+ if (this->scanner_state_ == ScannerState::STOPPING) {
433
+ ESP_LOGE(TAG, "Scan was not running when scan completed.");
434
+ } else if (this->scanner_state_ == ScannerState::STARTING) {
435
+ ESP_LOGE(TAG, "Scan was not started when scan completed.");
436
+ } else if (this->scanner_state_ == ScannerState::FAILED) {
437
+ ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
438
+ } else if (this->scanner_state_ == ScannerState::IDLE) {
439
+ ESP_LOGE(TAG, "Scan was idle when scan completed.");
440
+ } else if (this->scanner_state_ == ScannerState::STOPPED) {
441
+ ESP_LOGE(TAG, "Scan was stopped when scan completed.");
442
+ }
443
+ }
444
+ this->set_scanner_state_(ScannerState::STOPPED);
445
+ }
446
+ }
447
+
393
448
  void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
394
449
  ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
395
450
  if (param.status == ESP_BT_STATUS_DONE) {
@@ -444,34 +499,6 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
444
499
  this->set_scanner_state_(ScannerState::STOPPED);
445
500
  }
446
501
 
447
- void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
448
- ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
449
- if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
450
- if (xSemaphoreTake(this->scan_result_lock_, 0)) {
451
- if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
452
- this->scan_result_buffer_[this->scan_result_index_++] = param;
453
- }
454
- xSemaphoreGive(this->scan_result_lock_);
455
- }
456
- } else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
457
- // Scan finished on its own
458
- if (this->scanner_state_ != ScannerState::RUNNING) {
459
- if (this->scanner_state_ == ScannerState::STOPPING) {
460
- ESP_LOGE(TAG, "Scan was not running when scan completed.");
461
- } else if (this->scanner_state_ == ScannerState::STARTING) {
462
- ESP_LOGE(TAG, "Scan was not started when scan completed.");
463
- } else if (this->scanner_state_ == ScannerState::FAILED) {
464
- ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
465
- } else if (this->scanner_state_ == ScannerState::IDLE) {
466
- ESP_LOGE(TAG, "Scan was idle when scan completed.");
467
- } else if (this->scanner_state_ == ScannerState::STOPPED) {
468
- ESP_LOGE(TAG, "Scan was stopped when scan completed.");
469
- }
470
- }
471
- this->set_scanner_state_(ScannerState::STOPPED);
472
- }
473
- }
474
-
475
502
  void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
476
503
  esp_ble_gattc_cb_param_t *param) {
477
504
  for (auto *client : this->clients_) {
@@ -494,13 +521,15 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData
494
521
  return ESPBLEiBeacon(data.data.data());
495
522
  }
496
523
 
497
- void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
498
- this->scan_result_ = param;
524
+ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
499
525
  for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
500
- this->address_[i] = param.bda[i];
501
- this->address_type_ = param.ble_addr_type;
502
- this->rssi_ = param.rssi;
503
- this->parse_adv_(param);
526
+ this->address_[i] = scan_result.bda[i];
527
+ this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
528
+ this->rssi_ = scan_result.rssi;
529
+
530
+ // Parse advertisement data directly
531
+ uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len;
532
+ this->parse_adv_(scan_result.ble_adv, total_len);
504
533
 
505
534
  #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
506
535
  ESP_LOGVV(TAG, "Parse Result:");
@@ -558,13 +587,13 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
558
587
  ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
559
588
  }
560
589
 
561
- ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str());
590
+ ESP_LOGVV(TAG, " Adv data: %s",
591
+ format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str());
562
592
  #endif
563
593
  }
564
- void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
594
+
595
+ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
565
596
  size_t offset = 0;
566
- const uint8_t *payload = param.ble_adv;
567
- uint8_t len = param.adv_data_len + param.scan_rsp_len;
568
597
 
569
598
  while (offset + 2 < len) {
570
599
  const uint8_t field_length = payload[offset++]; // First byte is length of adv record