#include "OtaUpdater.h" #include #include #include "bootloader_common.h" #include "esp_flash_partitions.h" #include "esp_http_client.h" #include "esp_https_ota.h" #include "esp_ota_ops.h" #include "esp_partition.h" #include "esp_wifi.h" namespace { constexpr char latestReleaseUrl[] = "https://api.github.com/repos/jpirnay/crosspoint-reader/releases/latest"; /* This is buffer and size holder to keep upcoming data from latestReleaseUrl */ char* local_buf; int output_len; /* * When esp_crt_bundle.h included, it is pointing wrong header file * which is something under WifiClientSecure because of our framework based on arduno platform. * To manage this obstacle, don't include anything, just extern and it will point correct one. */ extern "C" { extern esp_err_t esp_crt_bundle_attach(void* conf); } esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) { return esp_http_client_set_header(http_client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); } esp_err_t event_handler(esp_http_client_event_t* event) { /* We are only interested in HTTP_EVENT_ON_DATA event */ if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK; if (event->data == nullptr || event->data_len == 0) { return ESP_OK; } const int newSize = output_len + event->data_len + 1; char* newBuf = static_cast(realloc(local_buf, static_cast(newSize))); if (newBuf == nullptr) { LOG_ERR("OTA", "HTTP Client Out of Memory Failed, Allocation %d", newSize); return ESP_ERR_NO_MEM; } local_buf = newBuf; memcpy(local_buf + output_len, event->data, event->data_len); output_len += event->data_len; local_buf[output_len] = '\0'; return ESP_OK; } /* event_handler */ } /* namespace */ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() { // Reset globals so retries start clean regardless of previous outcome local_buf = nullptr; output_len = 0; JsonDocument filter; esp_err_t esp_err; JsonDocument doc; esp_http_client_config_t client_config = { .url = latestReleaseUrl, .timeout_ms = 10000, .event_handler = event_handler, /* Default HTTP client buffer size 512 byte only */ .buffer_size = 8192, .buffer_size_tx = 8192, .crt_bundle_attach = esp_crt_bundle_attach, .keep_alive_enable = true, }; /* To track life time of local_buf, dtor will be called on exit from that function */ struct localBufCleaner { char** bufPtr; ~localBufCleaner() { if (*bufPtr) { free(*bufPtr); *bufPtr = NULL; } } } localBufCleaner = {&local_buf}; esp_http_client_handle_t client_handle = esp_http_client_init(&client_config); if (!client_handle) { LOG_ERR("OTA", "HTTP Client Handle Failed"); return INTERNAL_UPDATE_ERROR; } esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); if (esp_err != ESP_OK) { LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err)); esp_http_client_cleanup(client_handle); return INTERNAL_UPDATE_ERROR; } esp_err = esp_http_client_perform(client_handle); if (esp_err != ESP_OK) { LOG_ERR("OTA", "esp_http_client_perform Failed : %s", esp_err_to_name(esp_err)); esp_http_client_cleanup(client_handle); return HTTP_ERROR; } /* esp_http_client_close will be called inside cleanup as well*/ esp_err = esp_http_client_cleanup(client_handle); if (esp_err != ESP_OK) { LOG_ERR("OTA", "esp_http_client_cleanup Failed : %s", esp_err_to_name(esp_err)); return INTERNAL_UPDATE_ERROR; } filter["tag_name"] = true; filter["assets"][0]["name"] = true; filter["assets"][0]["browser_download_url"] = true; filter["assets"][0]["size"] = true; const DeserializationError error = deserializeJson(doc, local_buf, DeserializationOption::Filter(filter)); if (error) { LOG_ERR("OTA", "JSON parse failed: %s", error.c_str()); return JSON_PARSE_ERROR; } if (!doc["tag_name"].is()) { LOG_ERR("OTA", "No tag_name found"); return JSON_PARSE_ERROR; } if (!doc["assets"].is()) { LOG_ERR("OTA", "No assets found"); return JSON_PARSE_ERROR; } latestVersion = doc["tag_name"].as(); for (int i = 0; i < doc["assets"].size(); i++) { if (doc["assets"][i]["name"] == "firmware.bin") { otaUrl = doc["assets"][i]["browser_download_url"].as(); otaSize = doc["assets"][i]["size"].as(); totalSize = otaSize; updateAvailable = true; break; } } if (!updateAvailable) { LOG_ERR("OTA", "No firmware.bin asset found"); return NO_UPDATE; } LOG_DBG("OTA", "Found update: %s", latestVersion.c_str()); return OK; } bool OtaUpdater::isUpdateNewer() const { if (!updateAvailable || latestVersion.empty() || latestVersion == CROSSPOINT_VERSION) { return false; } int currentMajor, currentMinor, currentPatch; int latestMajor, latestMinor, latestPatch; const auto currentVersion = CROSSPOINT_VERSION; // semantic version check (only match on 3 segments) sscanf(latestVersion.c_str(), "%d.%d.%d", &latestMajor, &latestMinor, &latestPatch); sscanf(currentVersion, "%d.%d.%d", ¤tMajor, ¤tMinor, ¤tPatch); /* * Compare major versions. * If they differ, return true if latest major version greater than current major version * otherwise return false. */ if (latestMajor != currentMajor) return latestMajor > currentMajor; /* * Compare minor versions. * If they differ, return true if latest minor version greater than current minor version * otherwise return false. */ if (latestMinor != currentMinor) return latestMinor > currentMinor; /* * Check patch versions. */ if (latestPatch != currentPatch) return latestPatch > currentPatch; // If we reach here, it means all segments are equal. // One final check, if we're on an RC build (contains "-rc"), we should consider the latest version as newer even if // the segments are equal, since RC builds are pre-release versions. if (strstr(currentVersion, "-rc") != nullptr) { return true; } return false; } const std::string& OtaUpdater::getLatestVersion() const { return latestVersion; } void OtaUpdater::cleanupUpdate() { if (otaHandle) { const esp_err_t err = esp_https_ota_finish(otaHandle); if (err != ESP_OK) { LOG_ERR("OTA", "esp_https_ota_finish on cleanup: %s", esp_err_to_name(err)); } otaHandle = nullptr; } cancelRequested = false; esp_wifi_set_ps(WIFI_PS_MIN_MODEM); } void OtaUpdater::cancelUpdate() { if (otaHandle) { cleanupUpdate(); } else { cancelRequested = true; } } OtaUpdater::OtaUpdaterError OtaUpdater::beginInstallUpdate() { if (!isUpdateNewer()) { return UPDATE_OLDER_ERROR; } cleanupUpdate(); render = false; cancelRequested = false; esp_http_client_config_t client_config = { .url = otaUrl.c_str(), .timeout_ms = 10000, .max_redirection_count = 5, .buffer_size = 8192, .buffer_size_tx = 8192, .crt_bundle_attach = esp_crt_bundle_attach, .keep_alive_enable = true, }; esp_https_ota_config_t ota_config = { .http_config = &client_config, .http_client_init_cb = http_client_set_header_cb, }; /* For better timing and connectivity, we disable power saving for WiFi */ esp_wifi_set_ps(WIFI_PS_NONE); esp_err_t esp_err = esp_https_ota_begin(&ota_config, &otaHandle); if (esp_err != ESP_OK) { LOG_DBG("OTA", "HTTP OTA Begin Failed: %s", esp_err_to_name(esp_err)); cleanupUpdate(); return INTERNAL_UPDATE_ERROR; } return UPDATE_IN_PROGRESS; } /* Writes the otadata entry to boot from the most recently flashed OTA partition, * bypassing esp_ota_set_boot_partition()'s image_validate() call. * Used when esp_https_ota_finish() returns ESP_ERR_OTA_VALIDATE_FAILED on * unsigned Arduino builds (boot_comm efuse revision check false-positive). */ int OtaUpdater::forceSetOtaBootPartition() { const esp_partition_t* newPartition = esp_ota_get_next_update_partition(nullptr); if (newPartition == nullptr) { return ESP_ERR_NOT_FOUND; } const esp_partition_t* otaDataPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_OTA, nullptr); if (otaDataPartition == nullptr) { return ESP_ERR_NOT_FOUND; } esp_ota_select_entry_t otadata[2]; esp_err_t err = esp_partition_read(otaDataPartition, 0, &otadata[0], sizeof(esp_ota_select_entry_t)); if (err != ESP_OK) return err; err = esp_partition_read(otaDataPartition, otaDataPartition->erase_size, &otadata[1], sizeof(esp_ota_select_entry_t)); if (err != ESP_OK) return err; int activeSlot = bootloader_common_get_active_otadata(otadata); int nextSlot = (activeSlot == -1) ? 0 : (~activeSlot & 1); uint8_t otaAppCount = 0; while (esp_partition_find_first(ESP_PARTITION_TYPE_APP, static_cast(ESP_PARTITION_SUBTYPE_APP_OTA_MIN + otaAppCount), nullptr) != nullptr) { otaAppCount++; } if (otaAppCount == 0) return ESP_ERR_NOT_FOUND; const uint8_t subTypeId = newPartition->subtype & 0x0F; uint32_t newSeq; if (activeSlot == -1) { newSeq = subTypeId + 1; } else { uint32_t currentSeq = otadata[activeSlot].ota_seq; newSeq = currentSeq; while (newSeq % otaAppCount != static_cast(subTypeId)) { newSeq++; } if (newSeq == currentSeq) newSeq += otaAppCount; } otadata[nextSlot].ota_seq = newSeq; otadata[nextSlot].ota_state = ESP_OTA_IMG_VALID; otadata[nextSlot].crc = bootloader_common_ota_select_crc(&otadata[nextSlot]); err = esp_partition_erase_range(otaDataPartition, otaDataPartition->erase_size * static_cast(nextSlot), otaDataPartition->erase_size); if (err != ESP_OK) return err; return esp_partition_write(otaDataPartition, otaDataPartition->erase_size * static_cast(nextSlot), &otadata[nextSlot], sizeof(esp_ota_select_entry_t)); } OtaUpdater::OtaUpdaterError OtaUpdater::performInstallUpdateStep() { if (cancelRequested) { cleanupUpdate(); return UPDATE_CANCELLED; } if (!otaHandle) { return INTERNAL_UPDATE_ERROR; } esp_err_t esp_err = esp_https_ota_perform(otaHandle); processedSize = esp_https_ota_get_image_len_read(otaHandle); render = true; if (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS) { return UPDATE_IN_PROGRESS; } esp_wifi_set_ps(WIFI_PS_MIN_MODEM); if (esp_err != ESP_OK) { LOG_ERR("OTA", "esp_https_ota_perform Failed: %s", esp_err_to_name(esp_err)); cleanupUpdate(); return HTTP_ERROR; } if (!esp_https_ota_is_complete_data_received(otaHandle)) { LOG_ERR("OTA", "esp_https_ota_is_complete_data_received Failed"); cleanupUpdate(); return INTERNAL_UPDATE_ERROR; } esp_err_t finish_err = esp_https_ota_finish(otaHandle); otaHandle = nullptr; if (finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { /* Arduino unsigned builds fail boot_comm validation even though the image * is fully written. Force the boot partition to the new OTA slot by writing * the otadata entry directly, bypassing image_validate(). */ LOG_INF("OTA", "Validation failed (expected for unsigned Arduino builds) - forcing boot partition"); finish_err = forceSetOtaBootPartition(); if (finish_err != ESP_OK) { LOG_ERR("OTA", "forceSetOtaBootPartition failed: %s", esp_err_to_name(finish_err)); cleanupUpdate(); return VALIDATE_FAILED; } } else if (finish_err != ESP_OK) { LOG_ERR("OTA", "esp_https_ota_finish Failed: %s", esp_err_to_name(finish_err)); cleanupUpdate(); return INTERNAL_UPDATE_ERROR; } LOG_INF("OTA", "Update completed"); return OK; }