1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-14 17:22:20 +01:00
This commit is contained in:
J. Nick Koston
2025-06-16 14:43:29 +02:00
parent 68ef9cb3dc
commit bc49211dab
5 changed files with 421 additions and 120 deletions

View File

@@ -1,6 +1,8 @@
#ifdef USE_ESP32
#include "ble.h"
#include "ble_event_pool.h"
#include "queue_index.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -23,8 +25,7 @@ namespace esp32_ble {
static const char *const TAG = "esp32_ble";
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
// No longer need static allocator - using pre-allocated pool instead
void ESP32BLE::setup() {
global_ble = this;
@@ -301,8 +302,16 @@ void ESP32BLE::loop() {
break;
}
BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) {
size_t event_idx = this->ble_events_.pop();
while (event_idx != LockFreeIndexQueue<MAX_BLE_QUEUE_SIZE>::INVALID_INDEX) {
BLEEvent *ble_event = this->ble_event_pool_.get(event_idx);
if (ble_event == nullptr) {
// This should not happen - log error and continue
ESP_LOGE(TAG, "Invalid event index: %zu", event_idx);
event_idx = this->ble_events_.pop();
continue;
}
switch (ble_event->type_) {
case BLEEvent::GATTS: {
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
@@ -349,10 +358,9 @@ void ESP32BLE::loop() {
default:
break;
}
// Destructor will clean up external allocations for GATTC/GATTS
ble_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(ble_event, 1);
ble_event = this->ble_events_.pop();
// Return the event to the pool
this->ble_event_pool_.deallocate(event_idx);
event_idx = this->ble_events_.pop();
}
if (this->advertising_ != nullptr) {
this->advertising_->loop();
@@ -363,6 +371,31 @@ void ESP32BLE::loop() {
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped);
}
// Log pool usage periodically (every ~10 seconds)
static uint32_t last_pool_log = 0;
uint32_t now = millis();
if (now - last_pool_log > 10000) {
size_t created = this->ble_event_pool_.get_total_created();
if (created > 0) {
ESP_LOGD(TAG, "BLE event pool: %zu events created (peak usage), %zu currently allocated", created,
this->ble_event_pool_.get_allocated_count());
}
last_pool_log = now;
}
}
// Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p);
}
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
event->load_gattc_event(e, i, p);
}
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
event->load_gatts_event(e, i, p);
}
template<typename... Args> void enqueue_ble_event(Args... args) {
@@ -373,23 +406,35 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
return;
}
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
// Allocate an event from the pool
size_t event_idx = global_ble->ble_event_pool_.allocate();
if (event_idx == BLEEventPool<MAX_BLE_QUEUE_SIZE>::INVALID_INDEX) {
// Pool is full, drop the event
global_ble->ble_events_.increment_dropped_count();
return;
}
new (new_event) BLEEvent(args...);
// Push the event - since we're the only producer and we checked full() above,
// this should always succeed unless we have a bug
if (!global_ble->ble_events_.push(new_event)) {
// Get the event object
BLEEvent *event = global_ble->ble_event_pool_.get(event_idx);
if (event == nullptr) {
// This should not happen
ESP_LOGE(TAG, "Failed to get event from pool at index %zu", event_idx);
global_ble->ble_event_pool_.deallocate(event_idx);
global_ble->ble_events_.increment_dropped_count();
return;
}
// Load new event data (replaces previous event)
load_ble_event(event, args...);
// Push the event index to the queue
if (!global_ble->ble_events_.push(event_idx)) {
// This should not happen in SPSC queue with single producer
ESP_LOGE(TAG, "BLE queue push failed unexpectedly");
new_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(new_event, 1);
// Return to pool
global_ble->ble_event_pool_.deallocate(event_idx);
}
} // NOLINT(clang-analyzer-unix.Malloc)
}
// Explicit template instantiations for the friend function
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);

View File

@@ -12,7 +12,9 @@
#include "esphome/core/helpers.h"
#include "ble_event.h"
#include "ble_event_pool.h"
#include "queue.h"
#include "queue_index.h"
#ifdef USE_ESP32
@@ -147,7 +149,8 @@ class ESP32BLE : public Component {
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
LockFreeIndexQueue<MAX_BLE_QUEUE_SIZE> ble_events_;
BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_{};

View File

@@ -63,123 +63,66 @@ class BLEEvent {
// Constructor for GAP events - no external allocations needed
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->type_ = GAP;
this->event_.gap.gap_event = e;
if (p == nullptr) {
return; // Invalid event, but we can't log in header file
}
// Only copy the data we actually use for each GAP event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
// Copy only the fields we use from scan results
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
break;
default:
// We only handle 4 GAP event types, others are dropped
break;
}
this->init_gap_data(e, p);
}
// Constructor for GATTC events - uses heap allocation
// Creates a copy of the param struct since the original is only valid during the callback
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC;
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.data = nullptr;
break;
}
this->init_gattc_data(e, i, p);
}
// Constructor for GATTS events - uses heap allocation
// Creates a copy of the param struct since the original is only valid during the callback
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS;
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.data = nullptr;
break;
}
this->init_gatts_data(e, i, p);
}
// Destructor to clean up heap allocations
~BLEEvent() {
switch (this->type_) {
case GATTC:
delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data;
break;
case GATTS:
delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data;
break;
default:
break;
~BLEEvent() { this->cleanup_heap_data(); }
// Default constructor for pre-allocation in pool
BLEEvent() : type_(GAP) {}
// Clean up any heap-allocated data
void cleanup_heap_data() {
if (this->type_ == GAP) {
return;
}
if (this->type_ == GATTC) {
delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data;
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return;
}
if (this->type_ == GATTS) {
delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data;
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
}
}
// Load new event data for reuse (replaces previous event data)
void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GAP;
this->init_gap_data(e, p);
}
void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTC;
this->init_gattc_data(e, i, p);
}
void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTS;
this->init_gatts_data(e, i, p);
}
// Disable copy to prevent double-delete
@@ -224,6 +167,102 @@ class BLEEvent {
esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
private:
// Initialize GAP event data
void init_gap_data(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e;
if (p == nullptr) {
return;
}
// Copy data based on event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
break;
default:
break;
}
}
// Initialize GATTC event data
void init_gattc_data(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return;
}
// Heap-allocate param
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.data = nullptr;
break;
}
}
// Initialize GATTS event data
void init_gatts_data(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
return;
}
// Heap-allocate param
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.data = nullptr;
break;
}
}
};
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)

View File

@@ -0,0 +1,133 @@
#pragma once
#ifdef USE_ESP32
#include <atomic>
#include <cstddef>
#include "ble_event.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace esp32_ble {
// BLE Event Pool - Pre-allocated pool of BLEEvent objects to avoid heap fragmentation
// This is a lock-free pool that allows the BLE task to allocate events without malloc
template<size_t SIZE> class BLEEventPool {
public:
BLEEventPool() {
// Initialize all slots as unallocated
for (size_t i = 0; i < SIZE; i++) {
this->events_[i] = nullptr;
}
// Initialize the free list - all indices are initially free
for (size_t i = 0; i < SIZE - 1; i++) {
this->next_free_[i] = i + 1;
}
this->next_free_[SIZE - 1] = INVALID_INDEX;
this->free_head_.store(0, std::memory_order_relaxed);
this->allocated_count_.store(0, std::memory_order_relaxed);
this->total_created_.store(0, std::memory_order_relaxed);
}
~BLEEventPool() {
// Delete any events that were created
for (size_t i = 0; i < SIZE; i++) {
if (this->events_[i] != nullptr) {
delete this->events_[i];
}
}
}
// Allocate an event slot and return its index
// Returns INVALID_INDEX if pool is full
size_t allocate() {
while (true) {
size_t head = this->free_head_.load(std::memory_order_acquire);
if (head == INVALID_INDEX) {
// Pool is full
return INVALID_INDEX;
}
size_t next = this->next_free_[head];
// Try to update the free list head
if (this->free_head_.compare_exchange_weak(head, next, std::memory_order_release, std::memory_order_acquire)) {
this->allocated_count_.fetch_add(1, std::memory_order_relaxed);
return head;
}
// CAS failed, retry
}
}
// Deallocate an event slot by index
void deallocate(size_t index) {
if (index >= SIZE) {
return; // Invalid index
}
// No destructor call - events are reused
// The event's reset methods handle cleanup when switching types
while (true) {
size_t head = this->free_head_.load(std::memory_order_acquire);
this->next_free_[index] = head;
// Try to add this index back to the free list
if (this->free_head_.compare_exchange_weak(head, index, std::memory_order_release, std::memory_order_acquire)) {
this->allocated_count_.fetch_sub(1, std::memory_order_relaxed);
return;
}
// CAS failed, retry
}
}
// Get event by index, creating it if needed
BLEEvent *get(size_t index) {
if (index >= SIZE) {
return nullptr;
}
// Create event on first access (warm-up)
if (this->events_[index] == nullptr) {
// Use internal RAM for better performance
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
BLEEvent *event = allocator.allocate(1);
if (event == nullptr) {
// Fall back to regular allocation
event = new BLEEvent();
} else {
// Placement new to construct the object
new (event) BLEEvent();
}
this->events_[index] = event;
this->total_created_.fetch_add(1, std::memory_order_relaxed);
}
return this->events_[index];
}
// Get number of allocated events
size_t get_allocated_count() const { return this->allocated_count_.load(std::memory_order_relaxed); }
// Get total number of events created (high water mark)
size_t get_total_created() const { return this->total_created_.load(std::memory_order_relaxed); }
static constexpr size_t INVALID_INDEX = SIZE_MAX;
private:
BLEEvent *events_[SIZE]; // Array of pointers, allocated on demand
size_t next_free_[SIZE]; // Next free index for each slot
std::atomic<size_t> free_head_; // Head of the free list
std::atomic<size_t> allocated_count_; // Number of currently allocated events
std::atomic<size_t> total_created_; // Total events created (high water mark)
};
} // namespace esp32_ble
} // namespace esphome
#endif

View File

@@ -0,0 +1,81 @@
#pragma once
#ifdef USE_ESP32
#include <atomic>
#include <cstddef>
namespace esphome {
namespace esp32_ble {
// Lock-free SPSC queue that stores indices instead of pointers
// This allows us to use a pre-allocated pool of objects
template<size_t SIZE> class LockFreeIndexQueue {
public:
static constexpr size_t INVALID_INDEX = SIZE_MAX;
LockFreeIndexQueue() : head_(0), tail_(0), dropped_count_(0) {
// Initialize all slots to invalid
for (size_t i = 0; i < SIZE; i++) {
buffer_[i] = INVALID_INDEX;
}
}
bool push(size_t index) {
if (index == INVALID_INDEX)
return false;
size_t current_tail = tail_.load(std::memory_order_relaxed);
size_t next_tail = (current_tail + 1) % SIZE;
if (next_tail == head_.load(std::memory_order_acquire)) {
// Buffer full
dropped_count_.fetch_add(1, std::memory_order_relaxed);
return false;
}
buffer_[current_tail] = index;
tail_.store(next_tail, std::memory_order_release);
return true;
}
size_t pop() {
size_t current_head = head_.load(std::memory_order_relaxed);
if (current_head == tail_.load(std::memory_order_acquire)) {
return INVALID_INDEX; // Empty
}
size_t index = buffer_[current_head];
head_.store((current_head + 1) % SIZE, std::memory_order_release);
return index;
}
size_t size() const {
size_t tail = tail_.load(std::memory_order_acquire);
size_t head = head_.load(std::memory_order_acquire);
return (tail - head + SIZE) % SIZE;
}
size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
bool full() const {
size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
return next_tail == head_.load(std::memory_order_acquire);
}
protected:
size_t buffer_[SIZE];
std::atomic<size_t> head_;
std::atomic<size_t> tail_;
std::atomic<size_t> dropped_count_;
};
} // namespace esp32_ble
} // namespace esphome
#endif