1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 07:31:51 +00:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Jesse Hills
f5aab154a6 Merge pull request #9299 from esphome/bump-2025.6.3
2025.6.3
2025-07-03 13:07:17 +12:00
Jesse Hills
66e090ff5b Bump version to 2025.6.3 2025-07-03 08:27:46 +12:00
Craig Andrews
d41298897f [http_request] allow retrieval of more than just the first header (#9242) 2025-07-03 08:27:46 +12:00
J. Nick Koston
ba42de536c Fix crash when event last_event_type is null in web_server (#9266) 2025-07-03 08:27:46 +12:00
Jesse Hills
bdc9f5f3b2 Fix api log client crashing when api encryption is dynamic (#9245) 2025-07-03 08:27:46 +12:00
Rezoran
90f9ab0d3e [uart] fix: missing uart_config_t struct initialisation (#9235) 2025-07-03 08:27:46 +12:00
Jesse Hills
50b7349fe0 Merge pull request #9234 from esphome/bump-2025.6.2
2025.6.2
2025-06-28 15:47:02 +12:00
Jonathan Swoboda
61b3379f48 [i2c] Disable i2c scan on certain idf versions (#9237) 2025-06-28 13:33:05 +12:00
Samuel Sieb
5010a0f5e7 [mcp23xxx_base] fix pin interrupts (#9244)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-06-28 13:32:57 +12:00
Jesse Hills
948aa13fb9 Bump version to 2025.6.2 2025-06-27 23:16:13 +12:00
scaiper
9e993ac603 [esp32] Change `enable_lwip_mdns_queries default to True` (#9188) 2025-06-27 23:16:12 +12:00
Kevin Ahrendt
9f3f4ead4f [voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) 2025-06-27 23:16:12 +12:00
Kevin Ahrendt
068aa0ff1e [speaker] bugfix: continue to block tasks if stop flag is set (#9222) 2025-06-27 23:16:12 +12:00
Kevin Ahrendt
e146c0796a [audio] Bugfix: improve timeout handling (#9221) 2025-06-27 23:16:12 +12:00
Clyde Stubbs
cceab26bfb [lvgl] Fix dangling pointer issue with qrcode (#9190) 2025-06-27 23:16:12 +12:00
Jesse Hills
fa34adbf6c Merge pull request #9185 from esphome/bump-2025.6.1
2025.6.1
2025-06-24 07:27:59 +12:00
Jesse Hills
22e360d479 Bump version to 2025.6.1 2025-06-23 23:32:22 +12:00
myhomeiot
649936200e Restore access to BLEScanResult as get_scan_result (#9148) 2025-06-23 23:32:22 +12:00
rwrozelle
5d6e690c12 Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-23 23:32:22 +12:00
Jesse Hills
2f2ecadae7 [config validation] Add more ip address / network validators (#9181) 2025-06-23 23:32:22 +12:00
J. Nick Koston
6dfb9eba61 Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) 2025-06-23 23:32:22 +12:00
Edward Firmo
24587fe875 [nextion] Fix command spacing double timing and response blocking issues (#9134) 2025-06-23 23:32:22 +12:00
J. Nick Koston
a1aebe6a2c Eliminate memory fragmentation with BLE event pool (#9101) 2025-06-23 23:32:22 +12:00
32 changed files with 709 additions and 247 deletions

View File

@@ -490,7 +490,7 @@ esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow

View File

@@ -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.3
# 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

View File

@@ -29,8 +29,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD]
noise_psk: str | None = None
if CONF_ENCRYPTION in conf:
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
noise_psk = key
_LOGGER.info("Starting log output from %s using esphome API", address)
cli = APIClient(
address,

View File

@@ -5,6 +5,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
@@ -16,13 +17,13 @@ namespace audio {
static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
// The number of times the http read times out with no data before throwing an error
static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;
static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
static const uint8_t MAX_REDIRECTION = 5;
static const uint8_t MAX_REDIRECTIONS = 5;
static const char *const TAG = "audio_reader";
// Some common HTTP status codes - borrowed from http_request component accessed 20241224
enum HttpStatus {
@@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
client_config.url = uri.c_str();
client_config.cert_pem = nullptr;
client_config.disable_auto_redirect = false;
client_config.max_redirection_count = 10;
client_config.max_redirection_count = MAX_REDIRECTIONS;
client_config.event_handler = http_event_handler;
client_config.user_data = this;
client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
@@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
esp_err_t err = esp_http_client_open(this->client_, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open URL");
this->cleanup_connection_();
return err;
}
int64_t header_length = esp_http_client_fetch_headers(this->client_);
uint8_t reattempt_count = 0;
while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) {
this->cleanup_connection_();
if (header_length != -ESP_ERR_HTTP_EAGAIN) {
// Serious error, no recovery
return ESP_FAIL;
} else {
// Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available
this->client_ = esp_http_client_init(&client_config);
esp_http_client_open(this->client_, 0);
header_length = esp_http_client_fetch_headers(this->client_);
++reattempt_count;
}
}
if (header_length < 0) {
ESP_LOGE(TAG, "Failed to fetch headers");
this->cleanup_connection_();
return ESP_FAIL;
}
@@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
ssize_t redirect_count = 0;
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) {
err = esp_http_client_open(this->client_, 0);
if (err != ESP_OK) {
this->cleanup_connection_();
@@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() {
return AudioReaderState::FINISHED;
}
} else if (this->output_transfer_buffer_->free() > 0) {
size_t bytes_to_read = this->output_transfer_buffer_->free();
int received_len =
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
this->output_transfer_buffer_->free());
if (received_len > 0) {
this->output_transfer_buffer_->increase_buffer_length(received_len);
this->last_data_read_ms_ = millis();
} else if (received_len < 0) {
return AudioReaderState::READING;
} else if (received_len <= 0) {
// HTTP read error
this->cleanup_connection_();
return AudioReaderState::FAILED;
} else {
if (bytes_to_read > 0) {
// Read timed out
if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) {
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
delay(READ_WRITE_TIMEOUT_MS);
if (received_len == -1) {
// A true connection error occured, no chance at recovery
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
// Read timed out, manually verify if it has been too long since the last successful read
if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) {
ESP_LOGE(TAG, "Timed out");
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
delay(READ_WRITE_TIMEOUT_MS);
}
}

View File

@@ -605,7 +605,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_MDNS_QUERIES, default=False
CONF_ENABLE_LWIP_MDNS_QUERIES, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
@@ -760,7 +760,7 @@ async def to_code(config):
and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False):
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)

View File

@@ -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<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::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<esp_ble_gap_cb_param_t *>(&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<esp_ble_gap_cb_param_t *>(&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<esp_ble_gap_cb_param_t *>(&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<esp_ble_gap_cb_param_t *>(&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<esp_ble_gap_cb_param_t *>(&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<typename... Args> 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;

View File

@@ -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<BLEEvent, 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

@@ -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<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
// 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<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
@@ -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<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; // 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<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;
}
}
};
// 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

View File

@@ -0,0 +1,72 @@
#pragma once
#ifdef USE_ESP32
#include <atomic>
#include <cstddef>
#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<uint8_t SIZE> 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<BLEEvent> allocator(RAMAllocator<BLEEvent>::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<BLEEvent, SIZE> free_list_; // Free events ready for reuse
uint8_t total_created_; // Total events created (high water mark)
};
} // namespace esp32_ble
} // namespace esphome
#endif

View File

@@ -18,7 +18,7 @@
namespace esphome {
namespace esp32_ble {
template<class T, size_t SIZE> class LockFreeQueue {
template<class T, uint8_t SIZE> class LockFreeQueue {
public:
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
@@ -26,8 +26,8 @@ template<class T, size_t SIZE> 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 T, size_t SIZE> 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 T, size_t SIZE> 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<size_t> head_;
std::atomic<size_t> tail_;
std::atomic<size_t> dropped_count_;
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
std::atomic<uint16_t> 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<uint8_t> head_;
// Atomic: written by producer (push), read by consumer (pop) to check if empty
std::atomic<uint8_t> tail_;
};
} // namespace esp32_ble

View File

@@ -522,6 +522,7 @@ optional<ESPBLEiBeacon> 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<esp_ble_addr_type_t>(scan_result.ble_addr_type);

View File

@@ -85,6 +85,9 @@ class ESPBTDevice {
const std::vector<ServiceData> &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<ESPBLEiBeacon> get_ibeacon() const {
@@ -111,6 +114,7 @@ class ESPBTDevice {
std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{};
const BLEScanResult *scan_result_{nullptr};
};
class ESP32BLETracker;

View File

@@ -133,7 +133,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(std::string url, std:
std::string header_value = container->client_.header(i).c_str();
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
container->response_headers_[header_name].push_back(header_value);
break;
}
}

View File

@@ -42,7 +42,6 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
const std::string header_value = evt->header_value;
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
user_data->response_headers[header_name].push_back(header_value);
break;
}
break;
}

View File

@@ -1,5 +1,8 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -12,6 +15,8 @@ from esphome.const import (
CONF_SCL,
CONF_SDA,
CONF_TIMEOUT,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -19,6 +24,7 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
import esphome.final_validate as fv
LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
i2c_ns = cg.esphome_ns.namespace("i2c")
I2CBus = i2c_ns.class_("I2CBus")
@@ -40,6 +46,32 @@ def _bus_declare_type(value):
raise NotImplementedError
def validate_config(config):
if (
config[CONF_SCAN]
and CORE.is_esp32
and CORE.using_esp_idf
and esp32.get_esp32_variant()
in [
esp32.const.VARIANT_ESP32C5,
esp32.const.VARIANT_ESP32C6,
esp32.const.VARIANT_ESP32P4,
]
):
version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if version.major == 5 and (
(version.minor == 3 and version.patch <= 3)
or (version.minor == 4 and version.patch <= 1)
):
LOGGER.warning(
"There is a bug in esp-idf version %s that breaks I2C scan, I2C scan "
"has been disabled, see https://github.com/esphome/issues/issues/7128",
str(version),
)
config[CONF_SCAN] = False
return config
pin_with_input_and_output_support = pins.internal_gpio_pin_number(
{CONF_OUTPUT: True, CONF_INPUT: True}
)
@@ -65,6 +97,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
validate_config,
)

View File

@@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_SIZE, CONF_TEXT
from esphome.cpp_generator import MockObjClass
from ..defines import CONF_MAIN, literal
from ..defines import CONF_MAIN
from ..lv_validation import color, color_retmapper, lv_text
from ..lvcode import LocalVariable, lv, lv_expr
from ..schemas import TEXT_SCHEMA
@@ -34,7 +34,7 @@ class QrCodeType(WidgetType):
)
def get_uses(self):
return ("canvas", "img")
return ("canvas", "img", "label")
def obj_creator(self, parent: MockObjClass, config: dict):
dark_color = color_retmapper(config[CONF_DARK_COLOR])
@@ -45,10 +45,8 @@ class QrCodeType(WidgetType):
async def to_code(self, w: Widget, config):
if (value := config.get(CONF_TEXT)) is not None:
value = await lv_text.process(value)
with LocalVariable(
"qr_text", cg.const_char_ptr, value, modifier=""
) as str_obj:
lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})"))
with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj:
lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size())
qr_code_spec = QrCodeType()

View File

@@ -6,7 +6,11 @@ namespace mcp23xxx_base {
float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; }
void MCP23XXXGPIOPin::setup() { pin_mode(flags_); }
void MCP23XXXGPIOPin::setup() {
pin_mode(flags_);
this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_);
}
void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }

View File

@@ -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

View File

@@ -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,
}
)

View File

@@ -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) {

View File

@@ -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

View File

@@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) {
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED);
// Wait until the pipeline notifies us the source of the media file
EventBits_t event_bits =
xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP |
EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
EventBits_t event_bits = xEventGroupWaitBits(
this_pipeline->event_group_,
EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED |
@@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) {
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED);
// Wait until the reader notifies us that the media type is available
EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE |
EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
EventBits_t event_bits =
xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);

View File

@@ -42,7 +42,7 @@ uart_config_t IDFUARTComponent::get_config_() {
break;
}
uart_config_t uart_config;
uart_config_t uart_config{};
uart_config.baud_rate = this->baud_rate_;
uart_config.data_bits = data_bits;
uart_config.parity = parity;

View File

@@ -17,10 +17,11 @@ from esphome.const import (
AUTO_LOAD = ["socket"]
DEPENDENCIES = ["api", "microphone"]
CODEOWNERS = ["@jesserockz"]
CODEOWNERS = ["@jesserockz", "@kahrendt"]
CONF_ON_END = "on_end"
CONF_ON_INTENT_END = "on_intent_end"
CONF_ON_INTENT_PROGRESS = "on_intent_progress"
CONF_ON_INTENT_START = "on_intent_start"
CONF_ON_LISTENING = "on_listening"
CONF_ON_START = "on_start"
@@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_INTENT_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_INTENT_END): automation.validate_automation(
single=True
),
@@ -282,6 +286,13 @@ async def to_code(config):
config[CONF_ON_INTENT_START],
)
if CONF_ON_INTENT_PROGRESS in config:
await automation.build_automation(
var.get_intent_progress_trigger(),
[(cg.std_string, "x")],
config[CONF_ON_INTENT_PROGRESS],
)
if CONF_ON_INTENT_END in config:
await automation.build_automation(
var.get_intent_end_trigger(),

View File

@@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() {
break;
case State::AWAITING_RESPONSE:
this->signal_stop_();
break;
// Fallthrough intended to stop a streaming TTS announcement that has potentially started
case State::STREAMING_RESPONSE:
#ifdef USE_MEDIA_PLAYER
// Stop any ongoing media player announcement
@@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
switch (msg.event_type) {
case api::enums::VOICE_ASSISTANT_RUN_START:
ESP_LOGD(TAG, "Assist Pipeline running");
#ifdef USE_MEDIA_PLAYER
this->started_streaming_tts_ = false;
for (auto arg : msg.data) {
if (arg.name == "url") {
this->tts_response_url_ = std::move(arg.value);
}
}
#endif
this->defer([this]() { this->start_trigger_->trigger(); });
break;
case api::enums::VOICE_ASSISTANT_WAKE_WORD_START:
@@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
if (text.empty()) {
ESP_LOGW(TAG, "No text in STT_END event");
return;
} else if (text.length() > 500) {
text = text.substr(0, 497) + "...";
}
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
this->defer([this, text]() { this->stt_end_trigger_->trigger(text); });
@@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGD(TAG, "Intent started");
this->defer([this]() { this->intent_start_trigger_->trigger(); });
break;
case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: {
ESP_LOGD(TAG, "Intent progress");
std::string tts_url_for_trigger = "";
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
for (const auto &arg : msg.data) {
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform();
this->media_player_wait_for_announcement_start_ = true;
this->media_player_wait_for_announcement_end_ = false;
this->started_streaming_tts_ = true;
tts_url_for_trigger = this->tts_response_url_;
this->tts_response_url_.clear(); // Reset streaming URL
}
}
}
#endif
this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); });
break;
}
case api::enums::VOICE_ASSISTANT_INTENT_END: {
for (auto arg : msg.data) {
if (arg.name == "conversation_id") {
@@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGW(TAG, "No text in TTS_START event");
return;
}
if (text.length() > 500) {
text = text.substr(0, 497) + "...";
}
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
this->defer([this, text]() {
this->tts_start_trigger_->trigger(text);
@@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
this->defer([this, url]() {
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
this->media_player_wait_for_announcement_start_ = true;

View File

@@ -177,6 +177,7 @@ class VoiceAssistant : public Component {
Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; }
Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; }
Trigger<std::string> *get_intent_progress_trigger() const { return this->intent_progress_trigger_; }
Trigger<> *get_listening_trigger() const { return this->listening_trigger_; }
Trigger<> *get_end_trigger() const { return this->end_trigger_; }
Trigger<> *get_start_trigger() const { return this->start_trigger_; }
@@ -233,6 +234,7 @@ class VoiceAssistant : public Component {
Trigger<> *tts_stream_start_trigger_ = new Trigger<>();
Trigger<> *tts_stream_end_trigger_ = new Trigger<>();
#endif
Trigger<std::string> *intent_progress_trigger_ = new Trigger<std::string>();
Trigger<> *wake_word_detected_trigger_ = new Trigger<>();
Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>();
Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>();
@@ -268,6 +270,8 @@ class VoiceAssistant : public Component {
#endif
#ifdef USE_MEDIA_PLAYER
media_player::MediaPlayer *media_player_{nullptr};
std::string tts_response_url_{""};
bool started_streaming_tts_{false};
bool media_player_wait_for_announcement_start_{false};
bool media_player_wait_for_announcement_end_{false};
#endif

View File

@@ -1734,12 +1734,15 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
request->send(404);
}
static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; }
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type),
DETAIL_STATE);
auto *event = static_cast<event::Event *>(source);
return web_server->event_json(event, get_event_type(event), DETAIL_STATE);
}
std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) {
return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), DETAIL_ALL);
auto *event = static_cast<event::Event *>(source);
return web_server->event_json(event, get_event_type(event), DETAIL_ALL);
}
std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) {
return json::build_json([this, obj, event_type, start_config](JsonObject root) {

View File

@@ -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

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.6.0"
__version__ = "2025.6.3"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -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)

View File

@@ -646,7 +646,9 @@ lvgl:
on_click:
lvgl.qrcode.update:
id: lv_qr
text: homeassistant.io
text:
format: "A string with a number %d"
args: ['(int)(random_uint32() % 1000)']
- slider:
min_value: 0

View File

@@ -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