From 39d5cbc74a2284fe52d98b8d7e7b7f7b3a30cc48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:20:40 -0500 Subject: [PATCH 01/13] [esp32_ble_server] Replace EventEmitter with direct callbacks to reduce memory usage (#10946) --- CODEOWNERS | 1 - .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 17 +-- .../esp32_ble_server/ble_characteristic.h | 30 ++--- .../esp32_ble_server/ble_descriptor.cpp | 7 +- .../esp32_ble_server/ble_descriptor.h | 22 ++-- .../esp32_ble_server/ble_server.cpp | 12 +- .../components/esp32_ble_server/ble_server.h | 35 ++++-- .../ble_server_automations.cpp | 37 ++---- .../esp32_ble_server/ble_server_automations.h | 42 ++----- .../esp32_improv/esp32_improv_component.cpp | 14 +-- esphome/components/event_emitter/__init__.py | 5 - .../components/event_emitter/event_emitter.h | 117 ------------------ 13 files changed, 107 insertions(+), 234 deletions(-) delete mode 100644 esphome/components/event_emitter/__init__.py delete mode 100644 esphome/components/event_emitter/event_emitter.h diff --git a/CODEOWNERS b/CODEOWNERS index 3747acd2b5..0b9935faf7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -160,7 +160,6 @@ esphome/components/esp_ldo/* @clydebarrow esphome/components/espnow/* @jesserockz esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/event/* @nohat -esphome/components/event_emitter/* @Rapsssito esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 9eab9647b3..10fa09fcc3 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -26,7 +26,7 @@ from esphome.const import ( from esphome.core import CORE from esphome.schema_extractors import SCHEMA_EXTRACT -AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] +AUTO_LOAD = ["esp32_ble", "bytebuffer"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] DEPENDENCIES = ["esp32"] DOMAIN = "esp32_ble_server" diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index fabcc75321..d485d9fe2d 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -73,7 +73,7 @@ void BLECharacteristic::notify() { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { - descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector &value, uint16_t conn_id) { + descriptor->on_write([this](std::span value, uint16_t conn_id) { if (value.size() != 2) return; uint16_t cccd = encode_uint16(value[1], value[0]); @@ -208,8 +208,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (!param->read.need_rsp) break; // For some reason you can request a read but not want a response - this->EventEmitter::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, - param->read.conn_id); + if (this->on_read_callback_) { + (*this->on_read_callback_)(param->read.conn_id); + } uint16_t max_offset = 22; @@ -277,8 +278,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } if (!param->write.is_prep) { - this->EventEmitter, uint16_t>::emit_( - BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(this->value_, param->write.conn_id); + } } break; @@ -289,8 +291,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt break; this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { - this->EventEmitter, uint16_t>::emit_( - BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); + } } esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 97b3af2a21..4a29683f41 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -2,10 +2,12 @@ #include "ble_descriptor.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include +#include +#include +#include #ifdef USE_ESP32 @@ -22,22 +24,10 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -using namespace event_emitter; class BLEService; -namespace BLECharacteristicEvt { -enum VectorEvt { - ON_WRITE, -}; - -enum EmptyEvt { - ON_READ, -}; -} // namespace BLECharacteristicEvt - -class BLECharacteristic : public EventEmitter, uint16_t>, - public EventEmitter { +class BLECharacteristic { public: BLECharacteristic(ESPBTUUID uuid, uint32_t properties); ~BLECharacteristic(); @@ -76,6 +66,15 @@ class BLECharacteristic : public EventEmitter, uint16_t)> &&callback) { + this->on_write_callback_ = + std::make_unique, uint16_t)>>(std::move(callback)); + } + void on_read(std::function &&callback) { + this->on_read_callback_ = std::make_unique>(std::move(callback)); + } + protected: bool write_event_{false}; BLEService *service_{}; @@ -98,6 +97,9 @@ class BLECharacteristic : public EventEmitter, uint16_t)>> on_write_callback_; + std::unique_ptr> on_read_callback_; + esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; enum State : uint8_t { diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index afbe579513..16941cca0f 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -74,9 +74,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; this->value_.attr_len = param->write.len; memcpy(this->value_.attr_value, param->write.value, param->write.len); - this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, - std::vector(param->write.value, param->write.value + param->write.len), - param->write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(std::span(param->write.value, param->write.len), + param->write.conn_id); + } break; } default: diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 8d3c22c5a1..425462a316 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -1,30 +1,26 @@ #pragma once #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 #include #include +#include +#include +#include namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -using namespace event_emitter; class BLECharacteristic; -namespace BLEDescriptorEvt { -enum VectorEvt { - ON_WRITE, -}; -} // namespace BLEDescriptorEvt - -class BLEDescriptor : public EventEmitter, uint16_t> { +// Base class for BLE descriptors +class BLEDescriptor { public: BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); virtual ~BLEDescriptor(); @@ -39,6 +35,12 @@ class BLEDescriptor : public EventEmitterstate_ == CREATED; } bool is_failed() { return this->state_ == FAILED; } + // Direct callback registration - only allocates when callback is set + void on_write(std::function, uint16_t)> &&callback) { + this->on_write_callback_ = + std::make_unique, uint16_t)>>(std::move(callback)); + } + protected: BLECharacteristic *characteristic_{nullptr}; ESPBTUUID uuid_; @@ -46,6 +48,8 @@ class BLEDescriptor : public EventEmitter, uint16_t)>> on_write_callback_; + esp_gatt_perm_t permissions_{}; enum State : uint8_t { diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 89299bb417..942be7e597 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -147,20 +147,28 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { return nullptr; } +void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) { + for (auto &entry : this->callbacks_) { + if (entry.type == type) { + entry.callback(conn_id); + } + } +} + void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); this->add_client_(param->connect.conn_id); - this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); + this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; } case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); this->remove_client_(param->disconnect.conn_id); this->parent_->advertising_start(); - this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); + this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); break; } case ESP_GATTS_REG_EVT: { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index b5973ed099..48005b1346 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -13,6 +13,7 @@ #include #include #include +#include #ifdef USE_ESP32 @@ -24,18 +25,7 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -namespace BLEServerEvt { -enum EmptyEvt { - ON_CONNECT, - ON_DISCONNECT, -}; -} // namespace BLEServerEvt - -class BLEServer : public Component, - public GATTsEventHandler, - public BLEStatusEventHandler, - public Parented, - public EventEmitter { +class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { public: void setup() override; void loop() override; @@ -65,7 +55,25 @@ class BLEServer : public Component, void ble_before_disabled_event_handler() override; + // Direct callback registration - supports multiple callbacks + void on_connect(std::function &&callback) { + this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)}); + } + void on_disconnect(std::function &&callback) { + this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)}); + } + protected: + enum class CallbackType : uint8_t { + ON_CONNECT, + ON_DISCONNECT, + }; + + struct CallbackEntry { + CallbackType type; + std::function callback; + }; + struct ServiceEntry { ESPBTUUID uuid; uint8_t inst_id; @@ -76,6 +84,9 @@ class BLEServer : public Component, void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } + void dispatch_callbacks_(CallbackType type, uint16_t conn_id); + + std::vector callbacks_; std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp index 67e00a9bfe..0761de994a 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.cpp +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -14,9 +14,10 @@ Trigger, uint16_t> *BLETriggers::create_characteristic_on_w BLECharacteristic *characteristic) { Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); - characteristic->EventEmitter, uint16_t>::on( - BLECharacteristicEvt::VectorEvt::ON_WRITE, - [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + characteristic->on_write([on_write_trigger](std::span data, uint16_t id) { + // Convert span to vector for trigger + on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); + }); return on_write_trigger; } #endif @@ -25,9 +26,10 @@ Trigger, uint16_t> *BLETriggers::create_characteristic_on_w Trigger, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); - descriptor->on( - BLEDescriptorEvt::VectorEvt::ON_WRITE, - [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + descriptor->on_write([on_write_trigger](std::span data, uint16_t id) { + // Convert span to vector for trigger + on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); + }); return on_write_trigger; } #endif @@ -35,8 +37,7 @@ Trigger, uint16_t> *BLETriggers::create_descriptor_on_write #ifdef USE_ESP32_BLE_SERVER_ON_CONNECT Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { Trigger *on_connect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) - server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, - [on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); + server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); return on_connect_trigger; } #endif @@ -44,38 +45,22 @@ Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *serv #ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT Trigger *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { Trigger *on_disconnect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) - server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, - [on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); + server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); return on_disconnect_trigger; } #endif #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, - EventEmitterListenerID listener_id, const std::function &pre_notify_listener) { // Find and remove existing listener for this characteristic auto *existing = this->find_listener_(characteristic); if (existing != nullptr) { - // Remove the previous listener - characteristic->EventEmitter::off(BLECharacteristicEvt::EmptyEvt::ON_READ, - existing->listener_id); - // Remove the pre-notify listener - this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, existing->pre_notify_listener_id); // Remove from vector this->remove_listener_(characteristic); } - // Create a new listener for the pre-notify event - EventEmitterListenerID pre_notify_listener_id = - this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, - [pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) { - // Only call the pre-notify listener if the characteristic is the one we are interested in - if (characteristic == evt_characteristic) { - pre_notify_listener(); - } - }); // Save the entry to the vector - this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id}); + this->listeners_.push_back({characteristic, pre_notify_listener}); } BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_( diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 8fcb5842c3..543b1153fc 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -4,7 +4,6 @@ #include "ble_characteristic.h" #include "ble_descriptor.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/core/automation.h" #include @@ -18,10 +17,6 @@ namespace esp32_ble_server { namespace esp32_ble_server_automations { using namespace esp32_ble; -using namespace event_emitter; - -// Invalid listener ID constant - 0 is used as sentinel value in EventEmitter -static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0; class BLETriggers { public: @@ -41,38 +36,29 @@ class BLETriggers { }; #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION -enum BLECharacteristicSetValueActionEvt { - PRE_NOTIFY, -}; - // Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic -class BLECharacteristicSetValueActionManager - : public EventEmitter { +class BLECharacteristicSetValueActionManager { public: // Singleton pattern static BLECharacteristicSetValueActionManager *get_instance() { static BLECharacteristicSetValueActionManager instance; return &instance; } - void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, - const std::function &pre_notify_listener); - EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { + void set_listener(BLECharacteristic *characteristic, const std::function &pre_notify_listener); + bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; } + void emit_pre_notify(BLECharacteristic *characteristic) { for (const auto &entry : this->listeners_) { if (entry.characteristic == characteristic) { - return entry.listener_id; + entry.pre_notify_listener(); + break; } } - return INVALID_LISTENER_ID; - } - void emit_pre_notify(BLECharacteristic *characteristic) { - this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic); } private: struct ListenerEntry { BLECharacteristic *characteristic; - EventEmitterListenerID listener_id; - EventEmitterListenerID pre_notify_listener_id; + std::function pre_notify_listener; }; std::vector listeners_; @@ -87,24 +73,22 @@ template class BLECharacteristicSetValueAction : public Actionset_buffer(buffer.get_data()); } void play(Ts... x) override { // If the listener is already set, do nothing - if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) + if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) return; // Set initial value this->parent_->set_value(this->buffer_.value(x...)); // Set the listener for read events - this->listener_id_ = this->parent_->EventEmitter::on( - BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) { - // Set the value of the characteristic every time it is read - this->parent_->set_value(this->buffer_.value(x...)); - }); + this->parent_->on_read([this, x...](uint16_t id) { + // Set the value of the characteristic every time it is read + this->parent_->set_value(this->buffer_.value(x...)); + }); // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic BLECharacteristicSetValueActionManager::get_instance()->set_listener( - this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); + this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); } protected: BLECharacteristic *parent_; - EventEmitterListenerID listener_id_; }; #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index ca08ff0cca..f773083890 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -38,8 +38,7 @@ void ESP32ImprovComponent::setup() { }); } #endif - global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, - [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); + global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); // Start with loop disabled - will be enabled by start() when needed this->disable_loop(); @@ -57,12 +56,11 @@ void ESP32ImprovComponent::setup_characteristics() { this->error_->add_descriptor(error_descriptor); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); - this->rpc_->EventEmitter, uint16_t>::on( - BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector &data, uint16_t id) { - if (!data.empty()) { - this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); - } - }); + this->rpc_->on_write([this](std::span data, uint16_t id) { + if (!data.empty()) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); BLEDescriptor *rpc_descriptor = new BLE2902(); this->rpc_->add_descriptor(rpc_descriptor); diff --git a/esphome/components/event_emitter/__init__.py b/esphome/components/event_emitter/__init__.py deleted file mode 100644 index fcbbf26f02..0000000000 --- a/esphome/components/event_emitter/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -CODEOWNERS = ["@Rapsssito"] - -# Allows event_emitter to be configured in yaml, to allow use of the C++ api. - -CONFIG_SCHEMA = {} diff --git a/esphome/components/event_emitter/event_emitter.h b/esphome/components/event_emitter/event_emitter.h deleted file mode 100644 index 74afde03c0..0000000000 --- a/esphome/components/event_emitter/event_emitter.h +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once -#include -#include -#include - -#include "esphome/core/log.h" - -namespace esphome { -namespace event_emitter { - -using EventEmitterListenerID = uint32_t; -static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0; - -// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this) -// and a list of arguments. Supports multiple listeners for each event. -template class EventEmitter { - public: - EventEmitterListenerID on(EvtType event, std::function listener) { - EventEmitterListenerID listener_id = this->get_next_id_(); - - // Find or create event entry - EventEntry *entry = this->find_or_create_event_(event); - entry->listeners.push_back({listener_id, listener}); - - return listener_id; - } - - void off(EvtType event, EventEmitterListenerID id) { - EventEntry *entry = this->find_event_(event); - if (entry == nullptr) - return; - - // Remove listener with given id - for (auto it = entry->listeners.begin(); it != entry->listeners.end(); ++it) { - if (it->id == id) { - // Swap with last and pop for efficient removal - *it = entry->listeners.back(); - entry->listeners.pop_back(); - - // Remove event entry if no more listeners - if (entry->listeners.empty()) { - this->remove_event_(event); - } - return; - } - } - } - - protected: - void emit_(EvtType event, Args... args) { - EventEntry *entry = this->find_event_(event); - if (entry == nullptr) - return; - - // Call all listeners for this event - for (const auto &listener : entry->listeners) { - listener.callback(args...); - } - } - - private: - struct Listener { - EventEmitterListenerID id; - std::function callback; - }; - - struct EventEntry { - EvtType event; - std::vector listeners; - }; - - EventEmitterListenerID get_next_id_() { - // Simple incrementing ID, wrapping around at max - EventEmitterListenerID next_id = (this->current_id_ + 1); - if (next_id == INVALID_LISTENER_ID) { - next_id = 1; - } - this->current_id_ = next_id; - return this->current_id_; - } - - EventEntry *find_event_(EvtType event) { - for (auto &entry : this->events_) { - if (entry.event == event) { - return &entry; - } - } - return nullptr; - } - - EventEntry *find_or_create_event_(EvtType event) { - EventEntry *entry = this->find_event_(event); - if (entry != nullptr) - return entry; - - // Create new event entry - this->events_.push_back({event, {}}); - return &this->events_.back(); - } - - void remove_event_(EvtType event) { - for (auto it = this->events_.begin(); it != this->events_.end(); ++it) { - if (it->event == event) { - // Swap with last and pop - *it = this->events_.back(); - this->events_.pop_back(); - return; - } - } - } - - std::vector events_; - EventEmitterListenerID current_id_ = 0; -}; - -} // namespace event_emitter -} // namespace esphome From 19439199ccd2d23c2071ef067f0c727542f1e65a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:25:04 -0500 Subject: [PATCH 02/13] [api] Add configurable send queue limit to prevent OOM crashes (#10973) --- esphome/components/api/__init__.py | 15 +++++++ esphome/components/api/api_frame_helper.cpp | 49 +++++++++++++-------- esphome/components/api/api_frame_helper.h | 12 +++-- esphome/core/defines.h | 1 + 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index e0d4fc8df2..8f0910b9a3 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -61,6 +61,7 @@ CONF_CUSTOM_SERVICES = "custom_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" +CONF_MAX_SEND_QUEUE = "max_send_queue" def validate_encryption_key(value): @@ -183,6 +184,19 @@ CONFIG_SCHEMA = cv.All( host=8, # Abundant resources ln882x=8, # Moderate RAM ): cv.int_range(min=1, max=20), + # Maximum queued send buffers per connection before dropping connection + # Each buffer uses ~8-12 bytes overhead plus actual message size + # Platform defaults based on available RAM and typical message rates: + cv.SplitDefault( + CONF_MAX_SEND_QUEUE, + esp8266=5, # Limited RAM, need to fail fast + esp32=8, # More RAM, can buffer more + rp2040=5, # Limited RAM + bk72xx=8, # Moderate RAM + rtl87xx=8, # Moderate RAM + host=16, # Abundant resources + ln882x=8, # Moderate RAM + ): cv.int_range(min=1, max=64), } ).extend(cv.COMPONENT_SCHEMA), cv.rename_key(CONF_SERVICES, CONF_ACTIONS), @@ -205,6 +219,7 @@ async def to_code(config): cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) if CONF_MAX_CONNECTIONS in config: cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) + cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) # Set USE_API_SERVICES if any services are enabled if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index a63199a5c4..20f8fcaf61 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -81,7 +81,7 @@ const LogString *api_error_to_logstr(APIError err) { // Default implementation for loop - handles sending buffered data APIError APIFrameHelper::loop() { - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { APIError err = try_send_tx_buf_(); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { return err; @@ -103,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() { // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset) { - SendBuffer buffer; - buffer.size = total_write_len - offset; - buffer.data = std::make_unique(buffer.size); + // Check if queue is full + if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) { + HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_); + this->state_ = State::FAILED; + return; + } + + uint16_t buffer_size = total_write_len - offset; + auto &buffer = this->tx_buf_[this->tx_buf_tail_]; + buffer = std::make_unique(SendBuffer{ + .data = std::make_unique(buffer_size), + .size = buffer_size, + .offset = 0, + }); uint16_t to_skip = offset; uint16_t write_pos = 0; @@ -118,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, // Include this segment (partially or fully) const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; uint16_t len = static_cast(iov[i].iov_len) - to_skip; - std::memcpy(buffer.data.get() + write_pos, src, len); + std::memcpy(buffer->data.get() + write_pos, src, len); write_pos += len; to_skip = 0; } } - this->tx_buf_.push_back(std::move(buffer)); + + // Update circular buffer tracking + this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE; + this->tx_buf_count_++; } // This method writes data to socket or buffers it @@ -141,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ #endif // Try to send any existing buffered data first if there is any - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { APIError send_result = try_send_tx_buf_(); // If real error occurred (not just WOULD_BLOCK), return it if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { @@ -150,7 +164,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ // If there is still data in the buffer, we can't send, buffer // the new data and return - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } @@ -178,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ } // Common implementation for trying to send buffered data -// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method +// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method APIError APIFrameHelper::try_send_tx_buf_() { // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check - bool tx_buf_empty = false; - while (!tx_buf_empty) { + while (this->tx_buf_count_ > 0) { // Get the first buffer in the queue - SendBuffer &front_buffer = this->tx_buf_.front(); + SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get(); // Try to send the remaining data in this buffer - ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); + ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining()); if (sent == -1) { return this->handle_socket_write_error_(); } else if (sent == 0) { // Nothing sent but not an error return APIError::WOULD_BLOCK; - } else if (static_cast(sent) < front_buffer.remaining()) { + } else if (static_cast(sent) < front_buffer->remaining()) { // Partially sent, update offset // Cast to ensure no overflow issues with uint16_t - front_buffer.offset += static_cast(sent); + front_buffer->offset += static_cast(sent); return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer } else { // Buffer completely sent, remove it from the queue - this->tx_buf_.pop_front(); - // Update empty status for the loop condition - tx_buf_empty = this->tx_buf_.empty(); + this->tx_buf_[this->tx_buf_head_].reset(); + this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE; + this->tx_buf_count_--; // Continue loop to try sending the next buffer } } diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index c11d701ffe..3184250e8c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -1,7 +1,8 @@ #pragma once +#include #include -#include #include +#include #include #include #include @@ -79,7 +80,7 @@ class APIFrameHelper { virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; - bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } + bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } std::string getpeername() { return socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { @@ -161,7 +162,7 @@ class APIFrameHelper { }; // Containers (size varies, but typically 12+ bytes on 32-bit) - std::deque tx_buf_; + std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector reusable_iovs_; std::vector rx_buf_; @@ -174,7 +175,10 @@ class APIFrameHelper { State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - // 5 bytes total, 3 bytes padding + uint8_t tx_buf_head_{0}; + uint8_t tx_buf_tail_{0}; + uint8_t tx_buf_count_{0}; + // 8 bytes total, 0 bytes padding // Common initialization for both plaintext and noise protocols APIError init_common_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7fc42ea334..5516b06040 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -115,6 +115,7 @@ #define USE_API_NOISE #define USE_API_PLAINTEXT #define USE_API_SERVICES +#define API_MAX_SEND_QUEUE 8 #define USE_MD5 #define USE_SHA256 #define USE_MQTT From 0fd71ca2111da9030248a80b9e8c6b2df8c801ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:30:17 -0500 Subject: [PATCH 03/13] [mdns][openthread] Use StaticVector for services storage with compile-time capacity (#10976) --- esphome/components/mdns/__init__.py | 17 +++++++- esphome/components/mdns/mdns_component.cpp | 43 ++++---------------- esphome/components/mdns/mdns_component.h | 14 +++---- esphome/components/openthread/openthread.cpp | 9 ++-- esphome/components/openthread/openthread.h | 1 - esphome/core/defines.h | 1 + esphome/core/helpers.h | 10 +++++ 7 files changed, 44 insertions(+), 51 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index a84fe5a249..ce0241677d 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -17,6 +17,11 @@ from esphome.coroutine import CoroPriority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +# Components that create mDNS services at runtime +# IMPORTANT: If you add a new component here, you must also update the corresponding +# #ifdef blocks in mdns_component.cpp compile_records_() method +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server") + mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord") @@ -91,12 +96,20 @@ async def to_code(config): cg.add_define("USE_MDNS") - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) + # Calculate compile-time service count + service_count = sum( + 1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config + ) + len(config[CONF_SERVICES]) if config[CONF_SERVICES]: cg.add_define("USE_MDNS_EXTRA_SERVICES") + # Ensure at least 1 service (fallback service) + cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + for service in config[CONF_SERVICES]: txt = [ cg.StructInitializer( diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5d9788198f..eed2516c6a 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -74,32 +74,12 @@ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); void MDNSComponent::compile_records_() { this->hostname_ = App.get_name(); - // Calculate exact capacity needed for services vector - size_t services_count = 0; -#ifdef USE_API - if (api::global_api_server != nullptr) { - services_count++; - } -#endif -#ifdef USE_PROMETHEUS - services_count++; -#endif -#ifdef USE_WEBSERVER - services_count++; -#endif -#ifdef USE_MDNS_EXTRA_SERVICES - services_count += this->services_extra_.size(); -#endif - // Reserve for fallback service if needed - if (services_count == 0) { - services_count = 1; - } - this->services_.reserve(services_count); + // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES + // in mdns/__init__.py. If you add a new service here, update both locations. #ifdef USE_API if (api::global_api_server != nullptr) { - this->services_.emplace_back(); - auto &service = this->services_.back(); + auto &service = this->services_.emplace_next(); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.proto = MDNS_STR(SERVICE_TCP); service.port = api::global_api_server->get_port(); @@ -178,30 +158,23 @@ void MDNSComponent::compile_records_() { #endif // USE_API #ifdef USE_PROMETHEUS - this->services_.emplace_back(); - auto &prom_service = this->services_.back(); + auto &prom_service = this->services_.emplace_next(); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER - this->services_.emplace_back(); - auto &web_service = this->services_.back(); + auto &web_service = this->services_.emplace_next(); web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.proto = MDNS_STR(SERVICE_TCP); web_service.port = USE_WEBSERVER_PORT; #endif -#ifdef USE_MDNS_EXTRA_SERVICES - this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end()); -#endif - #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) // Publish "http" service if not using native API or any other services // This is just to have *some* mDNS service so that .local resolution works - this->services_.emplace_back(); - auto &fallback_service = this->services_.back(); + auto &fallback_service = this->services_.emplace_next(); fallback_service.service_type = "_http"; fallback_service.proto = "_tcp"; fallback_service.port = USE_WEBSERVER_PORT; @@ -214,7 +187,7 @@ void MDNSComponent::dump_config() { "mDNS:\n" " Hostname: %s", this->hostname_.c_str()); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), @@ -227,8 +200,6 @@ void MDNSComponent::dump_config() { #endif } -std::vector MDNSComponent::get_services() { return this->services_; } - } // namespace mdns } // namespace esphome #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f87ef08bcd..e0e268c914 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -2,13 +2,16 @@ #include "esphome/core/defines.h" #ifdef USE_MDNS #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace mdns { +// Service count is calculated at compile time by Python codegen +// MDNS_SERVICE_COUNT will always be defined + struct MDNSTXTRecord { std::string key; TemplatableValue value; @@ -36,18 +39,15 @@ class MDNSComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } #ifdef USE_MDNS_EXTRA_SERVICES - void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } + void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); } #endif - std::vector get_services(); + const StaticVector &get_services() const { return this->services_; } void on_shutdown() override; protected: -#ifdef USE_MDNS_EXTRA_SERVICES - std::vector services_extra_{}; -#endif - std::vector services_{}; + StaticVector services_{}; std::string hostname_; void compile_records_(); }; diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 5b5c113f83..57b972d195 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -143,11 +143,10 @@ void OpenThreadSrpComponent::setup() { return; } - // 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_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); - for (const auto &service : this->mdns_services_) { + // Get mdns services and copy their data (strings are copied with strdup below) + const auto &mdns_services = this->mdns_->get_services(); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size()); + for (const auto &service : mdns_services) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { ESP_LOGW(TAG, "Failed to allocate service entry"); diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index a9aff78e56..5d139c633d 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -57,7 +57,6 @@ class OpenThreadSrpComponent : public Component { protected: esphome::mdns::MDNSComponent *mdns_{nullptr}; - std::vector mdns_services_; std::vector> memory_pool_; void *pool_alloc_(size_t size); }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5516b06040..554e1ee13c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -82,6 +82,7 @@ #define USE_LVGL_TILEVIEW #define USE_LVGL_TOUCHSCREEN #define USE_MDNS +#define MDNS_SERVICE_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a28718de5a..53e82abfef 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -127,6 +127,16 @@ template class StaticVector { } } + // Return reference to next element and increment count (with bounds checking) + T &emplace_next() { + if (count_ >= N) { + // Should never happen with proper size calculation + // Return reference to last element to avoid crash + return data_[N - 1]; + } + return data_[count_++]; + } + size_t size() const { return count_; } bool empty() const { return count_ == 0; } From 41c073a45111e4d1290f0ca452a31027b48d88ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:33:58 -0500 Subject: [PATCH 04/13] [lock] Replace std::set with bitmask (saves 388B flash + 23B RAM per lock) (#10977) --- esphome/components/copy/lock/copy_lock.cpp | 2 +- esphome/components/lock/lock.h | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index 67a8acffec..25bd8c33ef 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -11,7 +11,7 @@ void CopyLock::setup() { traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_requires_code(source_->traits.get_requires_code()); - traits.set_supported_states(source_->traits.get_supported_states()); + traits.set_supported_states_mask(source_->traits.get_supported_states_mask()); traits.set_supports_open(source_->traits.get_supports_open()); this->publish_state(source_->state); diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 04c4cd71cd..9737569921 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -5,7 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" -#include +#include namespace esphome { namespace lock { @@ -44,16 +44,22 @@ class LockTraits { bool get_assumed_state() const { return this->assumed_state_; } void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } - bool supports_state(LockState state) const { return supported_states_.count(state); } - std::set get_supported_states() const { return supported_states_; } - void set_supported_states(std::set states) { supported_states_ = std::move(states); } - void add_supported_state(LockState state) { supported_states_.insert(state); } + bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); } + void set_supported_states(std::initializer_list states) { + supported_states_mask_ = 0; + for (auto state : states) { + supported_states_mask_ |= (1 << state); + } + } + uint8_t get_supported_states_mask() const { return supported_states_mask_; } + void set_supported_states_mask(uint8_t mask) { supported_states_mask_ = mask; } + void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); } protected: bool supports_open_{false}; bool requires_code_{false}; bool assumed_state_{false}; - std::set supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED}; + uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)}; }; /** This class is used to encode all control actions on a lock device. From 120a445abf41b84de646d7c006e92a911fcb59be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:37:47 -0500 Subject: [PATCH 05/13] [number] Reduce flash usage in NumberCall logging (#10983) --- esphome/components/number/number_call.cpp | 35 +++++++++++++++-------- esphome/components/number/number_call.h | 5 ++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 4219f85328..669dd65184 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -7,6 +7,17 @@ namespace number { static const char *const TAG = "number"; +// Helper functions to reduce code size for logging +void NumberCall::log_perform_warning_(const LogString *message) { + ESP_LOGW(TAG, "'%s': %s", this->parent_->get_name().c_str(), LOG_STR_ARG(message)); +} + +void NumberCall::log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, + float limit) { + ESP_LOGW(TAG, "'%s': %f %s %s %f", this->parent_->get_name().c_str(), val, LOG_STR_ARG(comparison), + LOG_STR_ARG(limit_type), limit); +} + NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } NumberCall &NumberCall::number_increment(bool cycle) { @@ -42,7 +53,7 @@ void NumberCall::perform() { const auto &traits = parent->traits; if (this->operation_ == NUMBER_OP_NONE) { - ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); + this->log_perform_warning_(LOG_STR("No operation")); return; } @@ -51,28 +62,28 @@ void NumberCall::perform() { float max_value = traits.get_max_value(); if (this->operation_ == NUMBER_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting number value", name); + ESP_LOGD(TAG, "'%s': Setting value", name); if (!this->value_.has_value() || std::isnan(*this->value_)) { - ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); + this->log_perform_warning_(LOG_STR("No value")); return; } target_value = this->value_.value(); } else if (this->operation_ == NUMBER_OP_TO_MIN) { if (std::isnan(min_value)) { - ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); + this->log_perform_warning_(LOG_STR("min undefined")); } else { target_value = min_value; } } else if (this->operation_ == NUMBER_OP_TO_MAX) { if (std::isnan(max_value)) { - ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); + this->log_perform_warning_(LOG_STR("max undefined")); } else { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); if (!parent->has_state()) { - ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); + this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); @@ -85,9 +96,9 @@ void NumberCall::perform() { } } } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); if (!parent->has_state()) { - ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); + this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); @@ -102,15 +113,15 @@ void NumberCall::perform() { } if (target_value < min_value) { - ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); + this->log_perform_warning_value_range_(LOG_STR("<"), LOG_STR("min"), target_value, min_value); return; } if (target_value > max_value) { - ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); + this->log_perform_warning_value_range_(LOG_STR(">"), LOG_STR("max"), target_value, max_value); return; } - ESP_LOGD(TAG, " New number value: %f", target_value); + ESP_LOGD(TAG, " New value: %f", target_value); this->parent_->control(target_value); } diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index bd50170be5..807207f0ec 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "number_traits.h" namespace esphome { @@ -33,6 +34,10 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + void log_perform_warning_(const LogString *message); + void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, + float limit); + Number *const parent_; NumberOperation operation_{NUMBER_OP_NONE}; optional value_; From 84c3cf5f1783ff12bd68fafd2a1b5aad9f72b903 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:38:58 -0500 Subject: [PATCH 06/13] [core] Replace std::pair with purpose-built named structs for component metadata (#10984) --- esphome/core/component.cpp | 46 +++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index ce4e2bf788..11d9501bb8 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -33,12 +33,22 @@ static const char *const TAG = "component"; // Using namespace-scope static to avoid guard variables (saves 16 bytes total) // This is safe because ESPHome is single-threaded during initialization namespace { +struct ComponentErrorMessage { + const Component *component; + const char *message; +}; + +struct ComponentPriorityOverride { + const Component *component; + float priority; +}; + // Error messages for failed components // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::unique_ptr>> component_error_messages; +std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::unique_ptr>> setup_priority_overrides; +std::unique_ptr> setup_priority_overrides; } // namespace namespace setup_priority { @@ -134,9 +144,9 @@ void Component::call_dump_config() { // Look up error message from global vector const char *error_msg = nullptr; if (component_error_messages) { - for (const auto &pair : *component_error_messages) { - if (pair.first == this) { - error_msg = pair.second; + for (const auto &entry : *component_error_messages) { + if (entry.component == this) { + error_msg = entry.message; break; } } @@ -306,17 +316,17 @@ void Component::status_set_error(const char *message) { if (message != nullptr) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { - component_error_messages = std::make_unique>>(); + component_error_messages = std::make_unique>(); } // Check if this component already has an error message - for (auto &pair : *component_error_messages) { - if (pair.first == this) { - pair.second = message; + for (auto &entry : *component_error_messages) { + if (entry.component == this) { + entry.message = message; return; } } // Add new error message - component_error_messages->emplace_back(this, message); + component_error_messages->emplace_back(ComponentErrorMessage{this, message}); } } void Component::status_clear_warning() { @@ -356,9 +366,9 @@ float Component::get_actual_setup_priority() const { // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) - for (const auto &pair : *setup_priority_overrides) { - if (pair.first == this) { - return pair.second; + for (const auto &entry : *setup_priority_overrides) { + if (entry.component == this) { + return entry.priority; } } } @@ -367,21 +377,21 @@ float Component::get_actual_setup_priority() const { void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { - setup_priority_overrides = std::make_unique>>(); + setup_priority_overrides = std::make_unique>(); // Reserve some space to avoid reallocations (most configs have < 10 overrides) setup_priority_overrides->reserve(10); } // Check if this component already has an override - for (auto &pair : *setup_priority_overrides) { - if (pair.first == this) { - pair.second = priority; + for (auto &entry : *setup_priority_overrides) { + if (entry.component == this) { + entry.priority = priority; return; } } // Add new override - setup_priority_overrides->emplace_back(this, priority); + setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } bool Component::has_overridden_loop() const { From 5932a4bd0ee7ffa0225fdcf5bd6be059c4a4a08a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:42:23 -0500 Subject: [PATCH 07/13] [web_server] Reduce flash and RAM usage by optimizing string construction (#10986) --- esphome/components/web_server/web_server.cpp | 94 ++++++++++---------- esphome/core/helpers.cpp | 20 ++++- esphome/core/helpers.h | 5 ++ 3 files changed, 72 insertions(+), 47 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 33141c2049..95e0d13b58 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -381,11 +381,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #endif // Helper functions to reduce code size by avoiding macro expansion -static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { - root["id"] = id; +static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { + char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null + const auto &object_id = obj->get_object_id(); + snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); + root["id"] = id_buf; if (start_config == DETAIL_ALL) { root["name"] = obj->get_name(); - root["icon"] = obj->get_icon(); + root["icon"] = obj->get_icon_ref(); root["entity_category"] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) @@ -393,17 +396,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id } } +// Keep as separate function even though only used once: reduces code size by ~48 bytes +// by allowing compiler to share code between template instantiations (bool, float, etc.) template -static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, +static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, JsonDetail start_config) { - set_json_id(root, obj, id, start_config); + set_json_id(root, obj, prefix, start_config); root["value"] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, - const std::string &state, const T &value, JsonDetail start_config) { - set_json_value(root, obj, id, value, start_config); +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, + const T &value, JsonDetail start_config) { + set_json_value(root, obj, prefix, value, start_config); root["state"] = state; } @@ -442,20 +447,20 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); + const auto uom_ref = obj->get_unit_of_measurement_ref(); + // Build JSON directly inline std::string state; if (std::isnan(value)) { state = "NA"; } else { - state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - if (!obj->get_unit_of_measurement().empty()) - state += " " + obj->get_unit_of_measurement(); + state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); } - set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); + set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); - if (!obj->get_unit_of_measurement().empty()) - root["uom"] = obj->get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; } return builder.serialize(); @@ -494,7 +499,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -567,7 +572,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); this->add_sorting_info_(root, obj); @@ -607,7 +612,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "button", start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -647,8 +652,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, - start_config); + set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -717,8 +721,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, - start_config); + set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; @@ -793,7 +796,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "light", start_config); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; light::LightJSONSchema::dump_json(*obj, root); @@ -881,8 +884,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -939,7 +942,9 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); + const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + + set_json_id(root, obj, "number", start_config); if (start_config == DETAIL_ALL) { root["min_value"] = value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); @@ -947,8 +952,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); root["mode"] = (int) obj->traits.get_mode(); - if (!obj->traits.get_unit_of_measurement().empty()) - root["uom"] = obj->traits.get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; this->add_sorting_info_(root, obj); } if (std::isnan(value)) { @@ -956,10 +961,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["state"] = "NA"; } else { root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - if (!obj->traits.get_unit_of_measurement().empty()) - state += " " + obj->traits.get_unit_of_measurement(); - root["state"] = state; + root["state"] = + value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); } return builder.serialize(); @@ -1013,7 +1016,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "date", start_config); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); root["value"] = value; root["state"] = value; @@ -1071,7 +1074,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "time", start_config); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); root["value"] = value; root["state"] = value; @@ -1129,7 +1132,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "datetime", start_config); std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); root["value"] = value; @@ -1184,7 +1187,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "text", start_config); root["min_length"] = obj->traits.get_min_length(); root["max_length"] = obj->traits.get_max_length(); root["pattern"] = obj->traits.get_pattern(); @@ -1245,7 +1248,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "select", value, value, start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root["option"].to(); for (auto &option : obj->traits.get_options()) { @@ -1314,7 +1317,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "climate", start_config); const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -1467,8 +1470,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, - start_config); + set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1546,8 +1548,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -1630,8 +1632,8 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro JsonObject root = builder.root(); char buf[16]; - set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), - PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); + set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), + value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1676,7 +1678,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "event", start_config); if (!event_type.empty()) { root["event_type"] = event_type; } @@ -1685,7 +1687,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty for (auto const &event_type : obj->get_event_types()) { event_types.add(event_type); } - root["device_class"] = obj->get_device_class(); + root["device_class"] = obj->get_device_class_ref(); this->add_sorting_info_(root, obj); } @@ -1748,7 +1750,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "update", start_config); root["value"] = obj->update_info.latest_version; root["state"] = update_state_to_string(obj->state); if (start_config == DETAIL_ALL) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 85c33ea2d3..d4f6809776 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include #include @@ -348,17 +349,34 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { +static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { auto multiplier = powf(10.0f, accuracy_decimals); value = roundf(value * multiplier) / multiplier; accuracy_decimals = 0; } +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + normalize_accuracy_decimals(value, accuracy_decimals); char tmp[32]; // should be enough, but we should maybe improve this at some point. snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { + normalize_accuracy_decimals(value, accuracy_decimals); + // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") + // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short + char tmp[64]; + if (unit_of_measurement.empty()) { + snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); + } else { + snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + } + return std::string(tmp); +} + int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 53e82abfef..e06f2d15ef 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -45,6 +45,9 @@ namespace esphome { +// Forward declaration to avoid circular dependency with string_ref.h +class StringRef; + /// @name STL backports ///@{ @@ -610,6 +613,8 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +/// Create a string from a value, an accuracy in decimals, and a unit of measurement. +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); From 9b6d62cd691d1c27c2d71e1f3ed43595602ed0fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:55:39 -0500 Subject: [PATCH 08/13] [web_server_idf] Fix watchdog timeout with unreliable event source connections (#11002) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server/web_server.cpp | 4 + .../web_server_idf/web_server_idf.cpp | 83 +++++++++++++++++-- .../web_server_idf/web_server_idf.h | 2 + 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 95e0d13b58..2df74e023e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -127,6 +127,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() { deferred_queue_.erase(deferred_queue_.begin()); this->consecutive_send_failures_ = 0; // Reset failure count on successful send } else { + // NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_() + // The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs + // fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic, + // also update the ESP-IDF implementation. this->consecutive_send_failures_++; if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { // Too many failures, connection is likely dead diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 51d763c508..c04285402b 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -25,6 +25,10 @@ #include "esphome/components/web_server/list_entities.h" #endif // USE_WEBSERVER +// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts +#include +#include + namespace esphome { namespace web_server_idf { @@ -46,6 +50,42 @@ DefaultHeaders default_headers_instance; DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; } +namespace { +// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full +/** + * Sends data on a socket in non-blocking mode. + * + * @param hd HTTP server handle (unused). + * @param sockfd Socket file descriptor. + * @param buf Buffer to send. + * @param buf_len Length of buffer. + * @param flags Flags for send(). + * @return + * - Number of bytes sent on success. + * - HTTPD_SOCK_ERR_INVALID if buf is nullptr. + * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). + * - HTTPD_SOCK_ERR_FAIL for other errors. + */ +int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { + if (buf == nullptr) { + return HTTPD_SOCK_ERR_INVALID; + } + + // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full + int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Buffer full - retry later + return HTTPD_SOCK_ERR_TIMEOUT; + } + // Real error + ESP_LOGD(TAG, "send error: errno %d", errno); + return HTTPD_SOCK_ERR_FAIL; + } + return ret; +} +} // namespace + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -384,6 +424,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * this->hd_ = req->handle; this->fd_.store(httpd_req_to_sockfd(req)); + // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full + httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send); + // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect std::string message = ws->get_config_json(); @@ -459,15 +502,45 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, - event_buffer_.size() - event_bytes_sent_, 0); - if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { - // Socket error - just return, the connection will be closed by httpd - // and our destroy callback will be called + size_t remaining = event_buffer_.size() - event_bytes_sent_; + int bytes_sent = + httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0); + if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) { + // EAGAIN/EWOULDBLOCK - socket buffer full, try again later + // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_() + // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs + // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also + // update the Arduino implementation. + this->consecutive_send_failures_++; + if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { + // Too many failures, connection is likely dead + ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends", + this->consecutive_send_failures_); + this->fd_.store(0); // Mark for cleanup + this->deferred_queue_.clear(); + } return; } + if (bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Real socket error - connection will be closed by httpd and destroy callback will be called + return; + } + if (bytes_sent <= 0) { + // Unexpected error or zero bytes sent + ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent); + return; + } + + // Successful send - reset failure counter + this->consecutive_send_failures_ = 0; event_bytes_sent_ += bytes_sent; + // Log partial sends for debugging + if (event_bytes_sent_ < event_buffer_.size()) { + ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_, + event_buffer_.size()); + } + if (event_bytes_sent_ == event_buffer_.size()) { event_buffer_.resize(0); event_bytes_sent_ = 0; diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 76540ef232..64fda12fda 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -283,6 +283,8 @@ class AsyncEventSourceResponse { std::unique_ptr entities_iterator_; std::string event_buffer_{""}; size_t event_bytes_sent_; + uint16_t consecutive_send_failures_{0}; + static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate }; using AsyncEventSourceClient = AsyncEventSourceResponse; From c0fb0ae06ff076e1c1de3ee8a1d21ea5f34a7f39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:57:59 -0500 Subject: [PATCH 09/13] [web_server_idf] Optimize parameter storage to reduce flash usage and memory overhead (#11003) --- .../web_server_idf/web_server_idf.cpp | 44 +++++++++++++------ .../web_server_idf/web_server_idf.h | 10 ++++- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index c04285402b..60a1b8acbf 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -204,8 +204,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const AsyncWebServerRequest::~AsyncWebServerRequest() { delete this->rsp_; - for (const auto &pair : this->params_) { - delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + for (auto *param : this->params_) { + delete param; // NOLINT(cppcoreguidelines-owning-memory) } } @@ -245,10 +245,22 @@ void AsyncWebServerRequest::redirect(const std::string &url) { } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { - httpd_resp_set_status(*this, code == 200 ? HTTPD_200 - : code == 404 ? HTTPD_404 - : code == 409 ? HTTPD_409 - : to_string(code).c_str()); + // Set status code - use constants for common codes to avoid string allocation + const char *status = nullptr; + switch (code) { + case 200: + status = HTTPD_200; + break; + case 404: + status = HTTPD_404; + break; + case 409: + status = HTTPD_409; + break; + default: + break; + } + httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); @@ -305,11 +317,14 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { #endif AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { - auto find = this->params_.find(name); - if (find != this->params_.end()) { - return find->second; + // Check cache first - only successful lookups are cached + for (auto *param : this->params_) { + if (param->name() == name) { + return param; + } } + // Look up value from query strings optional val = query_key_value(this->post_query_, name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); @@ -318,11 +333,14 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } } - AsyncWebParameter *param = nullptr; - if (val.has_value()) { - param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) + // Don't cache misses to avoid wasting memory when handlers check for + // optional parameters that don't exist in the request + if (!val.has_value()) { + return nullptr; } - this->params_.insert({name, param}); + + auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory) + this->params_.push_back(param); return param; } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 64fda12fda..3d482da0a5 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -30,10 +30,12 @@ using String = std::string; class AsyncWebParameter { public: - AsyncWebParameter(std::string value) : value_(std::move(value)) {} + AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {} + const std::string &name() const { return this->name_; } const std::string &value() const { return this->value_; } protected: + std::string name_; std::string value_; }; @@ -174,7 +176,11 @@ class AsyncWebServerRequest { protected: httpd_req_t *req_; AsyncWebServerResponse *rsp_{}; - std::map params_; + // Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search + // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid + // duplicate storage. Only successful lookups are cached to prevent cache pollution when + // handlers check for optional parameters that don't exist. + std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} From 20d9ae699c18433a397c9962bbc257eac27154a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:59:52 -0500 Subject: [PATCH 10/13] [logger] Conditionally compile runtime tag-specific log levels for performance (#11004) --- esphome/components/logger/__init__.py | 11 +++++++++-- esphome/components/logger/logger.cpp | 10 ++++++++-- esphome/components/logger/logger.h | 15 +++++++++++++-- esphome/core/defines.h | 1 + tests/components/logger/common-default_uart.yaml | 5 +++++ 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 7d1a591f0c..1d02073d27 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -95,6 +95,7 @@ DEFAULT = "DEFAULT" CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" +CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" UART_SELECTION_ESP32 = { @@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_INITIAL_LEVEL): is_log_level, + cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), @@ -291,8 +293,12 @@ async def to_code(config): ) cg.add(log.pre_setup()) - for tag, log_level in config[CONF_LOGS].items(): - cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) + # Enable runtime tag levels if logs are configured or explicitly enabled + logs_config = config[CONF_LOGS] + if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") + for tag, log_level in logs_config.items(): + cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) cg.add_define("USE_LOGGER") this_severity = LOG_LEVEL_SEVERITY.index(level) @@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): level = LOG_LEVELS[config[CONF_LEVEL]] logger = await cg.get_variable(config[CONF_LOGGER_ID]) if tag := config.get(CONF_TAG): + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") text = str(cg.statement(logger.set_log_level(tag, level))) else: text = str(cg.statement(logger.set_log_level(level))) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 4a69bd9853..9a9bf89fe3 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas #endif // USE_STORE_LOG_STR_IN_FLASH inline uint8_t Logger::level_for(const char *tag) { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; +#endif return this->current_level_; } @@ -220,7 +222,9 @@ void Logger::process_messages_() { } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } @@ -271,9 +275,11 @@ void Logger::dump_config() { } #endif +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); } +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index f0e0ed9a27..2099520049 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,6 +36,13 @@ struct device; namespace esphome::logger { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +// Comparison function for const char* keys in log_levels_ map +struct CStrCompare { + bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; } +}; +#endif + // ANSI color code last digit (30-38 range, store only last digit to save RAM) static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { '\0', // NONE @@ -133,8 +140,10 @@ class Logger : public Component { /// Set the default log level for this logger. void set_log_level(uint8_t level); +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, uint8_t log_level); + void set_log_level(const char *tag, uint8_t log_level); +#endif uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== @@ -242,7 +251,9 @@ class Logger : public Component { #endif // Large objects (internally aligned) - std::map log_levels_{}; +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS + std::map log_levels_{}; +#endif CallbackManager log_callback_{}; CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 554e1ee13c..d560007e71 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -48,6 +48,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG #define USE_LVGL_ARC diff --git a/tests/components/logger/common-default_uart.yaml b/tests/components/logger/common-default_uart.yaml index e8b56043eb..7939a5f9c5 100644 --- a/tests/components/logger/common-default_uart.yaml +++ b/tests/components/logger/common-default_uart.yaml @@ -6,11 +6,16 @@ esphome: format: "Warning: Logger level is %d" args: [id(logger_id).get_log_level()] - logger.set_level: WARN + - logger.set_level: + level: ERROR + tag: mqtt.client logger: id: logger_id level: DEBUG initial_level: INFO + logs: + mqtt.component: WARN select: - platform: logger From aa1afbd152cda35f8ab5b5dfda7839eaf68c47b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:02:41 -0500 Subject: [PATCH 11/13] [wifi] Optimize WPA2 EAP phase2 logging to reduce memory overhead (#11005) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8c7b55c274..c86e233059 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,7 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI #include -#include #ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) @@ -42,6 +41,25 @@ namespace wifi { static const char *const TAG = "wifi"; +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { + switch (type) { + case ESP_EAP_TTLS_PHASE2_PAP: + return "pap"; + case ESP_EAP_TTLS_PHASE2_CHAP: + return "chap"; + case ESP_EAP_TTLS_PHASE2_MSCHAP: + return "mschap"; + case ESP_EAP_TTLS_PHASE2_MSCHAPV2: + return "mschapv2"; + case ESP_EAP_TTLS_PHASE2_EAP: + return "eap"; + default: + return "unknown"; + } +} +#endif + float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { @@ -344,15 +362,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); -#ifdef USE_ESP32 -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - std::map phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, - {ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"}, - {ESP_EAP_TTLS_PHASE2_EAP, "eap"}}; - ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str()); -#endif +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2)); #endif bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); From 1b4c5f7976a8a035cfb03cbf0b6bf61213d2c580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:09:12 -0500 Subject: [PATCH 12/13] [light] Reduce flash usage by eliminating duplicate validation code --- esphome/components/light/light_call.cpp | 228 +++++++++++------------- esphome/components/light/light_call.h | 24 ++- 2 files changed, 120 insertions(+), 132 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index cbe9ed0454..361dc58acc 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -10,11 +10,15 @@ namespace light { static const char *const TAG = "light"; // Helper functions to reduce code size for logging -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN -static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) { - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max); +static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f, + float max = 1.0f) { + if (value < min || value > max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max); + value = clamp(value, min, max); + } } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN static void log_feature_not_supported(const char *name, const LogString *feature) { ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature)); } @@ -27,7 +31,6 @@ static void log_invalid_parameter(const char *name, const LogString *message) { ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message)); } #else -#define log_validation_warning(name, param_name, val, min, max) #define log_feature_not_supported(name, feature) #define log_color_mode_not_supported(name, feature) #define log_invalid_parameter(name, message) @@ -44,7 +47,7 @@ static void log_invalid_parameter(const char *name, const LogString *message) { } \ LightCall &LightCall::set_##name(type name) { \ this->name##_ = name; \ - this->set_flag_(flag, true); \ + this->set_flag_(flag); \ return *this; \ } @@ -181,6 +184,16 @@ void LightCall::perform() { } } +void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) { + auto *name = this->parent_->get_name().c_str(); + if (use_color_mode_log) { + log_color_mode_not_supported(name, feature); + } else { + log_feature_not_supported(name, feature); + } + this->clear_flag_(flag); +} + LightColorValues LightCall::validate_() { auto *name = this->parent_->get_name().c_str(); auto traits = this->parent_->get_traits(); @@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() { // Color mode check if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); - this->set_flag_(FLAG_HAS_COLOR_MODE, false); + this->clear_flag_(FLAG_HAS_COLOR_MODE); } // Ensure there is always a color mode set if (!this->has_color_mode()) { this->color_mode_ = this->compute_color_mode_(); - this->set_flag_(FLAG_HAS_COLOR_MODE, true); + this->set_flag_(FLAG_HAS_COLOR_MODE); } auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. this->transform_parameters_(); - // Brightness exists check - if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, LOG_STR("brightness")); - this->set_flag_(FLAG_HAS_BRIGHTNESS, false); - } - - // Transition length possible check - if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, LOG_STR("transitions")); - this->set_flag_(FLAG_HAS_TRANSITION, false); - } - - // Color brightness exists check - if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, LOG_STR("RGB brightness")); - this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); - } - - // RGB exists check - if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || - (this->has_blue() && this->blue_ > 0.0f)) { - if (!(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, LOG_STR("RGB color")); - this->set_flag_(FLAG_HAS_RED, false); - this->set_flag_(FLAG_HAS_GREEN, false); - this->set_flag_(FLAG_HAS_BLUE, false); - } - } - - // White value exists check - if (this->has_white() && this->white_ > 0.0f && - !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, LOG_STR("white value")); - this->set_flag_(FLAG_HAS_WHITE, false); - } - - // Color temperature exists check - if (this->has_color_temperature() && - !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, LOG_STR("color temperature")); - this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); - } - - // Cold/warm white value exists check - if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { - if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); - this->set_flag_(FLAG_HAS_COLD_WHITE, false); - this->set_flag_(FLAG_HAS_WARM_WHITE, false); - } - } - -#define VALIDATE_RANGE_(name_, upper_name, min, max) \ - if (this->has_##name_()) { \ - auto val = this->name_##_; \ - if (val < (min) || val > (max)) { \ - log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \ - this->name_##_ = clamp(val, (min), (max)); \ - } \ - } -#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) - - // Range checks - VALIDATE_RANGE(brightness, "Brightness") - VALIDATE_RANGE(color_brightness, "Color brightness") - VALIDATE_RANGE(red, "Red") - VALIDATE_RANGE(green, "Green") - VALIDATE_RANGE(blue, "Blue") - VALIDATE_RANGE(white, "White") - VALIDATE_RANGE(cold_white, "Cold white") - VALIDATE_RANGE(warm_white, "Warm white") - VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) - + // Business logic adjustments before validation // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. bool explicit_turn_off_request = this->has_state() && !this->state_; // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). if (this->has_brightness() && this->brightness_ == 0.0f) { this->state_ = false; - this->set_flag_(FLAG_HAS_STATE, true); + this->set_flag_(FLAG_HAS_STATE); this->brightness_ = 1.0f; } // Set color brightness to 100% if currently zero and a color is set. - if (this->has_red() || this->has_green() || this->has_blue()) { - if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { - this->color_brightness_ = 1.0f; - this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); - } + if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() && + this->parent_->remote_values.get_color_brightness() == 0.0f) { + this->color_brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS); } - // Create color values for the light with this call applied. + // Capability validation + if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) + this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false); + + // Transition length possible check + if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) + this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); + + if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) + this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true); + + // RGB exists check + if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || + (this->has_blue() && this->blue_ > 0.0f)) && + !(color_mode & ColorCapability::RGB)) { + log_color_mode_not_supported(name, LOG_STR("RGB color")); + this->clear_flag_(FLAG_HAS_RED); + this->clear_flag_(FLAG_HAS_GREEN); + this->clear_flag_(FLAG_HAS_BLUE); + } + + // White value exists check + if (this->has_white() && this->white_ > 0.0f && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) + this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true); + + // Color temperature exists check + if (this->has_color_temperature() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) + this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true); + + // Cold/warm white value exists check + if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) && + !(color_mode & ColorCapability::COLD_WARM_WHITE)) { + log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); + this->clear_flag_(FLAG_HAS_COLD_WHITE); + this->clear_flag_(FLAG_HAS_WARM_WHITE); + } + + // Create color values and validate+apply ranges in one step to eliminate duplicate checks auto v = this->parent_->remote_values; if (this->has_color_mode()) v.set_color_mode(this->color_mode_); if (this->has_state()) v.set_state(this->state_); - if (this->has_brightness()) - v.set_brightness(this->brightness_); - if (this->has_color_brightness()) - v.set_color_brightness(this->color_brightness_); - if (this->has_red()) - v.set_red(this->red_); - if (this->has_green()) - v.set_green(this->green_); - if (this->has_blue()) - v.set_blue(this->blue_); - if (this->has_white()) - v.set_white(this->white_); - if (this->has_color_temperature()) - v.set_color_temperature(this->color_temperature_); - if (this->has_cold_white()) - v.set_cold_white(this->cold_white_); - if (this->has_warm_white()) - v.set_warm_white(this->warm_white_); + +#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ + if (this->has_##field()) { \ + clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ + v.setter(this->field##_); \ + } + + VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") + VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") + VALIDATE_AND_APPLY(red, set_red, "Red") + VALIDATE_AND_APPLY(green, set_green, "Green") + VALIDATE_AND_APPLY(blue, set_blue, "Blue") + VALIDATE_AND_APPLY(white, set_white, "White") + VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") + VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") + VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), + traits.get_max_mireds()) + +#undef VALIDATE_AND_APPLY v.normalize_color(); // Flash length check if (this->has_flash_() && this->flash_length_ == 0) { - log_invalid_parameter(name, LOG_STR("flash length must be greater than zero")); - this->set_flag_(FLAG_HAS_FLASH, false); + log_invalid_parameter(name, LOG_STR("flash length must be >= zero")); + this->clear_flag_(FLAG_HAS_FLASH); } // validate transition length/flash length/effect not used at the same time @@ -330,42 +310,40 @@ LightColorValues LightCall::validate_() { // If effect is already active, remove effect start if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } // validate effect index if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); - this->set_flag_(FLAG_HAS_TRANSITION, false); - this->set_flag_(FLAG_HAS_FLASH, false); + this->clear_flag_(FLAG_HAS_TRANSITION); + this->clear_flag_(FLAG_HAS_FLASH); } if (this->has_flash_() && this->has_transition_()) { log_invalid_parameter(name, LOG_STR("flash cannot be used with transition")); - this->set_flag_(FLAG_HAS_TRANSITION, false); + this->clear_flag_(FLAG_HAS_TRANSITION); } if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) && supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; - this->set_flag_(FLAG_HAS_TRANSITION, true); + this->set_flag_(FLAG_HAS_TRANSITION); } if (this->has_transition_() && this->transition_length_ == 0) { // 0 transition is interpreted as no transition (instant change) - this->set_flag_(FLAG_HAS_TRANSITION, false); + this->clear_flag_(FLAG_HAS_TRANSITION); } - if (this->has_transition_() && !supports_transition) { - log_feature_not_supported(name, LOG_STR("transitions")); - this->set_flag_(FLAG_HAS_TRANSITION, false); - } + if (this->has_transition_() && !supports_transition) + this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); // If not a flash and turning the light off, then disable the light // Do not use light color values directly, so that effects can set 0% brightness @@ -374,17 +352,17 @@ LightColorValues LightCall::validate_() { if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect this->effect_ = 0; - this->set_flag_(FLAG_HAS_EFFECT, true); + this->set_flag_(FLAG_HAS_EFFECT); } } // Disable saving for flashes if (this->has_flash_()) - this->set_flag_(FLAG_SAVE, false); + this->clear_flag_(FLAG_SAVE); return v; } @@ -418,12 +396,12 @@ void LightCall::transform_parameters_() { const float gamma = this->parent_->get_gamma_correct(); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); - this->set_flag_(FLAG_HAS_COLD_WHITE, true); - this->set_flag_(FLAG_HAS_WARM_WHITE, true); + this->set_flag_(FLAG_HAS_COLD_WHITE); + this->set_flag_(FLAG_HAS_WARM_WHITE); } if (this->has_white()) { this->brightness_ = this->white_; - this->set_flag_(FLAG_HAS_BRIGHTNESS, true); + this->set_flag_(FLAG_HAS_BRIGHTNESS); } } } @@ -630,7 +608,7 @@ LightCall &LightCall::set_effect(optional effect) { } LightCall &LightCall::set_effect(uint32_t effect_number) { this->effect_ = effect_number; - this->set_flag_(FLAG_HAS_EFFECT, true); + this->set_flag_(FLAG_HAS_EFFECT); return *this; } LightCall &LightCall::set_effect(optional effect_number) { diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 7e04e1a767..d3a526b136 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -4,6 +4,10 @@ #include namespace esphome { + +// Forward declaration +struct LogString; + namespace light { class LightState; @@ -207,14 +211,14 @@ class LightCall { FLAG_SAVE = 1 << 15, }; - bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } - bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } - bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } - bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } - bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } + inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } + inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } + inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } + inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } + inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } - // Helper to set flag - void set_flag_(FieldFlags flag, bool value) { + // Helper to set flag - defaults to true for common case + void set_flag_(FieldFlags flag, bool value = true) { if (value) { this->flags_ |= flag; } else { @@ -222,6 +226,12 @@ class LightCall { } } + // Helper to clear flag - reduces code size for common case + void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; } + + // Helper to log unsupported feature and clear flag - reduces code duplication + void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log); + LightState *parent_; // Light state values - use flags_ to check if a value has been set. From 6b87187c663984c0f02ee91c70585c51b0651f2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 17:00:32 -0500 Subject: [PATCH 13/13] [esp32_ble_server] Optimize manufacturer_data storage to reduce memory overhead --- esphome/components/esp32_ble/ble.cpp | 4 ++++ esphome/components/esp32_ble/ble.h | 1 + .../components/esp32_ble/ble_advertising.cpp | 4 ++++ .../components/esp32_ble/ble_advertising.h | 1 + .../esp32_ble_server/ble_server.cpp | 3 ++- .../components/esp32_ble_server/ble_server.h | 23 ++++++++++++------- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 64cef70de2..cec92a2285 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -68,6 +68,10 @@ void ESP32BLE::advertising_set_service_data(const std::vector &data) { } void ESP32BLE::advertising_set_manufacturer_data(const std::vector &data) { + this->advertising_set_manufacturer_data(std::span(data)); +} + +void ESP32BLE::advertising_set_manufacturer_data(std::span data) { this->advertising_init_(); this->advertising_->set_manufacturer_data(data); this->advertising_start(); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 1aa3bc86ef..b49e5d12ee 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -118,6 +118,7 @@ class ESP32BLE : public Component { void advertising_start(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); + void advertising_set_manufacturer_data(std::span data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_service_data_and_name(std::span data, bool include_name); void advertising_add_service_uuid(ESPBTUUID uuid); diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index df70768c23..3bc0fabe7e 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -59,6 +59,10 @@ void BLEAdvertising::set_service_data(const std::vector &data) { } void BLEAdvertising::set_manufacturer_data(const std::vector &data) { + this->set_manufacturer_data(std::span(data)); +} + +void BLEAdvertising::set_manufacturer_data(std::span data) { delete[] this->advertising_data_.p_manufacturer_data; this->advertising_data_.p_manufacturer_data = nullptr; this->advertising_data_.manufacturer_len = data.size(); diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 7a31d926f6..70d58d5ce9 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -35,6 +35,7 @@ class BLEAdvertising { void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_manufacturer_data(const std::vector &data); + void set_manufacturer_data(std::span data); void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_service_data(const std::vector &data); void set_service_data(std::span data); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 25cc97eeaf..0e3a3b4a49 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -99,7 +99,8 @@ bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_ void BLEServer::restart_advertising_() { if (this->is_running()) { - this->parent_->advertising_set_manufacturer_data(this->manufacturer_data_); + this->parent_->advertising_set_manufacturer_data( + std::span(this->manufacturer_data_.get(), this->manufacturer_data_length_)); } } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 6fa86dd67f..788ad377ef 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -35,7 +35,11 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv bool is_running(); void set_manufacturer_data(const std::vector &data) { - this->manufacturer_data_ = data; + this->manufacturer_data_length_ = data.size(); + this->manufacturer_data_.reset(data.empty() ? nullptr : new uint8_t[data.size()]); + if (!data.empty()) { + memcpy(this->manufacturer_data_.get(), data.data(), data.size()); + } this->restart_advertising_(); } @@ -87,24 +91,27 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void remove_client_(uint16_t conn_id); void dispatch_callbacks_(CallbackType type, uint16_t conn_id); + // 4-byte aligned (pointers and vectors on 32-bit) std::vector callbacks_; - - std::vector manufacturer_data_{}; - esp_gatt_if_t gatts_if_{0}; - bool registered_{false}; - - uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; - uint8_t client_count_{0}; std::vector services_{}; std::vector services_to_start_{}; + std::unique_ptr manufacturer_data_{}; BLEService *device_information_service_{}; + // 2-byte aligned + uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; + + // 1-byte aligned + uint8_t manufacturer_data_length_{0}; + uint8_t client_count_{0}; + esp_gatt_if_t gatts_if_{0}; enum State : uint8_t { INIT = 0x00, REGISTERING, STARTING_SERVICE, RUNNING, } state_{INIT}; + bool registered_{false}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)