Merge branch 'extract_helpers' into integration

This commit is contained in:
J. Nick Koston
2025-06-27 12:50:37 -05:00
5 changed files with 59 additions and 30 deletions

View File

@@ -1,7 +1,6 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "ble.h" #include "ble.h"
#include "ble_event_pool.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"

View File

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

View File

@@ -139,6 +139,9 @@ class BLEEvent {
// Default constructor for pre-allocation in pool // Default constructor for pre-allocation in pool
BLEEvent() : type_(GAP) {} BLEEvent() : type_(GAP) {}
// Invoked on return to EventPool
void clear() { this->cleanup_heap_data(); }
// Clean up any heap-allocated data // Clean up any heap-allocated data
void cleanup_heap_data() { void cleanup_heap_data() {
if (this->type_ == GAP) { if (this->type_ == GAP) {

View File

@@ -4,22 +4,20 @@
#include <atomic> #include <atomic>
#include <cstddef> #include <cstddef>
#include "ble_event.h"
#include "queue.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/lock_free_queue.h"
namespace esphome { namespace esphome {
namespace esp32_ble {
// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation // Event Pool - On-demand pool of objects to avoid heap fragmentation
// Events are allocated on first use and reused thereafter, growing to peak usage // Events are allocated on first use and reused thereafter, growing to peak usage
template<uint8_t SIZE> class BLEEventPool { template<class T, uint8_t SIZE> class EventPool {
public: public:
BLEEventPool() : total_created_(0) {} EventPool() : total_created_(0) {}
~BLEEventPool() { ~EventPool() {
// Clean up any remaining events in the free list // Clean up any remaining events in the free list
BLEEvent *event; T *event;
while ((event = this->free_list_.pop()) != nullptr) { while ((event = this->free_list_.pop()) != nullptr) {
delete event; delete event;
} }
@@ -27,9 +25,9 @@ template<uint8_t SIZE> class BLEEventPool {
// Allocate an event from the pool // Allocate an event from the pool
// Returns nullptr if pool is full // Returns nullptr if pool is full
BLEEvent *allocate() { T *allocate() {
// Try to get from free list first // Try to get from free list first
BLEEvent *event = this->free_list_.pop(); T *event = this->free_list_.pop();
if (event != nullptr) if (event != nullptr)
return event; return event;
@@ -40,7 +38,7 @@ template<uint8_t SIZE> class BLEEventPool {
} }
// Use internal RAM for better performance // Use internal RAM for better performance
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL); RAMAllocator<T> allocator(RAMAllocator<T>::ALLOC_INTERNAL);
event = allocator.allocate(1); event = allocator.allocate(1);
if (event == nullptr) { if (event == nullptr) {
@@ -49,24 +47,25 @@ template<uint8_t SIZE> class BLEEventPool {
} }
// Placement new to construct the object // Placement new to construct the object
new (event) BLEEvent(); new (event) T();
this->total_created_++; this->total_created_++;
return event; return event;
} }
// Return an event to the pool for reuse // Return an event to the pool for reuse
void release(BLEEvent *event) { void release(T *event) {
if (event != nullptr) { if (event != nullptr) {
// Clean up the event's allocated memory
event->clear();
this->free_list_.push(event); this->free_list_.push(event);
} }
} }
private: private:
LockFreeQueue<BLEEvent, SIZE> free_list_; // Free events ready for reuse LockFreeQueue<T, SIZE> free_list_; // Free events ready for reuse
uint8_t total_created_; // Total events created (high water mark) uint8_t total_created_; // Total events created (high water mark)
}; };
} // namespace esp32_ble
} // namespace esphome } // namespace esphome
#endif #endif

View File

@@ -4,23 +4,25 @@
#include <atomic> #include <atomic>
#include <cstddef> #include <cstddef>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
/* /*
* BLE events come in from a separate Task (thread) in the ESP32 stack. Rather * Lock-free queue for single-producer single-consumer scenarios.
* than using mutex-based locking, this lock-free queue allows the BLE * This allows one thread to push items and another to pop them without
* task to enqueue events without blocking. The main loop() then processes * blocking each other.
* these events at a safer time.
* *
* This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
* The BLE task is the only producer, and the main loop() is the only consumer. * Common use cases:
* - BLE events: BLE task produces, main loop consumes
* - MQTT messages: main task produces, MQTT thread consumes
*/ */
namespace esphome { namespace esphome {
namespace esp32_ble {
template<class T, uint8_t SIZE> class LockFreeQueue { template<class T, uint8_t SIZE> class LockFreeQueue {
public: public:
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {}
bool push(T *element) { bool push(T *element) {
if (element == nullptr) if (element == nullptr)
@@ -29,14 +31,37 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
uint8_t current_tail = tail_.load(std::memory_order_relaxed); uint8_t current_tail = tail_.load(std::memory_order_relaxed);
uint8_t next_tail = (current_tail + 1) % SIZE; uint8_t next_tail = (current_tail + 1) % SIZE;
if (next_tail == head_.load(std::memory_order_acquire)) { // Read head before incrementing tail
uint8_t head_before = head_.load(std::memory_order_acquire);
if (next_tail == head_before) {
// Buffer full // Buffer full
dropped_count_.fetch_add(1, std::memory_order_relaxed); dropped_count_.fetch_add(1, std::memory_order_relaxed);
return false; return false;
} }
// Check if queue was empty before push
bool was_empty = (current_tail == head_before);
buffer_[current_tail] = element; buffer_[current_tail] = element;
tail_.store(next_tail, std::memory_order_release); tail_.store(next_tail, std::memory_order_release);
// Notify optimization: only notify if we need to
if (task_to_notify_ != nullptr) {
if (was_empty) {
// Queue was empty - consumer might be going to sleep, must notify
xTaskNotifyGive(task_to_notify_);
} else {
// Queue wasn't empty - check if consumer has caught up to previous tail
uint8_t head_after = head_.load(std::memory_order_acquire);
if (head_after == current_tail) {
// Consumer just caught up to where tail was - might go to sleep, must notify
xTaskNotifyGive(task_to_notify_);
}
// Otherwise: consumer is still behind, no need to notify
}
}
return true; return true;
} }
@@ -69,6 +94,8 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
return next_tail == head_.load(std::memory_order_acquire); return next_tail == head_.load(std::memory_order_acquire);
} }
void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
protected: protected:
T *buffer_[SIZE]; T *buffer_[SIZE];
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
@@ -77,9 +104,10 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
std::atomic<uint8_t> head_; std::atomic<uint8_t> head_;
// Atomic: written by producer (push), read by consumer (pop) to check if empty // Atomic: written by producer (push), read by consumer (pop) to check if empty
std::atomic<uint8_t> tail_; std::atomic<uint8_t> tail_;
// Task handle for notification (optional)
TaskHandle_t task_to_notify_;
}; };
} // namespace esp32_ble
} // namespace esphome } // namespace esphome
#endif #endif