diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index f018429984..88cb60675d 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -184,6 +184,23 @@ static const esp_partition_t *find_app_partition_at(uint32_t address, size_t min return found; } +// RAII helper for the destructive section of update_partition_table(). nvs_flash_deinit() is +// called immediately before the first partition-table write so that earlier failure paths leave +// NVS functional; this guard re-initializes NVS on every early return past that point so any +// component still running after a failed OTA can keep using NVS. The success path disarms the +// guard before returning because the device reboots immediately afterwards and reinit would only +// churn the partition cache. +namespace { +struct NvsReinitGuard { + bool armed{true}; + ~NvsReinitGuard() { + if (armed) { + nvs_flash_init(); + } + } +}; +} // namespace + // Validate the new partition table image staged in ``buf_`` and pick the slot the running app // will boot from after the update. Performs all non-destructive checks; the destructive write // is in ``update_partition_table()``. Side-effect: registers the live partition-table region @@ -375,10 +392,10 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { // Deinitialize NVS just before the first destructive write to the partition-table region. Doing // this here (instead of earlier) means that any failure path in the verify or copy phases above // returns with NVS still functional, so other components on the device aren't broken until reboot. - // Each failure path past this point calls nvs_flash_init() before returning so that, if the - // device keeps running, components that depend on NVS aren't permanently broken. The success - // path skips reinit because the device reboots immediately afterwards. + // The RAII guard re-initializes NVS on every early-return below; the success path disarms it + // immediately before returning, since the device reboots right after. nvs_flash_deinit(); + NvsReinitGuard nvs_guard; // Update the partition table err = esp_ota_begin(this->partition_table_part_, ESP_PARTITION_TABLE_MAX_LEN, &this->update_handle_); @@ -386,7 +403,6 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { esp_ota_abort(this->update_handle_); this->update_handle_ = 0; ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err); - nvs_flash_init(); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } err = esp_ota_write(this->update_handle_, this->buf_, ESP_PARTITION_TABLE_MAX_LEN); @@ -396,14 +412,12 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { esp_ota_abort(this->update_handle_); this->update_handle_ = 0; ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err); - nvs_flash_init(); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } err = esp_ota_end(this->update_handle_); this->update_handle_ = 0; // esp_ota_end releases the handle internally regardless of result if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err); - nvs_flash_init(); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } // esp_partition_unload_all() invalidates every cached partition entry, including the externally @@ -418,16 +432,15 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { const esp_partition_t *new_boot_partition = find_app_partition_at(new_part->pos.offset, 0); if (new_boot_partition == nullptr) { ESP_LOGE(TAG, "Selected app partition not found after partition table update"); - nvs_flash_init(); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address); err = esp_ota_set_boot_partition(new_boot_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err); - nvs_flash_init(); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } + nvs_guard.armed = false; return OTA_RESPONSE_OK; }