From 7ea51b1865eb2f85abcd1dcdd3345d548655924d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:17:28 -0500 Subject: [PATCH 1/4] [esphome.ota] Fix ESP32-S3 OTA authentication with hardware SHA acceleration (#11011) --- .../components/esphome/ota/ota_esphome.cpp | 156 +++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 2 - esphome/components/sha256/sha256.cpp | 33 ++++ esphome/components/sha256/sha256.h | 4 + esphome/core/hash_base.h | 2 +- 5 files changed, 119 insertions(+), 78 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f1506f066c..b65bfc5ab8 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } - // Generate nonce with appropriate hasher - bool success = false; + // Generate nonce - hasher must be created and used in same stack frame + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: + // 1. Hash objects must NEVER be passed to another function (different stack frame) + // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA + // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created + // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption. + // + // Buffer layout after AUTH_READ completes: + // [0]: auth_type (1 byte) + // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND + // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce + // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash + + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; + #ifdef USE_OTA_SHA256 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - sha256::SHA256 sha_hasher; - success = this->prepare_auth_nonce_(&sha_hasher); + hasher = &sha_hasher; } #endif #ifdef USE_OTA_MD5 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - md5::MD5Digest md5_hasher; - success = this->prepare_auth_nonce_(&md5_hasher); + hasher = &md5_hasher; } #endif - if (!success) { + const size_t hex_size = hasher->get_size() * 2; + const size_t nonce_len = hasher->get_size() / 4; + const size_t auth_buf_size = 1 + 3 * hex_size; + this->auth_buf_ = std::make_unique(auth_buf_size); + this->auth_buf_pos_ = 0; + + char *buf = reinterpret_cast(this->auth_buf_.get() + 1); + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { + this->log_auth_warning_(LOG_STR("Random failed")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); return false; } + + hasher->init(); + hasher->add(buf, nonce_len); + hasher->calculate(); + this->auth_buf_[0] = this->auth_type_; + hasher->get_hex(buf); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too + memcpy(log_buf, buf, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); +#endif } // Try to write auth_type + nonce @@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() { } // We have all the data, verify it - bool matches = false; + const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); + const char *cnonce = nonce + hex_size; + const char *response = cnonce + hex_size; + + // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions). + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; #ifdef USE_OTA_SHA256 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - sha256::SHA256 sha_hasher; - matches = this->verify_hash_auth_(&sha_hasher, hex_size); + hasher = &sha_hasher; } #endif #ifdef USE_OTA_MD5 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - md5::MD5Digest md5_hasher; - matches = this->verify_hash_auth_(&md5_hasher, hex_size); + hasher = &md5_hasher; } #endif - if (!matches) { - this->log_auth_warning_(LOG_STR("Password mismatch")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; - } - - // Authentication successful - clean up auth state - this->cleanup_auth_(); - - return true; -} - -bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { - // Calculate required buffer size using the hasher - const size_t hex_size = hasher->get_size() * 2; - const size_t nonce_len = hasher->get_size() / 4; - - // Buffer layout after AUTH_READ completes: - // [0]: auth_type (1 byte) - // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND - // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce - // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - // Total: 1 + 3*hex_size - const size_t auth_buf_size = 1 + 3 * hex_size; - this->auth_buf_ = std::make_unique(auth_buf_size); - this->auth_buf_pos_ = 0; - - // Generate nonce - char *buf = reinterpret_cast(this->auth_buf_.get() + 1); - if (!random_bytes(reinterpret_cast(buf), nonce_len)) { - this->log_auth_warning_(LOG_STR("Random failed")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); - return false; - } - - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); - - // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) - this->auth_buf_[0] = this->auth_type_; - hasher->get_hex(buf); - -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; - // Log nonce for debugging - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); -#endif - - return true; -} - -bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { - // Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout) - const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte - const char *cnonce = nonce + hex_size; // CNonce immediately follows nonce - const char *response = cnonce + hex_size; // Response immediately follows cnonce - - // Calculate expected hash: password + nonce + cnonce hasher->init(); hasher->add(this->password_.c_str(), this->password_.length()); hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too // Log CNonce memcpy(log_buf, cnonce, hex_size); log_buf[hex_size] = '\0'; @@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { #endif // Compare response - return hasher->equals_hex(response); + bool matches = hasher->equals_hex(response); + + if (!matches) { + this->log_auth_warning_(LOG_STR("Password mismatch")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + + // Authentication successful - clean up auth state + this->cleanup_auth_(); + + return true; } size_t ESPHomeOTAComponent::get_auth_hex_size_() const { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 1e26494fd0..d4a8410d35 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool handle_auth_send_(); bool handle_auth_read_(); bool select_auth_type_(); - bool prepare_auth_nonce_(HashBase *hasher); - bool verify_hash_auth_(HashBase *hasher, size_t hex_size); size_t get_auth_hex_size_() const; void cleanup_auth_(); void log_auth_warning_(const LogString *msg); diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 199460acbc..32abbd739d 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -10,6 +10,39 @@ namespace esphome::sha256 { #if defined(USE_ESP32) || defined(USE_LIBRETINY) +// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: +// +// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains +// internal state that the DMA engine references. This imposes two critical constraints: +// +// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to +// write to incorrect memory locations. This results in null pointer dereferences and crashes. +// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]). +// +// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same +// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack +// frame changes (function call/return), the DMA references become invalid and will produce +// truncated hash output (20 bytes instead of 32) or corrupt memory. +// +// CORRECT USAGE: +// void my_function() { +// sha256::SHA256 hasher; // Created locally +// hasher.init(); +// hasher.add(data, len); // Any size, no chunking needed +// hasher.calculate(); +// bool ok = hasher.equals_hex(expected); +// // hasher destroyed when function returns +// } +// +// INCORRECT USAGE (WILL FAIL ON ESP32-S3): +// void my_function() { +// sha256::SHA256 hasher; +// helper(&hasher); // WRONG: Passed to different stack frame +// } +// void helper(HashBase *h) { +// h->init(); // WRONG: Will produce truncated/corrupted output +// } + SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); } void SHA256::init() { diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index bb089bc314..a2b62799e1 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -39,6 +39,10 @@ class SHA256 : public esphome::HashBase { protected: #if defined(USE_ESP32) || defined(USE_LIBRETINY) + // CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration. + // The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another + // function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce + // truncated/incorrect hash results. mbedtls_sha256_context ctx_{}; #elif defined(USE_ESP8266) || defined(USE_RP2040) br_sha256_context ctx_{}; diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index 4eb6a89f53..c45c4df70b 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -39,7 +39,7 @@ class HashBase { /// Compare the hash against a provided hex-encoded hash bool equals_hex(const char *expected) { - uint8_t parsed[this->get_size()]; + uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes) if (!parse_hex(expected, parsed, this->get_size())) { return false; } From e2c5eeef975fea9f814a191c2cc10fbe35698f68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:32:51 -0500 Subject: [PATCH 2/4] [scheduler] Deduplicate item removal code with template helper (#11017) --- esphome/core/scheduler.cpp | 26 +++++--------------------- esphome/core/scheduler.h | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 71e2a00fbe..402084f306 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -118,7 +118,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->type = type; item->callback = std::move(func); // Initialize remove to false (though it should already be from constructor) - // Not using mark_item_removed_ helper since we're setting to false, not true #ifdef ESPHOME_THREAD_MULTI_ATOMICS item->remove.store(false, std::memory_order_relaxed); #else @@ -600,12 +599,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - } - } + total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -620,23 +614,13 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - this->to_remove_++; // Track removals for heap items - } - } + size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + total_cancelled += heap_cancelled; + this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - for (auto &item : this->to_add_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - // Don't track removals for to_add_ items - } - } + total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 885ee13754..2237915e07 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -280,19 +280,30 @@ class Scheduler { #endif } - // Helper to mark item for removal (platform-specific) + // Helper to mark matching items in a container as removed + // Returns the number of items marked for removal // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. - void mark_item_removed_(SchedulerItem *item) { + template + size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { + size_t count = 0; + for (auto &item : container) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + // Mark item for removal (platform-specific) #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic store - item->remove.store(true, std::memory_order_release); + // Multi-threaded with atomics: use atomic store + item->remove.store(true, std::memory_order_release); #else - // Single-threaded (ESPHOME_THREAD_SINGLE) or - // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write - // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! - item->remove = true; + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = true; #endif + count++; + } + } + return count; } // Template helper to check if any item in a container matches our criteria From c6e4a7911c110711c89fe8f1eb035a442b8919aa Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:10:23 -0400 Subject: [PATCH 3/4] [esp32] Improve version handling (#10899) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 248 ++++++++++----------------- script/generate-esp32-boards.py | 3 +- 2 files changed, 93 insertions(+), 158 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f5eda52cae..3fbbf68c71 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" -def _format_framework_espidf_version( - ver: cv.Version, release: str, for_platformio: bool -) -> str: - # format the given arduino (https://github.com/espressif/esp-idf/releases) version to +def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: + # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf - if for_platformio: - return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" if release: return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" @@ -317,157 +312,108 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) -# The platform-espressif32 version to use for arduino frameworks -# - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ARDUINO_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(3, 2, 1), + "latest": cv.Version(3, 3, 1), + "dev": cv.Version(3, 3, 1), +} +ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 1): cv.Version(55, 3, 31), + cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), + cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), + cv.Version(3, 2, 0): cv.Version(54, 3, 20), + cv.Version(3, 1, 3): cv.Version(53, 3, 13), + cv.Version(3, 1, 2): cv.Version(53, 3, 12), + cv.Version(3, 1, 1): cv.Version(53, 3, 11), + cv.Version(3, 1, 0): cv.Version(53, 3, 10), +} # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases -# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) -# The platformio/espressif32 version to use for esp-idf frameworks -# - https://github.com/platformio/platform-espressif32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(5, 4, 2), + "latest": cv.Version(5, 5, 1), + "dev": cv.Version(5, 5, 1), +} +ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 1): cv.Version(55, 3, 31), + cv.Version(5, 5, 0): cv.Version(55, 3, 31), + cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), + cv.Version(5, 3, 2): cv.Version(53, 3, 13), + cv.Version(5, 3, 1): cv.Version(53, 3, 13), + cv.Version(5, 3, 0): cv.Version(53, 3, 13), + cv.Version(5, 1, 6): cv.Version(51, 3, 7), + cv.Version(5, 1, 5): cv.Version(51, 3, 7), +} -# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions -SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 2, 2), - cv.Version(5, 2, 1), - cv.Version(5, 1, 2), - cv.Version(5, 1, 1), - cv.Version(5, 1, 0), - cv.Version(5, 0, 2), - cv.Version(5, 0, 1), - cv.Version(5, 0, 0), -] - -# pioarduino versions that don't require a release number -# List based on https://github.com/pioarduino/esp-idf/releases -SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ - cv.Version(5, 5, 1), - cv.Version(5, 5, 0), - cv.Version(5, 4, 2), - cv.Version(5, 4, 1), - cv.Version(5, 4, 0), - cv.Version(5, 3, 3), - cv.Version(5, 3, 2), - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 1, 5), - cv.Version(5, 1, 6), -] +# The platform-espressif32 version +# - https://github.com/pioarduino/platform-espressif32/releases +PLATFORM_VERSION_LOOKUP = { + "recommended": cv.Version(54, 3, 21, "2"), + "latest": cv.Version(55, 3, 31), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", +} def _check_versions(value): value = value.copy() - if value[CONF_TYPE] == FRAMEWORK_ARDUINO: - lookups = { - "dev": ( - cv.Version(3, 2, 1), - "https://github.com/espressif/arduino-esp32.git", - ), - "latest": (cv.Version(3, 2, 1), None), - "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), - } - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: - raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." - ) - - version, source = lookups[value[CONF_VERSION]] - else: - version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, - _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)), - ) - - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}" - - if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: - _LOGGER.warning( - "The selected Arduino framework version is not the recommended one. " - "If there are connectivity or build issues please remove the manual version." - ) - - return value - - lookups = { - "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 2, 2), None), - "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), - } - - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: + if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: + if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." + "Version needs to be explicitly set when a custom source or platform_version is used." ) - version, source = lookups[value[CONF_VERSION]] + platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]] + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] + else: + version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] else: version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - if version < cv.Version(5, 0, 0): - raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") - - # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below - has_platform_ver = CONF_PLATFORM_VERSION in value - - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) - ) - - if ( - is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION]) - ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X: - raise cv.Invalid( - f"ESP-IDF {str(version)} not supported by platformio/espressif32" - ) - - if ( - version in SUPPORTED_PLATFORMIO_ESP_IDF_5X - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ) and not has_platform_ver: - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'" - ) - - if ( - not is_platformio - and CONF_RELEASE not in value - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ): - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'" - ) value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_espidf_version( - version, value.get(CONF_RELEASE, None), is_platformio - ) - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}" + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + if version < cv.Version(3, 0, 0): + raise cv.Invalid("Only Arduino 3.0+ is supported.") + recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, _format_framework_arduino_version(version) + ) + else: + if version < cv.Version(5, 0, 0): + raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") + recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, + _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + ) - if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + if CONF_PLATFORM_VERSION not in value: + if platform_lookup is None: + raise cv.Invalid( + "Framework version not recognized; please specify platform_version" + ) + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if version != recommended_version: _LOGGER.warning( - "The selected ESP-IDF framework version is not the recommended one. " + "The selected framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." + ) + + if value[CONF_PLATFORM_VERSION] != _parse_platform_version( + str(PLATFORM_VERSION_LOOKUP["recommended"]) + ): + _LOGGER.warning( + "The selected platform version is not the recommended one. " "If there are connectivity or build issues please remove the manual version." ) @@ -477,26 +423,14 @@ def _check_versions(value): def _parse_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) - if ver.major >= 50: # a pioarduino version - release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" - if ver.extra: - release += f"-{ver.extra}" - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" - # if platform version is a valid version constraint, prefix the default package - cv.platformio_version_constraint(value) - return f"platformio/espressif32@{value}" + release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + release += f"-{ver.extra}" + return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" except cv.Invalid: return value -def _platform_is_platformio(value): - try: - ver = cv.Version.parse(cv.version_number(value)) - return ver.major < 50 - except cv.Invalid: - return "platformio" in value - - def _detect_variant(value): board = value.get(CONF_BOARD) variant = value.get(CONF_VARIANT) @@ -808,6 +742,8 @@ async def to_code(config): conf = config[CONF_FRAMEWORK] cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + if CONF_SOURCE in conf: + cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") @@ -850,8 +786,6 @@ async def to_code(config): cg.add_build_flag("-Wno-nonnull-compare") - cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) - add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 152a480d23..81b78b04be 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -7,9 +7,10 @@ import subprocess import sys import tempfile -from esphome.components.esp32 import ESP_IDF_PLATFORM_VERSION as ver +from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP from esphome.helpers import write_file_if_changed +ver = PLATFORM_VERSION_LOOKUP["recommended"] version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" root = Path(__file__).parent.parent boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py" From f26e71bae6d1f1df2b064ea93e36883a0385efc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 17:16:09 -0500 Subject: [PATCH 4/4] [ci] Fix clang-tidy after Arduino-as-IDF-component migration (#11031) --- .clang-tidy.hash | 2 +- esphome/components/ethernet/ethernet_component.h | 2 +- platformio.ini | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index f61b79de4d..a6222ebd64 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 +bc4001761441a1f0d32971287398739b2b1c45435440425f5ed5aacf1c1f8c2b diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c7cb0abb4c..6b4e342df5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -163,7 +163,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif diff --git a/platformio.ini b/platformio.ini index d97607fac5..a1cc1b3a49 100644 --- a/platformio.ini +++ b/platformio.ini @@ -129,7 +129,7 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip -framework = arduino +framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first @@ -274,6 +274,7 @@ build_unflags = [env:esp32-arduino-tidy] extends = common:esp32-arduino board = esp32dev +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-arduino-tidy build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags}