mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
[ota] Unify backend begin() signature and trim partition-table comments
Every OTA backend's begin() now takes ``(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP)``. ESPHomeOTAComponent::handle_data_() calls ``backend_->begin(ota_size, ota_type)`` unconditionally; the USE_OTA_PARTITIONS ifdef around the call disappears. The four non-ESP32 backends (esp8266, rp2040, libretiny, host) accept the new parameter and reject anything other than OTA_TYPE_UPDATE_APP up front, so the client-visible behaviour is unchanged. Same commit also trims the verbose block comments in the partition- table TU and the IDF backend header, keeping the genuinely non-obvious WHYs (slot-selection policy, NvsReinitGuard semantics, unload-then-null ordering, cache-init flag, dump_config nullptr fallback) and dropping the procedural narration. No functional change.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -13,7 +13,10 @@ static const char *const TAG = "ota.arduino_libretiny";
|
||||
|
||||
std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); }
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,7 +15,10 @@ static const char *const TAG = "ota.arduino_rp2040";
|
||||
|
||||
std::unique_ptr<ArduinoRP2040OTABackend> make_ota_backend() { return make_unique<ArduinoRP2040OTABackend>(); }
|
||||
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -50,7 +50,10 @@ static const char *const TAG = "ota.esp8266";
|
||||
|
||||
std::unique_ptr<ESP8266OTABackend> make_ota_backend() { return make_unique<ESP8266OTABackend>(); }
|
||||
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -16,14 +16,12 @@ static const char *const TAG = "ota.idf";
|
||||
|
||||
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -10,7 +10,9 @@ namespace esphome::ota {
|
||||
|
||||
std::unique_ptr<HostOTABackend> make_ota_backend() { return make_unique<HostOTABackend>(); }
|
||||
|
||||
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) {}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<const esp_partition_info_t *>(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<const esp_partition_info_t *>(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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user