diff --git a/Doxyfile b/Doxyfile index d1e950c548..a4dd65bf45 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.6.0 +PROJECT_NUMBER = 2025.6.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8adef79d2f..cf63ad34d7 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "ble.h" +#include "ble_event_pool.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -23,9 +24,6 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; -static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); - void ESP32BLE::setup() { global_ble = this; ESP_LOGCONFIG(TAG, "Running setup"); @@ -326,32 +324,77 @@ void ESP32BLE::loop() { } case BLEEvent::GAP: { esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; - if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) { - // Use the new scan event handler - no memcpy! - for (auto *scan_handler : this->gap_scan_event_handlers_) { - scan_handler->gap_scan_event_handler(ble_event->scan_result()); - } - } else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT || - gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT || - gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) { - // All three scan complete events have the same structure with just status - // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe - // This is verified at compile-time by static_assert checks in ble_event.h - // The struct already contains our copy of the status (copied in BLEEvent constructor) - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); - } + switch (gap_event) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + // Use the new scan event handler - no memcpy! + for (auto *scan_handler : this->gap_scan_event_handlers_) { + scan_handler->gap_scan_event_handler(ble_event->scan_result()); + } + break; + + // Scan complete events + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); + } + break; + + // Advertising complete events + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // All advertising complete events have the same structure with just status + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); + } + break; + + // RSSI complete event + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); + } + break; + + // Security events + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.security)); + } + break; + + default: + // Unknown/unhandled event + ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event); + break; } break; } default: break; } - // Destructor will clean up external allocations for GATTC/GATTS - ble_event->~BLEEvent(); - EVENT_ALLOCATOR.deallocate(ble_event, 1); + // Return the event to the pool + this->ble_event_pool_.release(ble_event); ble_event = this->ble_events_.pop(); } if (this->advertising_ != nullptr) { @@ -359,37 +402,41 @@ void ESP32BLE::loop() { } // Log dropped events periodically - size_t dropped = this->ble_events_.get_and_reset_dropped_count(); + uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); if (dropped > 0) { - ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped); + ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); } } +// 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 void enqueue_ble_event(Args... args) { - // Check if queue is full before allocating - if (global_ble->ble_events_.full()) { - // Queue is full, drop the event + // Allocate an event from the pool + BLEEvent *event = global_ble->ble_event_pool_.allocate(); + if (event == nullptr) { + // No events available - queue is full or we're out of memory global_ble->ble_events_.increment_dropped_count(); 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 - global_ble->ble_events_.increment_dropped_count(); - return; - } - new (new_event) BLEEvent(args...); + // Load new event data (replaces previous event) + load_ble_event(event, 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)) { - // 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); - } -} // NOLINT(clang-analyzer-unix.Malloc) + // Push the event to the queue + global_ble->ble_events_.push(event); + // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size +} // Explicit template instantiations for the friend function 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 void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { - // Only queue the 4 GAP events we actually handle + // Queue GAP events that components need to handle + // Scanning events - used by esp32_ble_tracker case ESP_GAP_BLE_SCAN_RESULT_EVT: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // Advertising events - used by esp32_ble_beacon and esp32_ble server + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // Connection events - used by ble_client + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + // Security events - used by ble_client and bluetooth_proxy + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: enqueue_ble_event(event, param); return; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 58c064a2ef..9fe996086e 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,6 +12,7 @@ #include "esphome/core/helpers.h" #include "ble_event.h" +#include "ble_event_pool.h" #include "queue.h" #ifdef USE_ESP32 @@ -148,6 +149,7 @@ class ESP32BLE : public Component { BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; LockFreeQueue ble_events_; + BLEEventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index f51095effd..dd3ec3da42 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -24,16 +24,45 @@ static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == si "ESP-IDF scan_stop_cmpl structure has unexpected size"); // Verify the status field is at offset 0 (first member) -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0, "status must be first member of scan_param_cmpl"); -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0, "status must be first member of scan_start_cmpl"); -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0, "status must be first member of scan_stop_cmpl"); +// Compile-time verification for advertising complete events +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_rsp_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_raw_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_start_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_stop_cmpl structure has unexpected size"); + +// Verify the status field is at offset 0 for advertising events +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0, + "status must be first member of adv_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0, + "status must be first member of scan_rsp_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0, + "status must be first member of adv_data_raw_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0, + "status must be first member of adv_start_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0, + "status must be first member of adv_stop_cmpl"); + +// Compile-time verification for RSSI complete event structure +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0, + "status must be first member of read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t), + "rssi must immediately follow status in read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), + "remote_addr must follow rssi in read_rssi_cmpl"); + // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // This class stores each event with minimal memory usage. // 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) == // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring // the data remains valid even after the BLE callback returns. The original // param pointer from ESP-IDF is only valid during the callback. +// +// CRITICAL DESIGN NOTE: +// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety. +// DO NOT attempt to optimize by removing these allocations or storing pointers +// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime +// than our event processing, and accessing it after the callback returns would +// result in use-after-free bugs and crashes. class BLEEvent { public: // NOLINTNEXTLINE(readability-identifier-naming) @@ -60,126 +96,86 @@ class BLEEvent { GATTS, }; + // Type definitions for cleaner method signatures + struct StatusOnlyData { + esp_bt_status_t status; + }; + + struct RSSICompleteData { + esp_bt_status_t status; + int8_t rssi; + esp_bd_addr_t remote_addr; + }; + // 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 + // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. + // The param pointer from ESP-IDF is only valid during the callback execution. + // Since BLE events are processed asynchronously in the main loop, we must create + // our own copy to ensure the data remains valid until the event is processed. 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(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(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 + // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. + // The param pointer from ESP-IDF is only valid during the callback execution. + // Since BLE events are processed asynchronously in the main loop, we must create + // our own copy to ensure the data remains valid until the event is processed. 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(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 @@ -191,12 +187,21 @@ class BLEEvent { struct gap_event { esp_gap_ble_cb_event_t gap_event; union { - BLEScanResult scan_result; // 73 bytes + BLEScanResult scan_result; // 73 bytes - Used by: esp32_ble_tracker // This matches ESP-IDF's scan complete event structures // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout - struct { - esp_bt_status_t status; - } scan_complete; // 1 byte + // Used by: esp32_ble_tracker + StatusOnlyData scan_complete; // 1 byte + // Advertising complete events all have same structure + // Used by: esp32_ble_beacon, esp32_ble server components + // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP + StatusOnlyData adv_complete; // 1 byte + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor component) + RSSICompleteData read_rssi_complete; // 8 bytes + // Security events - we store the full security union + // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client + esp_ble_sec_t security; // Variable size, but fits within scan_result size }; } gap; // 80 bytes total @@ -224,8 +229,170 @@ 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; } + esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; } + const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; } + const esp_ble_sec_t &security() const { return event_.gap.security; } + + 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; // Invalid event, but we can't log in header file + } + + // 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; + + // Advertising complete events - all have same structure with just status + // Used by: esp32_ble_beacon, esp32_ble server components + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->adv_data_cmpl.status; + break; + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status; + break; + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status; + break; + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_start_cmpl.status; + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status; + break; + + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor) + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status; + this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi; + memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t)); + break; + + // Security events - copy the entire security union + // Used by: ble_client, bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation + memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t)); + break; + + default: + // We only store data for GAP events that components currently use + // Unknown events still get queued and logged in ble.cpp:375 as + // "Unhandled GAP event type in loop" - this helps identify new events + // that components might need in the future + 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; // 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 + // IMPORTANT: This heap allocation provides clear ownership semantics: + // - The BLEEvent owns the allocated memory for its lifetime + // - The data remains valid from the BLE callback context until processed in the main loop + // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory + this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); + + // Copy data for events that need it + // The param struct contains pointers (e.g., notify.value) that point to temporary buffers. + // We must copy this data to ensure it remains valid when the event is processed later. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->event_.gattc.data = new std::vector(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(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; // 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 + // IMPORTANT: This heap allocation provides clear ownership semantics: + // - The BLEEvent owns the allocated memory for its lifetime + // - The data remains valid from the BLE callback context until processed in the main loop + // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory + this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); + + // Copy data for events that need it + // The param struct contains pointers (e.g., write.value) that point to temporary buffers. + // We must copy this data to ensure it remains valid when the event is processed later. + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->event_.gatts.data = new std::vector(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; + } + } }; +// Verify the gap_event struct hasn't grown beyond expected size +// The gap member in the union should be 80 bytes (including the gap_event enum) +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); + +// Verify esp_ble_sec_t fits within our union +static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); + // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h new file mode 100644 index 0000000000..ef123b1325 --- /dev/null +++ b/esphome/components/esp32_ble/ble_event_pool.h @@ -0,0 +1,72 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#include "ble_event.h" +#include "queue.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace esp32_ble { + +// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation +// Events are allocated on first use and reused thereafter, growing to peak usage +template class BLEEventPool { + public: + BLEEventPool() : total_created_(0) {} + + ~BLEEventPool() { + // Clean up any remaining events in the free list + BLEEvent *event; + while ((event = this->free_list_.pop()) != nullptr) { + delete event; + } + } + + // Allocate an event from the pool + // Returns nullptr if pool is full + BLEEvent *allocate() { + // Try to get from free list first + BLEEvent *event = this->free_list_.pop(); + if (event != nullptr) + return event; + + // Need to create a new event + if (this->total_created_ >= SIZE) { + // Pool is at capacity + return nullptr; + } + + // Use internal RAM for better performance + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + event = allocator.allocate(1); + + if (event == nullptr) { + // Memory allocation failed + return nullptr; + } + + // Placement new to construct the object + new (event) BLEEvent(); + this->total_created_++; + return event; + } + + // Return an event to the pool for reuse + void release(BLEEvent *event) { + if (event != nullptr) { + this->free_list_.push(event); + } + } + + private: + LockFreeQueue free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark) +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 56d2efd18b..75bf1eef25 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -18,7 +18,7 @@ namespace esphome { namespace esp32_ble { -template class LockFreeQueue { +template class LockFreeQueue { public: LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} @@ -26,8 +26,8 @@ template class LockFreeQueue { if (element == nullptr) return false; - size_t current_tail = tail_.load(std::memory_order_relaxed); - size_t next_tail = (current_tail + 1) % SIZE; + uint8_t current_tail = tail_.load(std::memory_order_relaxed); + uint8_t next_tail = (current_tail + 1) % SIZE; if (next_tail == head_.load(std::memory_order_acquire)) { // Buffer full @@ -41,7 +41,7 @@ template class LockFreeQueue { } T *pop() { - size_t current_head = head_.load(std::memory_order_relaxed); + uint8_t current_head = head_.load(std::memory_order_relaxed); if (current_head == tail_.load(std::memory_order_acquire)) { return nullptr; // Empty @@ -53,27 +53,30 @@ template class LockFreeQueue { } size_t size() const { - size_t tail = tail_.load(std::memory_order_acquire); - size_t head = head_.load(std::memory_order_acquire); + uint8_t tail = tail_.load(std::memory_order_acquire); + uint8_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); } + uint16_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; + uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; return next_tail == head_.load(std::memory_order_acquire); } protected: T *buffer_[SIZE]; - std::atomic head_; - std::atomic tail_; - std::atomic dropped_count_; + // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) + std::atomic dropped_count_; // 65535 max - more than enough for drop tracking + // Atomic: written by consumer (pop), read by producer (push) to check if full + std::atomic head_; + // Atomic: written by producer (push), read by consumer (pop) to check if empty + std::atomic tail_; }; } // namespace esp32_ble diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index c5906779f1..a1fb727dd0 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -522,6 +522,7 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData } void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { + this->scan_result_ = &scan_result; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) this->address_[i] = scan_result.bda[i]; this->address_type_ = static_cast(scan_result.ble_addr_type); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 16a100fb47..892f76f49c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -85,6 +85,9 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } + // Exposed through a function for use in lambdas + const BLEScanResult &get_scan_result() const { return *scan_result_; } + bool resolve_irk(const uint8_t *irk) const; optional get_ibeacon() const { @@ -111,6 +114,7 @@ class ESPBTDevice { std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; + const BLEScanResult *scan_result_{nullptr}; }; class ESP32BLETracker; diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 3de32bfde9..24c31713bc 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -33,6 +33,7 @@ bool Nextion::send_command_(const std::string &command) { #ifdef USE_NEXTION_COMMAND_SPACING if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) { + ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str()); return false; } #endif // USE_NEXTION_COMMAND_SPACING @@ -43,10 +44,6 @@ bool Nextion::send_command_(const std::string &command) { const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; this->write_array(to_send, sizeof(to_send)); -#ifdef USE_NEXTION_COMMAND_SPACING - this->command_pacer_.mark_sent(); -#endif // USE_NEXTION_COMMAND_SPACING - return true; } @@ -377,12 +374,6 @@ void Nextion::process_nextion_commands_() { size_t commands_processed = 0; #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP -#ifdef USE_NEXTION_COMMAND_SPACING - if (!this->command_pacer_.can_send()) { - return; // Will try again in next loop iteration - } -#endif - size_t to_process_length = 0; std::string to_process; @@ -430,6 +421,7 @@ void Nextion::process_nextion_commands_() { } #ifdef USE_NEXTION_COMMAND_SPACING this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent + ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); #endif break; case 0x02: // invalid Component ID or name was used diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5b1ea491e3..393c47e720 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -46,7 +46,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}" + "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() ) if network_name := config.get(CONF_NETWORK_NAME): @@ -54,14 +54,14 @@ def set_sdkconfig_options(config): if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}" + "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() ) if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}" + "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() ) if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}") + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) if CONF_FORCE_DATASET in config: if config[CONF_FORCE_DATASET]: @@ -98,7 +98,7 @@ _CONNECTION_SCHEMA = cv.Schema( cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, cv.Optional(CONF_PSKC): cv.hex_int, - cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int, + cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network, } ) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index f40a56952a..24b3c23960 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() { // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // component this->mdns_services_ = this->mdns_->get_services(); - ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : this->mdns_services_) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { @@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() { if (error != OT_ERROR_NONE) { ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error)); } - ESP_LOGW(TAG, "Added service: %s", full_service.c_str()); + ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); - ESP_LOGW(TAG, "Finished SRP setup"); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { diff --git a/esphome/components/openthread/tlv.py b/esphome/components/openthread/tlv.py index 45c8c47227..4a7d21c47d 100644 --- a/esphome/components/openthread/tlv.py +++ b/esphome/components/openthread/tlv.py @@ -1,5 +1,6 @@ # Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 import binascii +import ipaddress from esphome.const import CONF_CHANNEL @@ -37,6 +38,12 @@ def parse_tlv(tlv) -> dict: if tag in TLV_TYPES: if tag == 3: output[TLV_TYPES[tag]] = val.decode("utf-8") + elif tag == 7: + mesh_local_prefix = binascii.hexlify(val).decode("utf-8") + mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" + ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) + ipv6_address = ipaddress.IPv6Address(ipv6_bytes) + output[TLV_TYPES[tag]] = f"{ipv6_address}/64" else: output[TLV_TYPES[tag]] = int.from_bytes(val) return output diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 964f533215..bf69b81bb5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -3,7 +3,15 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime -from ipaddress import AddressValueError, IPv4Address, ip_address +from ipaddress import ( + AddressValueError, + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) import logging import os import re @@ -1176,6 +1184,14 @@ def ipv4address(value): return address +def ipv6address(value): + try: + address = IPv6Address(value) + except AddressValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 address") from exc + return address + + def ipv4address_multi_broadcast(value): address = ipv4address(value) if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))): @@ -1193,6 +1209,33 @@ def ipaddress(value): return address +def ipv4network(value): + """Validate that the value is a valid IPv4 network.""" + try: + network = IPv4Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv4 network") from exc + return network + + +def ipv6network(value): + """Validate that the value is a valid IPv6 network.""" + try: + network = IPv6Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 network") from exc + return network + + +def ipnetwork(value): + """Validate that the value is a valid IP network.""" + try: + network = ip_network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IP network") from exc + return network + + def _valid_topic(value): """Validate that this is a valid topic name/filter.""" if value is None: # Used to disable publishing and subscribing diff --git a/esphome/const.py b/esphome/const.py index 95a5dbe218..3ef82aff5b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.6.0" +__version__ = "2025.6.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 78deec8e65..bd1806affc 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -5,7 +5,7 @@ import fnmatch import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper -from ipaddress import _BaseAddress +from ipaddress import _BaseAddress, _BaseNetwork import logging import math import os @@ -621,6 +621,7 @@ ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 482fd1a453..f53b323bec 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -8,4 +8,6 @@ openthread: pan_id: 0x8f28 ext_pan_id: 0xd63e8e3e495ebbc3 pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 + mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true +