#include "OtaUpdater.h" #include #include #include #ifdef OTA_UPDATER_ANY_PARTITION #include #include #endif #include static String getFilenameExtension(const String & filename) { const auto pos = filename.lastIndexOf('.'); if (pos != -1) { return filename.substring(static_cast(pos) + 1); } return {}; } OtaUpdater::FileType OtaUpdater::classify(const String & filename, size_t filesize, size_t min_firmware_size) { const String extension = getFilenameExtension(filename); if (extension == "bin" && filename.endsWith("littlefs.bin")) { return FileType::FILESYSTEM; } if (extension == "bin") { String lower = filename; lower.toLowerCase(); const size_t min_size = lower.indexOf("recovery") >= 0 ? MIN_RECOVERY_FIRMWARE_SIZE : min_firmware_size; if (filesize >= min_size) { return FileType::FIRMWARE; } } if (extension == "json") { return FileType::JSON; } if (extension == "md5") { return FileType::MD5; } return FileType::UNSUPPORTED; } bool OtaUpdater::isCompatibleFirmware(const uint8_t * data, size_t len) { // 0xE9 magic at offset 0 indicates an esp bin, chip id at offset 12 #if CONFIG_IDF_TARGET_ESP32 constexpr uint8_t chip_id = ESP_CHIP_ID_ESP32; #elif CONFIG_IDF_TARGET_ESP32S2 constexpr uint8_t chip_id = ESP_CHIP_ID_ESP32S2; #elif CONFIG_IDF_TARGET_ESP32C3 constexpr uint8_t chip_id = ESP_CHIP_ID_ESP32C3; #elif CONFIG_IDF_TARGET_ESP32S3 constexpr uint8_t chip_id = ESP_CHIP_ID_ESP32S3; #elif CONFIG_IDF_TARGET_ESP32C6 constexpr uint8_t chip_id = ESP_CHIP_ID_ESP32C6; #else return true; #endif return len <= 12 || (data[0] == ESP_IMAGE_HEADER_MAGIC && data[12] == chip_id); } bool OtaUpdater::isRecoveryFirmware(const uint8_t * data, size_t len) { constexpr size_t offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t); if (len < offset + offsetof(esp_app_desc_t, time)) { return false; } esp_app_desc_t desc; memcpy(&desc, data + offset, offsetof(esp_app_desc_t, time)); return desc.magic_word == ESP_APP_DESC_MAGIC_WORD && strncmp(desc.project_name, RECOVERY_PROJECT_NAME, sizeof(desc.project_name)) == 0; } bool OtaUpdater::isRecoveryPartition(const esp_partition_t * partition, esp_app_desc_t * desc) { esp_app_desc_t d; if (partition == nullptr || esp_ota_get_partition_description(partition, &d) != ESP_OK || strncmp(d.project_name, RECOVERY_PROJECT_NAME, sizeof(d.project_name)) != 0) { return false; } if (desc != nullptr) { *desc = d; } return true; } bool OtaUpdater::parseMd5Digest(const uint8_t * data, size_t len, std::array & out) { size_t i = 0; while (i < len && (data[i] == ' ' || data[i] == '\t' || data[i] == '\r' || data[i] == '\n')) { ++i; } if (len - i < 32) { return false; } for (size_t n = 0; n < 32; n++) { const char c = static_cast(data[i + n]); const bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); if (!hex) { return false; } out[n] = (c >= 'A' && c <= 'F') ? static_cast(c - 'A' + 'a') : c; } out[32] = '\0'; return true; } bool OtaUpdater::confirmRunningApp() { #ifdef CONFIG_APP_ROLLBACK_ENABLE esp_ota_img_states_t state; if (esp_ota_get_state_partition(esp_ota_get_running_partition(), &state) == ESP_OK && state == ESP_OTA_IMG_PENDING_VERIFY) { return esp_ota_mark_app_valid_cancel_rollback() == ESP_OK; } #endif return false; } bool OtaUpdater::setMd5(const uint8_t * data, size_t len) { return parseMd5Digest(data, len, _md5); } void OtaUpdater::clearMd5() { _md5.front() = '\0'; _md5_applied = false; } bool OtaUpdater::hasMd5() const { return strlen(_md5.data()) == _md5.size() - 1; } #ifdef OTA_UPDATER_ANY_PARTITION void OtaUpdater::setError(esp_err_t err) { snprintf(_error_buf, sizeof(_error_buf), "%s (0x%x)", esp_err_to_name(err), static_cast(err)); _error = _error_buf; } void OtaUpdater::releaseTarget() { _target = nullptr; heap_caps_free(_dram); _dram = nullptr; } #endif bool OtaUpdater::beginFirmware(size_t filesize, const esp_partition_t * target, bool set_boot) { _md5_applied = false; _error = ""; confirmRunningApp(); if (target != nullptr) { #ifdef OTA_UPDATER_ANY_PARTITION if (filesize > target->size + 4096) { // allow for the multipart overhead _error = "Firmware does not fit in the partition"; return false; } _dram = static_cast(heap_caps_malloc(DRAM_CHUNK, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); if (_dram == nullptr) { _error = "No internal RAM for OTA buffer"; return false; } _target = target; _written = 0; _erased = 0; _set_boot = set_boot; if (hasMd5()) { _md5Builder.begin(); _md5_applied = true; } return true; #else _error = "Writing to a specific partition is not supported"; return false; #endif } if (!Update.begin(filesize - sizeof(esp_image_header_t))) { return false; } if (hasMd5()) { Update.setMD5(_md5.data()); _md5_applied = true; } return true; } bool OtaUpdater::beginFilesystem() { clearMd5(); // so Update.end() doesn't compare against a stale digest _error = ""; // the HTTP content length is the multipart body size, not the file size, so it can exceed // the partition by a few hundred bytes. Let the Update library size against the whole partition. return Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS); } bool OtaUpdater::write(const uint8_t * data, size_t len) { #ifdef OTA_UPDATER_ANY_PARTITION if (_target != nullptr) { while (len) { const size_t n = len < DRAM_CHUNK ? len : DRAM_CHUNK; memcpy(_dram, data, n); const size_t need = _written + n; if (need > _erased) { size_t erase_to = (need + FLASH_SECTOR - 1) / FLASH_SECTOR * FLASH_SECTOR; if (erase_to > _target->size) { erase_to = _target->size; } const size_t erase_len = erase_to - _erased; if (erase_len > 0) { const esp_err_t err = esp_partition_erase_range(_target, _erased, erase_len); if (err != ESP_OK) { setError(err); return false; } _erased = erase_to; } } const esp_err_t err = esp_partition_write(_target, _written, _dram, n); if (err != ESP_OK) { setError(err); return false; } if (_md5_applied) { _md5Builder.add(_dram, n); } _written += n; data += n; len -= n; } return true; } #endif return Update.write(const_cast(data), len) == len; } bool OtaUpdater::end() { #ifdef OTA_UPDATER_ANY_PARTITION if (_target != nullptr) { const esp_partition_t * target = _target; const bool set_boot = _set_boot; releaseTarget(); if (_md5_applied) { _md5Builder.calculate(); if (strcmp(_md5Builder.toString().c_str(), _md5.data()) != 0) { _error = "MD5 check failed"; return false; } } esp_image_metadata_t meta = {}; esp_partition_pos_t part_pos = {.offset = target->address, .size = target->size}; esp_err_t err = esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta); if (err != ESP_OK) { setError(err); return false; } if (set_boot) { err = esp_ota_set_boot_partition(target); if (err != ESP_OK) { setError(err); return false; } } return true; } #endif return Update.end(true); } void OtaUpdater::abort() { #ifdef OTA_UPDATER_ANY_PARTITION if (_target != nullptr) { releaseTarget(); return; } #endif if (Update.isRunning()) { Update.abort(); } } size_t OtaUpdater::progress() const { #ifdef OTA_UPDATER_ANY_PARTITION if (_target != nullptr) { return _written; } #endif return Update.progress(); } const char * OtaUpdater::errorString() const { return _error[0] != '\0' ? _error : Update.errorString(); }