diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 84d6fa3c0e..00089c000d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -101,13 +101,8 @@ void ESPHomeOTAComponent::dump_config() { } #endif #ifdef USE_OTA_PARTITIONS - // Avoid running esp_image_verify here: it reads and checksums the entire app image, which is too - // expensive for a config dump. The address comes from a cached lookup; the precise used size is - // computed lazily by update_partition_table() the first time a partition-table OTA is requested. - // Guard against esp_ota_get_running_partition() returning nullptr (can happen after the partition - // cache has been unloaded) so dump_config never crashes. - // Single ESP_LOGCONFIG call so the lines stay together as one log message; on the (rare) - // nullptr path we surface zeros rather than dereferencing. + // running_app_part can be nullptr if the partition cache was unloaded by a prior aborted + // partition-table OTA; surface zeros instead of dereferencing. const esp_partition_t *running_app_part = esp_ota_get_running_partition(); ESP_LOGCONFIG(TAG, " Partition access allowed\n" @@ -397,12 +392,8 @@ void ESPHomeOTAComponent::handle_data_() { this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif -#ifdef USE_OTA_PARTITIONS + // begin() may block for a few seconds while it locks flash. error_code = this->backend_->begin(ota_size, ota_type); -#else - // This will block for a few seconds as it locks flash - error_code = this->backend_->begin(ota_size); -#endif if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) update_started = true; diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index dcd71e92dd..4cc99202a7 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -13,7 +13,10 @@ static const char *const TAG = "ota.arduino_libretiny"; std::unique_ptr make_ota_backend() { return make_unique(); } -OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { +OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size, OTAType ota_type) { + if (ota_type != OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA // where the exact firmware size is unknown due to multipart encoding if (image_size == 0) { diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 3d426e6759..c2716a44d1 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -8,7 +8,7 @@ namespace esphome::ota { class ArduinoLibreTinyOTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index bc8ef812e6..0ca0602519 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -15,7 +15,10 @@ static const char *const TAG = "ota.arduino_rp2040"; std::unique_ptr make_ota_backend() { return make_unique(); } -OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { +OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size, OTAType ota_type) { + if (ota_type != OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } // OTA size of 0 is not currently handled, but // web_server is not supported for RP2040, so this is not an issue. bool ret = Update.begin(image_size, U_FLASH); diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 05bd2f5cc4..d04d5c1a84 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -10,7 +10,7 @@ namespace esphome::ota { class ArduinoRP2040OTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 7c9d392532..6a678fb419 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -50,7 +50,10 @@ static const char *const TAG = "ota.esp8266"; std::unique_ptr make_ota_backend() { return make_unique(); } -OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { +OTAResponseTypes ESP8266OTABackend::begin(size_t image_size, OTAType ota_type) { + if (ota_type != OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } // 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 diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index b364e216a3..21b5c12c2d 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -14,7 +14,7 @@ namespace esphome::ota { /// by not having a global Update object in .bss. class ESP8266OTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index e84d8762a1..42d106bf1f 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -16,14 +16,12 @@ static const char *const TAG = "ota.idf"; std::unique_ptr make_ota_backend() { return make_unique(); } -#ifdef USE_OTA_PARTITIONS OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) { +#ifdef USE_OTA_PARTITIONS this->ota_type_ = ota_type; if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { - // Partition table images produced by gen_esp32part.py are padded with 0xFF and an MD5 entry to - // exactly ESP_PARTITION_TABLE_MAX_LEN bytes. Reject anything else: an undersized image would - // leave trailing bytes from the previous table in place after the partial write, and an - // oversized image cannot fit in the reserved region. This is stricter than verify alone. + // Reject any size other than ESP_PARTITION_TABLE_MAX_LEN: under- leaves stale bytes from the + // previous table; over- can't fit the reserved region. if (image_size != ESP_PARTITION_TABLE_MAX_LEN) { ESP_LOGE(TAG, "Wrong partition table size: expected %u bytes, got %zu", ESP_PARTITION_TABLE_MAX_LEN, image_size); return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; @@ -38,7 +36,9 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; } #else -OTAResponseTypes IDFOTABackend::begin(size_t image_size) { + if (ota_type != ota::OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } #endif #ifdef USE_OTA_ROLLBACK // If we're starting an OTA, the current boot is good enough - mark it valid @@ -148,10 +148,8 @@ void IDFOTABackend::abort() { this->partition_table_part_ = nullptr; } #endif - // Always tear down any open OTA handle. update_partition_table() opens a handle internally to - // write the new partition table; if esp_ota_write/esp_ota_end fail mid-flight, the handle must - // be released here so it isn't leaked. esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG - // harmlessly, so the unconditional call is safe whether or not we're mid-update. + // esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG harmlessly, so this is safe whether + // or not an update is in flight. esp_ota_abort(this->update_handle_); this->update_handle_ = 0; } diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 59712945f2..5e45b6d016 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -10,9 +10,7 @@ namespace esphome::ota { #ifdef USE_OTA_PARTITIONS -// Dedicated staging buffer size for the new partition table image. Must be at least -// ESP_PARTITION_TABLE_MAX_LEN (0xC00) so the entire partition table fits before verification. -// Kept separate from any OTA chunk-transfer buffer to avoid coupling unrelated sizes. +// Staging buffer holds the entire partition table for verification before any flash op. static constexpr size_t PARTITION_TABLE_BUFFER_SIZE = ESP_PARTITION_TABLE_MAX_LEN; // 0xC00 void get_running_app_position(uint32_t &offset, size_t &size); @@ -20,11 +18,7 @@ void get_running_app_position(uint32_t &offset, size_t &size); class IDFOTABackend final { public: -#ifdef USE_OTA_PARTITIONS OTAResponseTypes begin(size_t image_size, ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP); -#else - OTAResponseTypes begin(size_t image_size); -#endif void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); @@ -33,10 +27,8 @@ class IDFOTABackend final { protected: #ifdef USE_OTA_PARTITIONS - // Outcome of validating an incoming partition-table image. ``target_app_index`` is the - // entry in the new table that the running app will boot from after the update; - // ``copy_source_part`` is non-null when the running app must be copied into that slot - // first (the source is the matching slot in the *current* partition table). + // copy_source_part non-null means the running app must be copied from this slot in the current + // table into target_app_index in the new table before the table is committed. struct PartitionTablePlan { int target_app_index{-1}; const esp_partition_t *copy_source_part{nullptr}; @@ -54,11 +46,9 @@ class IDFOTABackend final { char expected_bin_md5_[32]; bool md5_set_{false}; #ifdef USE_OTA_PARTITIONS - // Place the byte buffer first so it sits immediately after the preceding `bool md5_set_`, - // eliminating the 3-byte alignment padding that an int-sized member would otherwise force. - // Remaining members are 4-byte-aligned and pack tightly after the buffer. The backend is - // constructed on each incoming OTA connection and destroyed on cleanup_connection_(), so this - // 3 KiB is only resident during an active OTA, not permanently. + // Buffer first so it packs tightly after the preceding `bool md5_set_` with no alignment + // padding. Only resident during an active OTA: the backend is constructed per connection and + // destroyed on cleanup_connection_(). uint8_t buf_[PARTITION_TABLE_BUFFER_SIZE]; size_t buf_written_{0}; size_t image_size_{0}; diff --git a/esphome/components/ota/ota_backend_host.cpp b/esphome/components/ota/ota_backend_host.cpp index 2e2132418d..a2c9f2cc33 100644 --- a/esphome/components/ota/ota_backend_host.cpp +++ b/esphome/components/ota/ota_backend_host.cpp @@ -10,7 +10,9 @@ namespace esphome::ota { std::unique_ptr make_ota_backend() { return make_unique(); } -OTAResponseTypes HostOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UPDATE_PREPARE; } +OTAResponseTypes HostOTABackend::begin(size_t image_size, OTAType ota_type) { + return OTA_RESPONSE_ERROR_UPDATE_PREPARE; +} void HostOTABackend::set_update_md5(const char *expected_md5) {} diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index 300facf72f..4451fdfe18 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -9,7 +9,7 @@ namespace esphome::ota { /// OTA triggers to compile for host platform during development. class HostOTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp index f912916c73..910c63e8d7 100644 --- a/esphome/components/ota/ota_partitions_esp_idf.cpp +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -21,9 +21,8 @@ static inline bool check_overlap(uint32_t a_offset, size_t a_size, uint32_t b_of return (a_offset + a_size > b_offset && b_offset + b_size > a_offset); } -// Find the first registered APP partition whose address matches `address` and whose size is at least -// `min_size`. Returns nullptr when no match exists. Encapsulates the iterator + release pattern so -// callers don't have to repeat (and correctly handle) the find/get/next/release dance. +// Wraps esp_partition_find/_get/_next/_release. Returns nullptr if no APP partition at `address` +// is at least `min_size` bytes. static const esp_partition_t *find_app_partition_at(uint32_t address, size_t min_size) { const esp_partition_t *found = nullptr; esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); @@ -39,12 +38,8 @@ 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. +// Re-inits NVS unless disarmed. Used so failure paths past nvs_flash_deinit() leave NVS usable +// for any component still running after a failed OTA. namespace { struct NvsReinitGuard { bool armed{true}; @@ -56,14 +51,12 @@ struct NvsReinitGuard { }; } // 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 -// as ``partition_table_part_`` so the caller can write to it; ``abort()`` releases it on error. +// Validates the staged partition table and picks the post-update boot slot. All non-destructive +// checks live here; the destructive write is in update_partition_table(). +// Side effect: registers the live partition-table region as partition_table_part_ so the caller +// can write to it; abort() releases it on error. OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size, PartitionTablePlan &plan) { - // Register the live primary partition table as an external partition so we can mmap it for - // verification and later issue esp_ota_begin/esp_ota_write against it. esp_err_t err = esp_partition_register_external( nullptr, ESP_PRIMARY_PARTITION_TABLE_OFFSET, ESP_PARTITION_TABLE_SIZE, "PrimaryPrtTable", ESP_PARTITION_TYPE_PARTITION_TABLE, ESP_PARTITION_SUBTYPE_PARTITION_TABLE_PRIMARY, &this->partition_table_part_); @@ -72,7 +65,6 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - // Verify existing partition table int num_partitions = 0; const esp_partition_info_t *existing_partition_table = nullptr; esp_partition_mmap_handle_t partition_table_map; @@ -89,8 +81,6 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - // Verify new partition table. esp_partition_table_verify expects ESP_PARTITION_TABLE_MAX_LEN - // bytes; ``buf_`` is sized to that exactly. const esp_partition_info_t *new_partition_table = reinterpret_cast(this->buf_); err = esp_partition_table_verify(new_partition_table, true, &num_partitions); if (err != ESP_OK) { @@ -98,8 +88,8 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - // Check for missing checksum entry. esp_partition_table_verify does not fail in this case and - // the ESP would not boot after the update. + // esp_partition_table_verify does not catch a missing MD5 entry, but the bootloader refuses + // to boot from a table without one. bool checksum_found = false; for (size_t i = 0; i < ESP_PARTITION_TABLE_MAX_ENTRIES; i++) { if (new_partition_table[i].magic == ESP_PARTITION_MAGIC_MD5) { @@ -112,11 +102,9 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - // Walk the new table once, populating: the chosen target app slot, presence of otadata/nvs, - // and otadata-vs-running-app overlap. Selection policy when multiple app slots can host the - // running app: pick the FIRST eligible slot in table order. The no-copy path (offsets already - // match) is preferred over the copy path; within each path we lock in the first match and stop - // searching. This keeps the choice deterministic and table-ordering-stable. + // Slot-selection policy when multiple slots can host the running app: pick the FIRST eligible + // slot in table order, preferring the no-copy path (matching offset) over the copy path. + // Deterministic and table-ordering-stable. int app_partitions_found = 0; int new_app_part_index = -1; int new_app_part_index_with_copy = -1; @@ -130,13 +118,12 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a app_partitions_found++; if (new_part->pos.size >= running_app_size) { if (new_part->pos.offset == running_app_offset) { - // No-copy path: same offset as running app, first match wins. if (new_app_part_index == -1) { new_app_part_index = i; } } else if (new_app_part_index_with_copy == -1 && !check_overlap(running_app_offset, running_app_size, new_part->pos.offset, running_app_size)) { - // Copy path: needs a registered source partition in the *current* table at the new slot's offset. + // esp_partition_copy needs a registered source partition in the current table. const esp_partition_t *p = find_app_partition_at(new_part->pos.offset, running_app_size); if (p != nullptr) { new_app_part_index_with_copy = i; @@ -176,7 +163,6 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - // No-copy preferred; copy path only when no-copy slot was unavailable. if (new_app_part_index != -1) { plan.target_app_index = new_app_part_index; plan.copy_source_part = nullptr; @@ -193,10 +179,8 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - // Get running app partition and used size. A zero size means we couldn't determine the running - // app (e.g., esp_ota_get_running_partition() returned nullptr after a previous aborted partition - // table OTA called esp_partition_unload_all()). Without a valid size we cannot safely compute - // overlap or copy bounds, so fail before any flash operation. + // Without a valid running-app size we cannot compute overlap or copy bounds. zero indicates + // esp_ota_get_running_partition() failed (e.g. cache unloaded by a previous aborted OTA). uint32_t running_app_offset; size_t running_app_size; get_running_app_position(running_app_offset, running_app_size); @@ -211,26 +195,21 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { return validate_result; } - // Past this point any failure (power loss, watchdog reset, write error after the table has been - // partially erased) can leave the device unable to boot. Logged at ERROR severity so the message - // is visible in default log filters. + // ERROR severity so the warning shows up in default log filters; any failure past this point + // can leave the device unable to boot. ESP_LOGE(TAG, "Starting partition table update.\n" " DO NOT REMOVE POWER until the device reboots successfully.\n" " Loss of power during this operation may permanently brick the device."); - // Hold the watchdog open for the entire critical section: optional app copy, partition-table - // erase/write, and boot partition selection. None of the steps below should yield long enough - // to require a refresh, but bundling them under a single guard avoids spurious resets if the - // underlying ESP-IDF calls take longer than expected on a given chip variant. + // One guard over the whole critical section in case an IDF call takes longer than expected on + // some chip variant. watchdog::WatchdogManager watchdog(15000); esp_err_t err; const esp_partition_info_t *new_partition_table = reinterpret_cast(this->buf_); - // Copy the running app partition to new position if needed. - // esp_ota_get_running_partition() is still valid here (we have not yet called - // esp_partition_unload_all()) and returns the same partition that find_app_partition_at would - // have located, without an extra iterator walk. + // esp_ota_get_running_partition() is still valid here (esp_partition_unload_all() has not run) + // so use it directly instead of repeating the iterator walk. if (plan.copy_source_part != nullptr) { const esp_partition_t *running_app_part = esp_ota_get_running_partition(); ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address, @@ -244,11 +223,8 @@ 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. - // 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. + // Deinit NVS only just before the first destructive write so verify/copy failure paths return + // with NVS still functional. The guard re-inits on early returns; success disarms it. nvs_flash_deinit(); NvsReinitGuard nvs_guard; @@ -262,23 +238,20 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { } err = esp_ota_write(this->update_handle_, this->buf_, ESP_PARTITION_TABLE_MAX_LEN); if (err != ESP_OK) { - // Release the handle eagerly; abort() would also do this, but cleaning up locally keeps the - // partial-write failure path self-contained. esp_ota_abort(this->update_handle_); this->update_handle_ = 0; ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err); 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 + this->update_handle_ = 0; // esp_ota_end releases the handle internally if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - // esp_partition_unload_all() invalidates every cached partition entry, including the externally - // registered `partition_table_part_`, so the explicit deregister call is redundant. Do the - // unload first, then null the member pointer so it never dangles past invalidation; if abort() - // were ever to observe an in-between state, it would see a non-null but freed pointer and crash. + // unload first, then null the member pointer; if abort() ran between the two steps it would + // see a freed pointer. esp_partition_unload_all() invalidates partition_table_part_ too, so + // an explicit deregister would be redundant. esp_partition_unload_all(); this->partition_table_part_ = nullptr; @@ -299,26 +272,19 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { return OTA_RESPONSE_OK; } -// Process-scoped cache of the running app's flash position. Cannot live on IDFOTABackend -// because the backend is created/destroyed per OTA connection, while the cached values must -// survive across connections: once a previously aborted partition-table OTA has called -// esp_partition_unload_all(), esp_ota_get_running_partition() no longer returns valid data, -// so we have to remember the answer from the first successful call. The running app does not -// move within a boot, so a single capture is valid for the process lifetime. +// Process-scoped cache. Cannot be a backend member: backends are per-connection but the cache +// must outlive a connection that called esp_partition_unload_all(), after which +// esp_ota_get_running_partition() no longer returns valid data. static bool s_running_app_initialized = false; static uint32_t s_running_app_cached_offset = 0; static size_t s_running_app_cached_size = 0; +// Flag-gated rather than size==0 so a failed first call doesn't poison the cache. void get_running_app_position(uint32_t &offset, size_t &size) { - // Returns the start address and the used length (rounded up to flash sectors) of the running app. - // The ``s_running_app_initialized`` flag (rather than ``size == 0``) gates the cache so a failed - // first call does not poison it; the next caller retries. Values are written atomically only - // after the full computation succeeds. if (!s_running_app_initialized) { const esp_partition_t *running_app_part = esp_ota_get_running_partition(); if (running_app_part == nullptr || running_app_part->erase_size == 0) { - // Cannot determine the running app right now; surface zeros without committing to the cache - // so a later call has a chance to succeed. + // Surface zeros without committing to the cache so a later call has a chance to succeed. offset = 0; size = 0; return; @@ -337,7 +303,7 @@ void get_running_app_position(uint32_t &offset, size_t &size) { image_metadata.image_len < running_app_part->size) { pending_size = image_metadata.image_len; } - // Round up to flash sector size so the copy spans complete erase blocks. + // Round up to a full flash sector so the copy spans complete erase blocks. pending_size = ((pending_size + running_app_part->erase_size - 1) / running_app_part->erase_size) * running_app_part->erase_size;