From db92c0a7669166a6f5a772748e53c25958f0a3ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 00:07:40 -0500 Subject: [PATCH] Fix backport IDF compile, restore erase watchdog and error mappings --- .../components/ota/ota_backend_esp_idf.cpp | 33 ++++++++++++++++--- esphome/components/ota/ota_backend_esp_idf.h | 2 +- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 2c7e522924..57840b3203 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -2,10 +2,12 @@ #include "ota_backend_esp_idf.h" #include "esphome/components/md5/md5.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" #include +#include #include #include @@ -70,12 +72,23 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) esp_err_t err; #ifdef OTA_BLOCK_ERASE_AHEAD this->erased_end_ = 0; +#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE + // esp_ota_begin() refuses to start while the running app is unconfirmed; + // esp_ota_resume() does not, so keep that behavior here. + esp_ota_img_states_t running_state; + if (esp_ota_get_state_partition(esp_ota_get_running_partition(), &running_state) == ESP_OK && + running_state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGE(TAG, "Running app has not confirmed state (ESP_OTA_IMG_PENDING_VERIFY)"); + return OTA_RESPONSE_ERROR_UNKNOWN; + } +#endif // erase_size 0 (!= OTA_WITH_SEQUENTIAL_WRITES) means no erase; erase_ahead_() handles it err = esp_ota_resume(this->partition_, 0, 0, &this->update_handle_); -#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE +#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // esp_ota_begin() does this on IDF 5.5+; esp_ota_resume() does not. Prevents + // booting a half-written slot after a crash mid-OTA. Not available on the + // 5.3.3/5.4.2 backports, whose esp_ota_begin() did not invalidate either. if (err == ESP_OK) { - // esp_ota_begin() does this; esp_ota_resume() does not. Prevents booting a - // half-written slot after a crash mid-OTA. esp_ota_invalidate_inactive_ota_data_slot(); } #endif @@ -84,7 +97,7 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) #endif if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err); + ESP_LOGE(TAG, "OTA begin failed (err=0x%X)", err); esp_ota_abort(this->update_handle_); this->update_handle_ = 0; if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { @@ -145,6 +158,9 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err); if (err == ESP_ERR_OTA_VALIDATE_FAILED) { return OTA_RESPONSE_ERROR_MAGIC; + } else if (err == ESP_ERR_INVALID_SIZE) { + // Sequential-writes fallback: IDF's lazy erase reports overflow here + return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; } @@ -165,10 +181,13 @@ OTAResponseTypes IDFOTABackend::erase_ahead_(size_t len) { // range into 64 KiB block erases where aligned, sector erases elsewhere. const size_t erase_to = std::min(this->partition_->size, (end + BLOCK_ERASE_SIZE - 1) & ~(BLOCK_ERASE_SIZE - 1)); + // A block erase is one uninterruptible flash op (typically ~150 ms, seconds + // on aged flash) and the transfer loop may not have fed the WDT for ~1s. + watchdog::WatchdogManager watchdog(15000); esp_err_t err = esp_partition_erase_range(this->partition_, this->erased_end_, erase_to - this->erased_end_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_partition_erase_range failed (err=0x%X)", err); - return OTA_RESPONSE_ERROR_WRITING_FLASH; + return err == ESP_ERR_INVALID_SIZE ? OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE : OTA_RESPONSE_ERROR_WRITING_FLASH; } this->erased_end_ = erase_to; return OTA_RESPONSE_OK; @@ -266,6 +285,10 @@ void IDFOTABackend::abort() { // or not an update is in flight. esp_ota_abort(this->update_handle_); this->update_handle_ = 0; + this->written_ = 0; +#ifdef OTA_BLOCK_ERASE_AHEAD + this->erased_end_ = 0; +#endif } } // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index ac9032019f..323e020761 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -75,7 +75,7 @@ class IDFOTABackend final { // Keep md5_ first since its digest_ is alignas(32) on DMA-SHA variants; md5_set_ stays last so buf_ packs tightly. md5::MD5Digest md5_{}; esp_ota_handle_t update_handle_{0}; - const esp_partition_t *partition_; + const esp_partition_t *partition_{nullptr}; size_t written_{0}; // Bytes handed to esp_ota_write() #ifdef OTA_BLOCK_ERASE_AHEAD size_t erased_end_{0}; // Erased up to this partition offset; must stay >= written_