From 34067f8b15b7159e8391c9905a301e110d610e94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:29:15 -1000 Subject: [PATCH 1/7] [esp8266] Native OTA backend to reduce Arduino dependencies (#12675) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp8266/__init__.py | 8 +- .../esp8266/exclude_updater.py.script | 21 ++ .../components/esphome/ota/ota_esphome.cpp | 2 +- .../http_request/ota/ota_http_request.cpp | 2 +- esphome/components/ota/__init__.py | 2 +- .../ota/ota_backend_arduino_esp8266.cpp | 89 ----- .../ota/ota_backend_arduino_esp8266.h | 33 -- .../components/ota/ota_backend_esp8266.cpp | 356 ++++++++++++++++++ esphome/components/ota/ota_backend_esp8266.h | 58 +++ .../web_server/ota/ota_web_server.cpp | 7 +- 10 files changed, 446 insertions(+), 132 deletions(-) create mode 100644 esphome/components/esp8266/exclude_updater.py.script delete mode 100644 esphome/components/ota/ota_backend_arduino_esp8266.cpp delete mode 100644 esphome/components/ota/ota_backend_arduino_esp8266.h create mode 100644 esphome/components/ota/ota_backend_esp8266.cpp create mode 100644 esphome/components/ota/ota_backend_esp8266.h diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index a74f9ee8ce..c4969a79b2 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -191,7 +191,8 @@ async def to_code(config): cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option( - "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] + "extra_scripts", + ["pre:testing_mode.py", "pre:exclude_updater.py", "post:post_build.py"], ) conf = config[CONF_FRAMEWORK] @@ -278,3 +279,8 @@ def copy_files(): testing_mode_file, CORE.relative_build_path("testing_mode.py"), ) + exclude_updater_file = dir / "exclude_updater.py.script" + copy_file_if_changed( + exclude_updater_file, + CORE.relative_build_path("exclude_updater.py"), + ) diff --git a/esphome/components/esp8266/exclude_updater.py.script b/esphome/components/esp8266/exclude_updater.py.script new file mode 100644 index 0000000000..69331e3b03 --- /dev/null +++ b/esphome/components/esp8266/exclude_updater.py.script @@ -0,0 +1,21 @@ +# pylint: disable=E0602 +Import("env") # noqa + +import os + +# Filter out Updater.cpp from the Arduino core build +# This saves 228 bytes of .bss by not instantiating the global Update object +# ESPHome uses its own native OTA backend instead + + +def filter_updater_from_core(env, node): + """Filter callback to exclude Updater.cpp from framework build.""" + path = node.get_path() + if path.endswith("Updater.cpp"): + print(f"ESPHome: Excluding {os.path.basename(path)} from build (using native OTA backend)") + return None + return node + + +# Apply the filter to framework sources +env.AddBuildMiddleware(filter_updater_from_core, "**/cores/esp8266/Updater.cpp") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b589a6119f..f9984e1425 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -10,7 +10,7 @@ #endif #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_libretiny.h" #include "esphome/components/ota/ota_backend_arduino_rp2040.h" #include "esphome/components/ota/ota_backend_esp_idf.h" diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 058579752e..2cd7489e38 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -7,7 +7,7 @@ #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" #include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_rp2040.h" #include "esphome/components/ota/ota_backend_esp_idf.h" diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 8bed9cee42..a514a7482f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -148,7 +148,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "ota_backend_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, "ota_backend_arduino_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp deleted file mode 100644 index 375c4e7200..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend_arduino_esp8266.h" -#include "ota_backend.h" - -#include "esphome/components/esp8266/preferences.h" -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ota { - -static const char *const TAG = "ota.arduino_esp8266"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { - // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space - if (image_size == 0) { - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; - } - bool ret = Update.begin(image_size, U_FLASH); - if (ret) { - esp8266::preferences_prevent_write(true); - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_BOOTSTRAP) - return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; - if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; - if (error == UPDATE_ERROR_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; - if (error == UPDATE_ERROR_SPACE) - return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { - Update.setMD5(md5); - this->md5_set_ = true; -} - -OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { - size_t written = Update.write(data, len); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoESP8266OTABackend::end() { - // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 - // This matches the behavior of the old web_server OTA implementation - bool success = Update.end(!this->md5_set_); - - // On ESP8266, Update.end() might return false even with error code 0 - // Check the actual error code to determine success - uint8_t error = Update.getError(); - - if (success || error == UPDATE_ERROR_OK) { - return OTA_RESPONSE_OK; - } - - ESP_LOGE(TAG, "End error: %d", error); - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoESP8266OTABackend::abort() { - Update.end(); - esp8266::preferences_prevent_write(false); -} - -} // namespace ota -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h deleted file mode 100644 index e1b9015cc7..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace ota { - -class ArduinoESP8266OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - bool supports_compression() override { return true; } -#else - bool supports_compression() override { return false; } -#endif - - private: - bool md5_set_{false}; -}; - -} // namespace ota -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp new file mode 100644 index 0000000000..4b84708cd9 --- /dev/null +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -0,0 +1,356 @@ +#ifdef USE_ESP8266 +#include "ota_backend_esp8266.h" +#include "ota_backend.h" + +#include "esphome/components/esp8266/preferences.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include + +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +// Note: FLASH_SECTOR_SIZE (0x1000) is already defined in spi_flash_geometry.h + +// Flash header offsets +static constexpr uint8_t FLASH_MODE_OFFSET = 2; + +// Firmware magic bytes +static constexpr uint8_t FIRMWARE_MAGIC = 0xE9; +static constexpr uint8_t GZIP_MAGIC_1 = 0x1F; +static constexpr uint8_t GZIP_MAGIC_2 = 0x8B; + +// ESP8266 flash memory base address (memory-mapped flash starts here) +static constexpr uint32_t FLASH_BASE_ADDRESS = 0x40200000; + +// Boot mode extraction from GPI register (bits 16-19 contain boot mode) +static constexpr int BOOT_MODE_SHIFT = 16; +static constexpr int BOOT_MODE_MASK = 0xf; + +// Boot mode indicating UART download mode (OTA not possible) +static constexpr int BOOT_MODE_UART_DOWNLOAD = 1; + +// Minimum buffer size when memory is constrained +static constexpr size_t MIN_BUFFER_SIZE = 256; + +namespace esphome::ota { + +static const char *const TAG = "ota.esp8266"; + +std::unique_ptr make_ota_backend() { return make_unique(); } + +OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space + if (image_size == 0) { + // Round down to sector boundary: subtract one sector, then mask to sector alignment + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + image_size = (ESP.getFreeSketchSpace() - FLASH_SECTOR_SIZE) & ~(FLASH_SECTOR_SIZE - 1); + } + + // Check boot mode - if boot mode is UART download mode, + // we will not be able to reset into normal mode once update is done + int boot_mode = (GPI >> BOOT_MODE_SHIFT) & BOOT_MODE_MASK; + if (boot_mode == BOOT_MODE_UART_DOWNLOAD) { + return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; + } + + // Check flash configuration - real size must be >= configured size + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (!ESP.checkFlashConfig(false)) { + return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; + } + + // Get current sketch size + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + uint32_t sketch_size = ESP.getSketchSize(); + + // Size of current sketch rounded to sector boundary + uint32_t current_sketch_size = (sketch_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); + + // Size of update rounded to sector boundary + uint32_t rounded_size = (image_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); + + // End of available space for sketch and update (start of filesystem) + uint32_t update_end_address = FS_start - FLASH_BASE_ADDRESS; + + // Calculate start address for the update (write from end backwards) + this->start_address_ = (update_end_address > rounded_size) ? (update_end_address - rounded_size) : 0; + + // Check if there's enough space for both current sketch and update + if (this->start_address_ < current_sketch_size) { + return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; + } + + // Allocate buffer for sector writes (use smaller buffer if memory constrained) + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + this->buffer_size_ = (ESP.getFreeHeap() > 2 * FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : MIN_BUFFER_SIZE; + + // ESP8266's umm_malloc guarantees 4-byte aligned allocations, which is required + // for spi_flash_write(). This is the same pattern used by Arduino's Updater class. + this->buffer_ = make_unique(this->buffer_size_); + if (!this->buffer_) { + return OTA_RESPONSE_ERROR_UNKNOWN; + } + + this->current_address_ = this->start_address_; + this->image_size_ = image_size; + this->buffer_len_ = 0; + this->md5_set_ = false; + + // Disable WiFi sleep during update + wifi_set_sleep_type(NONE_SLEEP_T); + + // Prevent preference writes during update + esp8266::preferences_prevent_write(true); + + // Initialize MD5 computation + this->md5_.init(); + + ESP_LOGD(TAG, "OTA begin: start=0x%08" PRIX32 ", size=%zu", this->start_address_, image_size); + + return OTA_RESPONSE_OK; +} + +void ESP8266OTABackend::set_update_md5(const char *md5) { + // Parse hex string to bytes + if (parse_hex(md5, this->expected_md5_, 16)) { + this->md5_set_ = true; + } +} + +OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) { + if (!this->buffer_) { + return OTA_RESPONSE_ERROR_UNKNOWN; + } + + size_t written = 0; + while (written < len) { + // Calculate how much we can buffer + size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_); + memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer); + this->buffer_len_ += to_buffer; + written += to_buffer; + + // If buffer is full, write to flash + if (this->buffer_len_ == this->buffer_size_ && !this->write_buffer_()) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + } + + return OTA_RESPONSE_OK; +} + +bool ESP8266OTABackend::erase_sector_if_needed_() { + if ((this->current_address_ % FLASH_SECTOR_SIZE) != 0) { + return true; // Not at sector boundary + } + + App.feed_wdt(); + if (spi_flash_erase_sector(this->current_address_ / FLASH_SECTOR_SIZE) != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Flash erase failed at 0x%08" PRIX32, this->current_address_); + return false; + } + return true; +} + +bool ESP8266OTABackend::flash_write_() { + App.feed_wdt(); + if (spi_flash_write(this->current_address_, reinterpret_cast(this->buffer_.get()), this->buffer_len_) != + SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Flash write failed at 0x%08" PRIX32, this->current_address_); + return false; + } + return true; +} + +bool ESP8266OTABackend::write_buffer_() { + if (this->buffer_len_ == 0) { + return true; + } + + if (!this->erase_sector_if_needed_()) { + return false; + } + + // Patch flash mode in first sector if needed + // This is analogous to what esptool.py does when it receives a --flash_mode argument + bool is_first_sector = (this->current_address_ == this->start_address_); + uint8_t original_flash_mode = 0; + bool patched_flash_mode = false; + + // Only patch if we have enough bytes to access flash mode offset and it's not GZIP + if (is_first_sector && this->buffer_len_ > FLASH_MODE_OFFSET && this->buffer_[0] != GZIP_MAGIC_1) { + // Not GZIP compressed - check and patch flash mode + uint8_t current_flash_mode = this->get_flash_chip_mode_(); + uint8_t buffer_flash_mode = this->buffer_[FLASH_MODE_OFFSET]; + + if (buffer_flash_mode != current_flash_mode) { + original_flash_mode = buffer_flash_mode; + this->buffer_[FLASH_MODE_OFFSET] = current_flash_mode; + patched_flash_mode = true; + } + } + + if (!this->flash_write_()) { + return false; + } + + // Restore original flash mode for MD5 calculation + if (patched_flash_mode) { + this->buffer_[FLASH_MODE_OFFSET] = original_flash_mode; + } + + // Update MD5 with original (unpatched) data + this->md5_.add(this->buffer_.get(), this->buffer_len_); + + this->current_address_ += this->buffer_len_; + this->buffer_len_ = 0; + + return true; +} + +bool ESP8266OTABackend::write_buffer_final_() { + // Similar to write_buffer_(), but without flash mode patching or MD5 update (for final padded write) + if (this->buffer_len_ == 0) { + return true; + } + + if (!this->erase_sector_if_needed_() || !this->flash_write_()) { + return false; + } + + this->current_address_ += this->buffer_len_; + this->buffer_len_ = 0; + + return true; +} + +OTAResponseTypes ESP8266OTABackend::end() { + // Write any remaining buffered data + if (this->buffer_len_ > 0) { + // Add actual data to MD5 before padding + this->md5_.add(this->buffer_.get(), this->buffer_len_); + + // Pad to 4-byte alignment for flash write + while (this->buffer_len_ % 4 != 0) { + this->buffer_[this->buffer_len_++] = 0xFF; + } + if (!this->write_buffer_final_()) { + this->abort(); + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + } + + // Calculate actual bytes written + size_t actual_size = this->current_address_ - this->start_address_; + + // Check if any data was written + if (actual_size == 0) { + ESP_LOGE(TAG, "No data written"); + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // Verify MD5 if set (strict mode), otherwise use lenient mode + // In lenient mode (no MD5), we accept whatever was written + if (this->md5_set_) { + this->md5_.calculate(); + if (!this->md5_.equals_bytes(this->expected_md5_)) { + ESP_LOGE(TAG, "MD5 mismatch"); + this->abort(); + return OTA_RESPONSE_ERROR_MD5_MISMATCH; + } + } else { + // Lenient mode: adjust size to what was actually written + // This matches Arduino's Update.end(true) behavior + this->image_size_ = actual_size; + } + + // Verify firmware header + if (!this->verify_end_()) { + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // Write eboot command to copy firmware on next boot + eboot_command ebcmd; + ebcmd.action = ACTION_COPY_RAW; + ebcmd.args[0] = this->start_address_; + ebcmd.args[1] = 0x00000; // Destination: start of flash + ebcmd.args[2] = this->image_size_; + eboot_command_write(&ebcmd); + + ESP_LOGI(TAG, "OTA update staged: 0x%08" PRIX32 " -> 0x00000, size=%zu", this->start_address_, this->image_size_); + + // Clean up + this->buffer_.reset(); + esp8266::preferences_prevent_write(false); + + return OTA_RESPONSE_OK; +} + +void ESP8266OTABackend::abort() { + this->buffer_.reset(); + this->buffer_len_ = 0; + this->image_size_ = 0; + esp8266::preferences_prevent_write(false); +} + +bool ESP8266OTABackend::verify_end_() { + uint32_t buf; + if (spi_flash_read(this->start_address_, &buf, 4) != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Failed to read firmware header"); + return false; + } + + uint8_t *bytes = reinterpret_cast(&buf); + + // Check for GZIP (compressed firmware) + if (bytes[0] == GZIP_MAGIC_1 && bytes[1] == GZIP_MAGIC_2) { + // GZIP compressed - can't verify further + return true; + } + + // Check firmware magic byte + if (bytes[0] != FIRMWARE_MAGIC) { + ESP_LOGE(TAG, "Invalid firmware magic: 0x%02X (expected 0x%02X)", bytes[0], FIRMWARE_MAGIC); + return false; + } + +#if !FLASH_MAP_SUPPORT + // Check if new firmware's flash size fits (only when auto-detection is disabled) + // With FLASH_MAP_SUPPORT (modern cores), flash size is auto-detected from chip + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + uint32_t bin_flash_size = ESP.magicFlashChipSize((bytes[3] & 0xf0) >> 4); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (bin_flash_size > ESP.getFlashChipRealSize()) { + ESP_LOGE(TAG, "Firmware flash size (%" PRIu32 ") exceeds chip size (%" PRIu32 ")", bin_flash_size, + ESP.getFlashChipRealSize()); + return false; + } +#endif + + return true; +} + +uint8_t ESP8266OTABackend::get_flash_chip_mode_() { + uint32_t data; + if (spi_flash_read(0x0000, &data, 4) != SPI_FLASH_RESULT_OK) { + return 0; // Default to QIO + } + return (reinterpret_cast(&data))[FLASH_MODE_OFFSET]; +} + +} // namespace esphome::ota +#endif // USE_ESP8266 diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h new file mode 100644 index 0000000000..a9d6dd2ccc --- /dev/null +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -0,0 +1,58 @@ +#pragma once +#ifdef USE_ESP8266 +#include "ota_backend.h" + +#include "esphome/components/md5/md5.h" +#include "esphome/core/defines.h" + +#include + +namespace esphome::ota { + +/// OTA backend for ESP8266 using native SDK functions. +/// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM +/// by not having a global Update object in .bss. +class ESP8266OTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; + // Compression supported in all ESP8266 Arduino versions ESPHome supports (>= 2.7.0) + bool supports_compression() override { return true; } + + protected: + /// Erase flash sector if current address is at sector boundary + bool erase_sector_if_needed_(); + + /// Write buffer to flash (does not update address or clear buffer) + bool flash_write_(); + + /// Write buffered data to flash and update MD5 + bool write_buffer_(); + + /// Write buffered data to flash without MD5 update (for final padded write) + bool write_buffer_final_(); + + /// Verify the firmware header is valid + bool verify_end_(); + + /// Get current flash chip mode from flash header + uint8_t get_flash_chip_mode_(); + + std::unique_ptr buffer_; + size_t buffer_size_{0}; + size_t buffer_len_{0}; + + uint32_t start_address_{0}; + uint32_t current_address_{0}; + size_t image_size_{0}; + + md5::MD5Digest md5_{}; + uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest + bool md5_set_{false}; +}; + +} // namespace esphome::ota +#endif // USE_ESP8266 diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 572c351245..b8bea40b84 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -10,9 +10,7 @@ #endif #ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include -#elif defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) #include #endif #endif // USE_ARDUINO @@ -120,9 +118,6 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf // Platform-specific pre-initialization #ifdef USE_ARDUINO -#ifdef USE_ESP8266 - Update.runAsync(true); -#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) if (Update.isRunning()) { Update.abort(); From e9f2d75aab3a8e31f82e82e30ac7d0bc811e617d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:34:45 -1000 Subject: [PATCH 2/7] [core] Add format_hex_to helper for zero-allocation hex formatting (#12670) --- esphome/components/one_wire/one_wire_bus.cpp | 6 ++++-- esphome/components/sx126x/sx126x.cpp | 4 +++- esphome/components/sx127x/sx127x.cpp | 4 +++- esphome/core/helpers.cpp | 13 +++++++++++++ esphome/core/helpers.h | 18 ++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index c2542177cf..27b7d58a0f 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -49,7 +49,8 @@ void OneWireBus::search() { break; auto *address8 = reinterpret_cast(&address); if (crc8(address8, 7) != address8[7]) { - ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str()); + char hex_buf[17]; + ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex_to(hex_buf, address)); } else { this->devices_.push_back(address); } @@ -82,8 +83,9 @@ void OneWireBus::dump_devices_(const char *tag) { ESP_LOGW(tag, " Found no devices!"); } else { ESP_LOGCONFIG(tag, " Found devices:"); + char hex_buf[17]; // uint64_t = 16 hex chars + null for (auto &address : this->devices_) { - ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex(address).c_str(), LOG_STR_ARG(get_model_str(address & 0xff))); + ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex_to(hex_buf, address), LOG_STR_ARG(get_model_str(address & 0xff))); } } } diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index bb59f26b79..707d6f1fbf 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -527,7 +527,9 @@ void SX126x::dump_config() { this->spreading_factor_, cr, this->preamble_size_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->is_failed()) { ESP_LOGE(TAG, "Configuring SX126x failed"); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 8e6db5dc9e..3185574b1a 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -476,7 +476,9 @@ void SX127x::dump_config() { ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) { ESP_LOGCONFIG(TAG, diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index d7d32ea28f..5e361ecce2 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -297,6 +297,19 @@ std::string format_hex(const uint8_t *data, size_t length) { } std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + size_t max_bytes = (buffer_size - 1) / 2; + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + buffer[2 * i] = format_hex_char(data[i] >> 4); + buffer[2 * i + 1] = format_hex_char(data[i] & 0x0F); + } + buffer[length * 2] = '\0'; + return buffer; +} + // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 48a2313e2c..4319e32510 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -730,6 +730,24 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { output[12] = '\0'; } +/// Format byte array as lowercase hex to buffer (base implementation). +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); + +/// Format byte array as lowercase hex to buffer. Automatically deduces buffer size. +/// Truncates output if data exceeds buffer capacity. Returns pointer to buffer. +template inline char *format_hex_to(char (&buffer)[N], const uint8_t *data, size_t length) { + static_assert(N >= 3, "Buffer must hold at least one hex byte (3 chars)"); + return format_hex_to(buffer, N, data, length); +} + +/// Format an unsigned integer in lowercased hex to buffer, starting with the most significant byte. +template::value, int> = 0> +inline char *format_hex_to(char (&buffer)[N], T val) { + static_assert(N >= sizeof(T) * 2 + 1, "Buffer too small for type"); + val = convert_big_endian(val); + return format_hex_to(buffer, reinterpret_cast(&val), sizeof(T)); +} + /// Format the six-byte array \p mac into a MAC address. std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. From a275f37135ae249196691d813b93abc5bcf60dd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:35:16 -1000 Subject: [PATCH 3/7] [udp] Use stack buffer for listen address logging in dump_config (#12667) --- esphome/components/udp/udp_component.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 9105ced21e..daa6c52f98 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -130,7 +130,8 @@ void UDPComponent::dump_config() { for (const auto &address : this->addresses_) ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); if (this->listen_address_.has_value()) { - ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str()); + char addr_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf)); } ESP_LOGCONFIG(TAG, " Broadcasting: %s\n" From be0bf1e5b92a736d0b7bdc6ae95f60a68e92e2ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:35:36 -1000 Subject: [PATCH 4/7] [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) --- esphome/components/lvgl/defines.py | 10 +++------- esphome/components/lvgl/lv_validation.py | 11 +++-------- esphome/components/lvgl/lvcode.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1d528b2f73..91077a1ff4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging -from typing import TYPE_CHECKING, Any +from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS @@ -96,13 +96,9 @@ class LValidator: return None if isinstance(value, Lambda): # Local import to avoid circular import - from .lvcode import CodeContext, LambdaContext + from .lvcode import get_lambda_context_args - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 9c1dd22085..947e44b131 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any +from typing import Any import esphome.codegen as cg from esphome.components import image @@ -404,14 +404,9 @@ class TextValidator(LValidator): self, value: Any, args: list[tuple[SafeExpType, str]] | None = None ) -> Expression: # Local import to avoid circular import at module level + from .lvcode import get_lambda_context_args - from .lvcode import CodeContext, LambdaContext - - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index e2c70642a8..b79d1e88dd 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,4 +1,5 @@ import abc +from typing import TYPE_CHECKING from esphome import codegen as cg from esphome.config import Config @@ -200,6 +201,21 @@ class LvContext(LambdaContext): return self.add(*args) +def get_lambda_context_args() -> list[tuple[SafeExpType, str]]: + """Get automation parameters from the current lambda context if available. + + When called from outside LVGL's context (e.g., from interval), + CodeContext.code_context will be None, so return empty args. + """ + if CodeContext.code_context is None: + return [] + if TYPE_CHECKING: + # CodeContext base class doesn't define get_automation_parameters(), + # but LambdaContext and LvContext (the concrete implementations) do. + assert isinstance(CodeContext.code_context, LambdaContext) + return CodeContext.code_context.get_automation_parameters() + + class LocalVariable(MockObj): """ Create a local variable and enclose the code using it within a block. From f243e609a51b96771269b37e206e4aaaec811930 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:35:58 -1000 Subject: [PATCH 5/7] [wifi] Use StringRef and std::span in WiFiConnectStateListener to avoid allocations (#12672) --- esphome/components/wifi/wifi_component.h | 4 +++- esphome/components/wifi/wifi_component_esp8266.cpp | 5 +++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++-- esphome/components/wifi/wifi_component_libretiny.cpp | 5 +++-- esphome/components/wifi/wifi_component_pico_w.cpp | 7 +++++-- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 6 +++--- esphome/components/wifi_info/wifi_info_text_sensor.h | 6 ++++-- esphome/components/wifi_signal/wifi_signal_sensor.h | 4 +++- 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 604efa8a7e..4f888292f1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -6,7 +6,9 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" +#include #include #include @@ -274,7 +276,7 @@ class WiFiScanResultsListener { */ class WiFiConnectStateListener { public: - virtual void on_wifi_connect_state(const std::string &ssid, const bssid_t &bssid) = 0; + virtual void on_wifi_connect_state(StringRef ssid, std::span bssid) = 0; }; /** Listener interface for WiFi power save mode changes. diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 550b5579ff..598ae2d5b7 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -526,7 +526,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -559,8 +559,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connected = false; s_sta_connecting = false; #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif break; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 212514af93..67314ae31f 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -737,7 +737,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -772,8 +772,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = true; #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 340537b228..2aa6fa3484 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -303,7 +303,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -357,8 +357,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ s_sta_connecting = false; #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif break; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 61709852ff..b755b8544f 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -256,8 +256,10 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_LISTENERS + String ssid = WiFi.SSID(); + bssid_t bssid = this->wifi_bssid(); for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); } // For static IP configurations, notify IP listeners immediately as the IP is already configured #ifdef USE_WIFI_MANUAL_IP @@ -275,8 +277,9 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index eae0f87b40..0cca3e16ef 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -103,8 +103,8 @@ void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_list void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void SSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { - this->publish_state(ssid); +void SSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { + this->publish_state(ssid.str()); } /**************** @@ -115,7 +115,7 @@ void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_lis void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void BSSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { +void BSSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { char buf[18] = "unknown"; if (mac_address_is_valid(bssid.data())) { format_mac_addr_upper(bssid.data(), buf); diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index b2242372da..6beb1372f5 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -2,10 +2,12 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI #include +#include namespace esphome::wifi_info { @@ -52,7 +54,7 @@ class SSIDWiFiInfo final : public Component, public text_sensor::TextSensor, pub void dump_config() override; // WiFiConnectStateListener interface - void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; + void on_wifi_connect_state(StringRef ssid, std::span bssid) override; }; class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { @@ -61,7 +63,7 @@ class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, pu void dump_config() override; // WiFiConnectStateListener interface - void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; + void on_wifi_connect_state(StringRef ssid, std::span bssid) override; }; class PowerSaveModeWiFiInfo final : public Component, diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index 9f581f1eb2..2e1f8cbb2b 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -2,9 +2,11 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI +#include namespace esphome::wifi_signal { #ifdef USE_WIFI_LISTENERS @@ -28,7 +30,7 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { #ifdef USE_WIFI_LISTENERS // WiFiConnectStateListener interface - update RSSI immediately on connect - void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override { this->update(); } + void on_wifi_connect_state(StringRef ssid, std::span bssid) override { this->update(); } #endif }; From a6097f4a0f7fd8229d12fdd61bf627b08aad7aff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:36:19 -1000 Subject: [PATCH 6/7] [wifi] Eliminate heap allocations in dump_config logging (#12664) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 16 ++++++++++++---- esphome/components/wifi/wifi_component.h | 6 ++++++ .../components/wifi/wifi_component_esp8266.cpp | 12 ++++++++++++ .../components/wifi/wifi_component_esp_idf.cpp | 13 +++++++++++++ .../components/wifi/wifi_component_libretiny.cpp | 8 ++++++++ .../components/wifi/wifi_component_pico_w.cpp | 8 ++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5fa894d8f9..50c0938cf1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -899,12 +899,20 @@ void WiFiComponent::print_connect_params_() { ESP_LOGCONFIG(TAG, " Disabled"); return; } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { - ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf)); } } int8_t rssi = wifi_rssi(); + // Use stack buffers for SSID and all IP addresses to avoid heap allocations + char ssid_buf[SSID_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'") "\n" " BSSID: " LOG_SECRET("%s") "\n" @@ -915,9 +923,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), - get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), - wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf), + wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf)); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid())); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4f888292f1..ff2bfe12a4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -61,6 +61,9 @@ namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; +/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) +static constexpr size_t SSID_BUFFER_SIZE = 33; + struct SavedWifiSettings { char ssid[33]; char password[65]; @@ -408,6 +411,9 @@ class WiFiComponent : public Component { network::IPAddresses wifi_sta_ip_addresses(); std::string wifi_ssid(); + /// Write SSID to buffer without heap allocation. + /// Returns pointer to buffer, or empty string if not connected. + const char *wifi_ssid_to(std::span buffer); bssid_t wifi_bssid(); int8_t wifi_rssi(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 598ae2d5b7..1c744648bb 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -913,6 +913,18 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + struct station_config conf {}; + if (!wifi_station_get_config(&conf)) { + buffer[0] = '\0'; + return buffer.data(); + } + // conf.ssid is uint8[32], not null-terminated if full + size_t len = strnlen(reinterpret_cast(conf.ssid), sizeof(conf.ssid)); + memcpy(buffer.data(), conf.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { if (WiFi.status() != WL_CONNECTED) return WIFI_RSSI_DISCONNECTED; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 67314ae31f..b26ac3d2e2 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1086,6 +1086,19 @@ std::string WiFiComponent::wifi_ssid() { size_t len = strnlen(ssid_s, sizeof(info.ssid)); return {ssid_s, len}; } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + wifi_ap_record_t info{}; + esp_err_t err = esp_wifi_sta_get_ap_info(&info); + if (err != ESP_OK) { + buffer[0] = '\0'; + return buffer.data(); + } + // info.ssid is uint8[33], but only 32 bytes are SSID data + size_t len = strnlen(reinterpret_cast(info.ssid), 32); + memcpy(buffer.data(), info.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2aa6fa3484..9b8653d0db 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -554,6 +554,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct LibreTiny API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index b755b8544f..1aa737ff4a 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -214,6 +214,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct CYW43 API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } From 5e99dd14ae78a9c0a59f4595a91af945430c3ed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:36:35 -1000 Subject: [PATCH 7/7] [ethernet] Eliminate heap allocations in dump_config logging (#12665) --- .../ethernet/ethernet_component.cpp | 28 +++++++++++++------ .../components/ethernet/ethernet_component.h | 2 ++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 793ebdec42..114000401f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -644,6 +644,12 @@ void EthernetComponent::dump_connect_params_() { dns_ip2 = dns_getserver(1); } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " IP Address: %s\n" " Hostname: '%s'\n" @@ -651,9 +657,9 @@ void EthernetComponent::dump_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - network::IPAddress(&ip.ip).str().c_str(), App.get_name().c_str(), - network::IPAddress(&ip.netmask).str().c_str(), network::IPAddress(&ip.gw).str().c_str(), - network::IPAddress(dns_ip1).str().c_str(), network::IPAddress(dns_ip2).str().c_str()); + network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf)); #if USE_NETWORK_IPV6 struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -665,12 +671,13 @@ void EthernetComponent::dump_connect_params_() { } #endif /* USE_NETWORK_IPV6 */ + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " MAC Address: %s\n" " Is Full Duplex: %s\n" " Link Speed: %u", - this->get_eth_mac_address_pretty().c_str(), YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), - this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } #ifdef USE_ETHERNET_SPI @@ -711,11 +718,16 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { } std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span buf) { uint8_t mac[6]; get_eth_mac_address_raw(mac); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); } eth_duplex_t EthernetComponent::get_duplex_mode() { diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index bffed4dc4a..490a9d026e 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #ifdef USE_ESP32 @@ -93,6 +94,7 @@ class EthernetComponent : public Component { void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); + const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); bool powerdown();