#include "AppUpgrade.h" #include "esp_log.h" #include #include #include #include #include "global.h" #include "JsonConstrain.h" #include "BLE_UpdateService.h" #include #include #include #include #include static const char* TAG = "AppUpdater"; TaskHandle_t Update_Task_Handle = NULL; TaskHandle_t versionCheckTask_Handle = NULL; volatile bool g_UpdateCancelFlag = false; // cancellation flag UpdateMode g_UpdateMode = UpdateMode::UPDATE_BOTH; // Default to updating both files and firmware String updateUrl = ""; Version otaVersion; AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin) : localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs) { // Use dynamic buffer size based on available memory - much more conservative now size_t available_heap = ESP.getFreeHeap(); size_t buffer_size = std::min(BUFFER_SIZE, available_heap / 16); // Use at most 1/16 of free heap // Ensure minimum viable size if (buffer_size < 1024) buffer_size = 1024; // Absolute minimum is 1KB downloadBuffer.reset(new uint8_t[buffer_size]); baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL); // Ensure baseUrl ends with a single '/' if(!baseUrl.endsWith("/")) baseUrl += "/"; ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s, buffer=%u bytes", localVersion.toString().c_str(), baseUrl.c_str(), buffer_size); } void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) { progressCb = callback; } void AppUpdater::setUpdateMode(UpdateMode mode) { updateMode = mode; g_UpdateMode = mode; // Sync with global mode for firmware update task } UpdateMode AppUpdater::getUpdateMode() const { return updateMode; } void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) { status = newStatus; if (progressCb) { progressCb(status, percentage, message); } } AppUpdater::ManifestCheckResult AppUpdater::checkManifest() { String url = buildUrl(manifestName); ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str()); String payload; for(int attempt=0; attempt MAX_MANIFEST_SIZE){ ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length()); return ManifestCheckResult::ERROR_TOO_LARGE; } // Parse JSON DeserializationError error = deserializeJson(jsonManifest, payload); ESP_LOGD(TAG, "Manifest deserialized"); if (error) { ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str()); return ManifestCheckResult::ERROR_PARSE_FAILED; } // Check for files section jsonFilesArray = jsonManifest["files"]; if (jsonFilesArray.isNull()) { ESP_LOGE(TAG, "No files section in manifest"); return ManifestCheckResult::ERROR_NO_FILES_SECTION; }else{ ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size()); } // Check for version section JsonObject jsonVersion = jsonManifest["version"]; ESP_LOGD(TAG, "Version section found"); if (jsonVersion.isNull()) { ESP_LOGE(TAG, "No version section in manifest"); return ManifestCheckResult::ERROR_NO_VERSION; } // Get the remote version byte major = jsonVersion["major"] | 0; byte minor = jsonVersion["minor"] | 0; byte patch = jsonVersion["patch"] | 0; otaVersion = {major, minor, patch}; //Version localVersion; //::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch); // Check if an update is available updateAvailable = false; // Only mark update available if remote is strictly newer than local if (otaVersion <= localVersion) { ESP_LOGI(TAG, "No updates available: remote=%s, local=%s", otaVersion.toString().c_str(), localVersion.toString().c_str()); return ManifestCheckResult::VERSION_CURRENT; }else{ updateAvailable = true; ESP_LOGI(TAG, "Update available: remote=%s, local=%s", otaVersion.toString().c_str(), localVersion.toString().c_str()); } //ESP_LOGD(TAG, "Manifest content: %s", payload.c_str()); return ManifestCheckResult::UPDATE_AVAILABLE; } bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) { //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); // Construct full URL String url = buildUrl(remotePath); ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath); // Quick skip: if exists and size & MD5 match bool skip = false; if(fileSystem.exists(localPath)){ String localMd5 = getLocalMD5(localPath); ESP_LOGI(TAG, "Local file exists: %s, MD5: %s, Expected: %s", localPath, localMd5.c_str(), expectedMd5); if(localMd5.equals(expectedMd5)) skip = true; } else { ESP_LOGI(TAG, "Local file does not exist: %s", localPath); } if(skip){ ESP_LOGI(TAG, "File already up to date: %s", localPath); updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath); return true; } ESP_LOGI(TAG, "Need to download file: %s (local MD5 mismatch or file missing)", localPath); // Start the download HTTPClient http; int httpCode = -1; for(int attempt=0; attempt 0) { ESP_LOGW(TAG, "Retrying download of %s (attempt %d/%d)", localPath, retry, MAX_RETRIES); // Need to re-fetch the file for retry - use the REMOTE path, not local path String url = buildUrl(remotePath); HTTPClient http; http.begin(url); int httpCode = http.GET(); if (httpCode != HTTP_CODE_OK) { ESP_LOGE(TAG, "Retry download failed: %d", httpCode); http.end(); continue; // Try next retry if available } stream = http.getStreamPtr(); contentLength = http.getSize(); } MD5Builder md5; md5.begin(); size_t totalRead = 0; // Create temporary filename in the same directory as the target file String targetDir = String(localPath); int lastSlash = targetDir.lastIndexOf('/'); String tempPath; if (lastSlash >= 0) { // Extract directory and filename String directory = targetDir.substring(0, lastSlash + 1); String filename = targetDir.substring(lastSlash + 1); tempPath = directory + "temp_" + filename + ".download"; } else { // File is in root directory tempPath = "/temp_" + String(localPath) + ".download"; } // Clean up any existing temp file first if (fileSystem.exists(tempPath.c_str())) { ESP_LOGW(TAG, "Removing existing temp file: %s", tempPath.c_str()); fileSystem.remove(tempPath.c_str()); } ESP_LOGI(TAG, "Using temp file path: %s for target: %s", tempPath.c_str(), localPath); // Open temporary file for writing (LittleFS will create directories automatically) File file = fileSystem.open(tempPath.c_str(), FILE_WRITE, true); // true = create if not exists if (!file) { ESP_LOGE(TAG, "Failed to open temporary file for writing: %s", tempPath.c_str()); // Try to diagnose the issue ESP_LOGE(TAG, "LittleFS info - Used: %u bytes, Total: %u bytes", LittleFS.usedBytes(), LittleFS.totalBytes()); // Check if we're out of space if (LittleFS.usedBytes() >= LittleFS.totalBytes() * 0.95) { ESP_LOGE(TAG, "LittleFS nearly full - may not have space for temp file"); } return false; } //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); if (contentLength > 0) { // Single pass with known content length while (totalRead < contentLength) { if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; } size_t available = stream->available(); if (available) { size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE))); // Write to temp file and update MD5 if (file.write(downloadBuffer.get(), readLen) != readLen) { ESP_LOGE(TAG, "Failed to write to temporary file"); file.close(); fileSystem.remove(tempPath.c_str()); return false; } md5.add(downloadBuffer.get(), readLen); totalRead += readLen; updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath); } yield(); } } else { // Unknown content length: read until stream ends for (;;) { if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; } size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE); if (readLen == 0) { break; } if (file.write(downloadBuffer.get(), readLen) != readLen) { ESP_LOGE(TAG, "Failed to write to temporary file"); file.close(); fileSystem.remove(tempPath.c_str()); return false; } md5.add(downloadBuffer.get(), readLen); totalRead += readLen; // Progress unknown; emit periodic heartbeats at 0% // For unknown size, send heartbeats every ~16KB if((totalRead & 0x3FFF) == 0){ updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); } yield(); } } file.close(); md5.calculate(); String calculatedMd5 = md5.toString(); // Verify MD5 hash updateProgress(UpdateStatus::VERIFYING, 90, localPath); ESP_LOGI(TAG, "MD5 verification for %s: Expected='%s', Calculated='%s'", localPath, expectedMd5, calculatedMd5.c_str()); // Compare MD5 case-insensitively (in case there are case differences) String expectedMd5Lower = String(expectedMd5); expectedMd5Lower.toLowerCase(); String calculatedMd5Lower = calculatedMd5; calculatedMd5Lower.toLowerCase(); if (!calculatedMd5Lower.equals(expectedMd5Lower)) { ESP_LOGE(TAG, "MD5 mismatch for %s (attempt %d/%d). Expected: %s, Got: %s", localPath, retry+1, MAX_RETRIES+1, expectedMd5, calculatedMd5.c_str()); ESP_LOGE(TAG, "Length comparison - Expected: %d chars, Got: %d chars", strlen(expectedMd5), calculatedMd5.length()); fileSystem.remove(tempPath.c_str()); if (retry < MAX_RETRIES) { // Will retry in next loop iteration continue; } // Special case for certain file types - allow them to be used even with MD5 mismatch // This is a fallback option for non-critical files like HTML pages bool isNonCriticalFile = false; if (String(localPath).endsWith(".html") || String(localPath).endsWith(".css") || String(localPath).endsWith(".js")) { isNonCriticalFile = true; } if (isNonCriticalFile) { ESP_LOGW(TAG, "Using file %s despite MD5 mismatch (non-critical file)", localPath); // We'll still keep this file but report it as a verification failure // Ensure target directory exists before rename String dirPath = String(localPath); int targetLastSlash = dirPath.lastIndexOf('/'); if (targetLastSlash > 0) { dirPath = dirPath.substring(0, targetLastSlash); if (!fileSystem.exists(dirPath.c_str())) { ESP_LOGI(TAG, "Creating target directory: %s", dirPath.c_str()); String dummyFile = dirPath + "/.dummy"; File dummy = fileSystem.open(dummyFile.c_str(), FILE_WRITE, true); if (dummy) { dummy.print("temp"); dummy.close(); fileSystem.remove(dummyFile.c_str()); } } } // Rename the temp file to the final location if (fileSystem.exists(localPath)) { fileSystem.remove(localPath); } if (!fileSystem.rename(tempPath.c_str(), localPath)) { ESP_LOGE(TAG, "Failed to rename temporary file for non-critical use: %s -> %s", tempPath.c_str(), localPath); fileSystem.remove(tempPath.c_str()); return false; } // Return false to indicate verification failure, but the file will still be used return false; } return false; } updateProgress(UpdateStatus::VERIFYING, 95, localPath); // Ensure target directory exists before rename String dirPath = String(localPath); int targetLastSlash = dirPath.lastIndexOf('/'); if (targetLastSlash > 0) { dirPath = dirPath.substring(0, targetLastSlash); if (!fileSystem.exists(dirPath.c_str())) { ESP_LOGI(TAG, "Creating target directory: %s", dirPath.c_str()); String dummyFile = dirPath + "/.dummy"; File dummy = fileSystem.open(dummyFile.c_str(), FILE_WRITE, true); if (dummy) { dummy.print("temp"); dummy.close(); fileSystem.remove(dummyFile.c_str()); } } } // Replace original file with verified temp file if (fileSystem.exists(localPath)) { fileSystem.remove(localPath); } if (!fileSystem.rename(tempPath.c_str(), localPath)) { ESP_LOGE(TAG, "Failed to rename temporary file: %s -> %s", tempPath.c_str(), localPath); fileSystem.remove(tempPath.c_str()); return false; } updateProgress(UpdateStatus::VERIFYING, 100, localPath); return true; } return false; // All retries failed } String AppUpdater::getLocalMD5(const char* filePath){ File file = fileSystem.open(filePath, "r"); if(!file){ ESP_LOGE(TAG, "Error opening %s...", filePath); return String(); } MD5Builder md5Builder; md5Builder.begin(); size_t fileSize = file.size(); size_t totalRead = 0; size_t readLen = 0; while (totalRead < fileSize) { readLen = file.readBytes(reinterpret_cast(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE))); md5Builder.add(downloadBuffer.get(), readLen); totalRead += readLen; } md5Builder.calculate(); file.close(); return md5Builder.toString(); } bool AppUpdater::updateFilesArray() { int successCount = 0; int totalFiles = jsonFilesArray.size(); int failedCount = 0; ESP_LOGI(TAG, "Found %d files in manifest", totalFiles); // Iterate over each file entry in the manifest for (JsonObject file : jsonFilesArray) { const char* remotePath = file["path"]; const char* localPath = remotePath; // If path begins with "data/" or "/data/" strip only the "data" portion, retaining the leading slash if (localPath) { if (strncmp(localPath, "data/", 5) == 0) { localPath += 4; // points to '/' } else if (strncmp(localPath, "/data/", 6) == 0) { localPath += 5; // points to '/' } } const char* expectedMd5 = file["md5"]; // Skip invalid entries if (!remotePath || !localPath || !expectedMd5) { ESP_LOGE(TAG, "Invalid file entry in manifest"); continue; } // Attempt to update the file if (updateFile(remotePath, localPath, expectedMd5)) { successCount++; } else { failedCount++; ESP_LOGE(TAG, "Failed to update file: %s", localPath); // Continue with remaining files instead of stopping } } ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated, %d failed", successCount, totalFiles, failedCount); // Consider the update successful if most files updated correctly // Allow up to 20% of files to fail before considering the entire update failed return (failedCount <= totalFiles * 0.2); } bool AppUpdater::updateApp() { updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update"); // Check for firmware section in manifest if (!jsonManifest["firmware"].is() || !jsonManifest["firmware"]["md5"].is()) { ESP_LOGE(TAG, "Invalid firmware section in manifest"); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest"); return false; } // Get the firmware MD5 hash and URL const char* expectedMd5 = jsonManifest["firmware"]["md5"]; String firmwareUrl = buildUrl(appName); // First, try a HEAD request to verify server connectivity and file availability HTTPClient httpHead; bool fileExists = false; httpHead.begin(firmwareUrl); httpHead.setTimeout(10); int headCode = httpHead.sendRequest("HEAD"); if (headCode == HTTP_CODE_OK) { ESP_LOGI(TAG, "Firmware file exists on server, size: %d bytes", httpHead.getSize()); fileExists = true; } else { ESP_LOGW(TAG, "HEAD request failed: %d, proceeding with direct download attempt", headCode); } httpHead.end(); // Download the firmware with progressive timeouts HTTPClient http; int httpCode = -1; for(int attempt=0; attemptlabel, update->label); // Check for sufficient free memory - much more lenient now with chunked downloads size_t freeHeap = ESP.getFreeHeap(); size_t minRequiredHeap = 40 * 1024; // Only require 40KB minimum ESP_LOGI(TAG, "Free heap: %d bytes, firmware size: %d bytes", freeHeap, firmwareSize); if (freeHeap < minRequiredHeap) { ESP_LOGE(TAG, "Critically low memory: %d bytes (minimum required: %d bytes)", freeHeap, minRequiredHeap); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Critically low memory"); http.end(); return false; } // Check if we can roll back (just for logging, not a blocker) if (!Update.canRollBack()) { ESP_LOGW(TAG, "No valid firmware to roll back to"); } if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) { ESP_LOGE(TAG, "Firmware: Not enough space for update"); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update"); http.end(); return false; } // Set up MD5 checking MD5Builder md5; md5.begin(); // Prepare for download with improved resilience WiFiClient* stream = http.getStreamPtr(); unsigned long lastProgressTime = millis(); // Track time since last progress unsigned long connectStartTime = millis(); // Track total download time unsigned long lastWatchdogKick = millis(); // Track watchdog kicks // Calculate smaller buffer size based on heap size_t bufferSize = std::min(size_t(4096), ESP.getFreeHeap() / 16); // Use at most 1/16 of free heap, max 4KB if (bufferSize < 1024) bufferSize = 1024; // Minimum 1KB ESP_LOGI(TAG, "Using download buffer size: %u bytes (free heap: %u bytes)", bufferSize, ESP.getFreeHeap()); // Create a buffer just for this operation if needed std::unique_ptr localBuffer; uint8_t* downloadBuf = nullptr; if (bufferSize <= BUFFER_SIZE) { // Use the existing downloadBuffer downloadBuf = downloadBuffer.get(); } else { // Create a larger buffer for this specific download localBuffer.reset(new uint8_t[bufferSize]); downloadBuf = localBuffer.get(); } updateProgress(UpdateStatus::DOWNLOADING, 0, "Firmware download started"); // Download and verify firmware if (firmwareSize > 0) { size_t remaining = firmwareSize; size_t totalReceived = 0; size_t failedReads = 0; // Count consecutive read failures const size_t MAX_FAILED_READS = 10; // Allow up to 10 consecutive failures while (remaining > 0 && failedReads < MAX_FAILED_READS) { // Check for cancellation if(g_UpdateCancelFlag) { ESP_LOGE(TAG, "Update cancelled by user"); Update.abort(); http.end(); return false; } // Check WiFi status every 5 seconds if (millis() - lastWatchdogKick > 5000) { if (WiFi.status() != WL_CONNECTED) { ESP_LOGE(TAG, "WiFi disconnected during download"); Update.abort(); http.end(); return false; } // Reset watchdog to prevent timeout esp_task_wdt_reset(); lastWatchdogKick = millis(); } // Calculate optimum chunk size to balance speed vs reliability size_t chunk = std::min(remaining, bufferSize); size_t available = stream->available(); if (available > 0) { // Read what's available (up to our chunk size) size_t read = stream->readBytes(downloadBuf, std::min(available, chunk)); if (read > 0) { // Reset failure counter on successful read failedReads = 0; // Update MD5 and write firmware md5.add(downloadBuf, read); if (Update.write(downloadBuf, read) != read) { ESP_LOGE(TAG, "Write failed"); Update.abort(); http.end(); return false; } remaining -= read; totalReceived += read; lastProgressTime = millis(); // Reset progress timer // Update progress every ~2% or at least every 2 seconds static int lastPercent = 0; int percent = (totalReceived * 100) / firmwareSize; if (percent != lastPercent || (millis() - lastProgressTime > 2000)) { updateProgress(UpdateStatus::DOWNLOADING, percent, "Firmware"); lastPercent = percent; } } else { // Handle zero bytes read despite data being available failedReads++; ESP_LOGW(TAG, "Read returned 0 bytes despite data available (failure %d/%d)", failedReads, MAX_FAILED_READS); delay(100); // Short delay before retry } } else { // No data currently available if (millis() - lastProgressTime > 60000) { // No progress for 60 seconds - too long ESP_LOGE(TAG, "Download timed out - no progress for 60 seconds"); Update.abort(); http.end(); return false; } // Send periodic progress updates to keep client informed if (millis() - lastWatchdogKick > 5000) { int percent = (totalReceived * 100) / firmwareSize; updateProgress(UpdateStatus::DOWNLOADING, percent, String("Firmware: " + String(percent) + "% - waiting for data...").c_str()); } delay(100); // Short delay to prevent CPU hogging } } // Check if we failed due to too many consecutive read failures if (failedReads >= MAX_FAILED_READS) { ESP_LOGE(TAG, "Too many consecutive read failures"); Update.abort(); http.end(); return false; } } else { // Unknown size: stream until end (less common case) ESP_LOGW(TAG, "Firmware size unknown, streaming until end"); size_t totalReceived = 0; size_t emptyReads = 0; const size_t MAX_EMPTY_READS = 20; // Allow up to 20 empty reads before considering done while (emptyReads < MAX_EMPTY_READS) { if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; } // Reset watchdog periodically if (millis() - lastWatchdogKick > 5000) { esp_task_wdt_reset(); lastWatchdogKick = millis(); } size_t available = stream->available(); if (available > 0) { size_t read = stream->readBytes(downloadBuf, std::min(available, bufferSize)); if (read > 0) { emptyReads = 0; // Reset empty read counter md5.add(downloadBuf, read); if (Update.write(downloadBuf, read) != read) { ESP_LOGE(TAG, "Write failed"); Update.abort(); http.end(); return false; } totalReceived += read; lastProgressTime = millis(); // Just update with received byte count since we don't know total updateProgress(UpdateStatus::DOWNLOADING, 0, String("Firmware: " + String(totalReceived / 1024) + "KB received").c_str()); } else { emptyReads++; delay(100); } } else { // No data available if (totalReceived > 0 && millis() - lastProgressTime > 30000) { // If we've received data but nothing for 30s, probably done ESP_LOGI(TAG, "No data for 30s after receiving %d bytes, assuming download complete", totalReceived); break; } emptyReads++; delay(100); } } if (totalReceived < 100*1024) { // Less than 100KB received ESP_LOGE(TAG, "Downloaded firmware too small (%d bytes)", totalReceived); Update.abort(); http.end(); return false; } } // Verify MD5 md5.calculate(); String calculatedMd5 = md5.toString(); updateProgress(UpdateStatus::VERIFYING, 95, "firmware"); if (!calculatedMd5.equals(expectedMd5)) { ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str()); updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch"); Update.abort(); http.end(); return false; } // Finish update if (!Update.end()) { ESP_LOGE(TAG, "Update end failed: %d", Update.getError()); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed"); // Try to roll back if possible if (Update.hasError() && Update.canRollBack()) { ESP_LOGI(TAG, "Rolling back to previous version"); Update.rollBack(); // Don't restart here, let the main task do it } http.end(); return false; } http.end(); updateProgress(UpdateStatus::COMPLETE, 100, "Firmware: Complete"); return true; } bool AppUpdater::IsUpdateAvailable(){ return updateAvailable; } String AppUpdater::buildUrl(const char* path) const { if(!path || !*path) return baseUrl; // just base String p(path); // If already absolute URL, pass through if(p.startsWith("http://") || p.startsWith("https://")) return p; // Strip leading slashes to avoid double while(p.startsWith("/")) p.remove(0,1); // Ensure baseUrl has single trailing slash String b = baseUrl; if(!b.endsWith("/")) b += "/"; return b + p; } AsyncEventSource* eventProgress = nullptr; void startFirmwareUpdateTask(AsyncEventSource* evProg) { eventProgress = evProg; if(Update_Task_Handle) { ESP_LOGW(TAG, "Firmware update task already running"); return; } // Create task with higher priority (3) and optimized stack size xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*6, NULL, 3, &Update_Task_Handle); } void firmwareUpdateTask(void* parameter) { static const char* TAG = "UpdateTask"; // Initialize watchdog timer for the update task esp_task_wdt_init(60, true); // 60 second timeout, panic on timeout esp_task_wdt_add(NULL); // Add current task to watchdog try { loadUpdateJson(); esp_task_wdt_reset(); // Reset watchdog timer after JSON loading // Initialize updater with smart pointer std::unique_ptr updater(new AppUpdater( LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin")); updater->setProgressCallback(updateProgress); ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str()); // Check and perform updates auto manifestResult = updater->checkManifest(); if (manifestResult != AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE) { // Handle different error cases std::string errorMsg; switch (manifestResult) { case AppUpdater::ManifestCheckResult::ERROR_FETCH_FAILED: errorMsg = "Failed to fetch manifest"; break; case AppUpdater::ManifestCheckResult::ERROR_TOO_LARGE: errorMsg = "Manifest file too large"; break; case AppUpdater::ManifestCheckResult::ERROR_PARSE_FAILED: errorMsg = "Failed to parse manifest"; break; case AppUpdater::ManifestCheckResult::ERROR_NO_FILES_SECTION: errorMsg = "Manifest missing files section"; break; case AppUpdater::ManifestCheckResult::ERROR_NO_VERSION: errorMsg = "Manifest missing version section"; break; case AppUpdater::ManifestCheckResult::VERSION_CURRENT: errorMsg = "Current version is up to date"; // This is not actually an error ESP_LOGI(TAG, "No update needed: %s", errorMsg.c_str()); updateProgress(AppUpdater::UpdateStatus::MESSAGE, 0, errorMsg.c_str()); // Don't throw, just exit gracefully break; default: errorMsg = "Unknown manifest check error"; } if (manifestResult != AppUpdater::ManifestCheckResult::VERSION_CURRENT) { ESP_LOGE(TAG, "Manifest check failed: %s", errorMsg.c_str()); updateProgress(AppUpdater::UpdateStatus::ERROR, 0, errorMsg.c_str()); } } if (updater->IsUpdateAvailable()) { bool filesUpdated = true; bool firmwareUpdated = false; // Initialize to false - only set to true if firmware is actually updated // Update files based on update mode if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) { ESP_LOGI(TAG, "Update mode includes files, updating files..."); filesUpdated = updater->updateFilesArray(); if (!filesUpdated) { ESP_LOGW(TAG, "Some files failed to update"); if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY) { ESP_LOGE(TAG, "Files-only update failed"); updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update files"); // Skip to cleanup since this is files-only mode and it failed goto cleanup; } else { ESP_LOGW(TAG, "File update failed, but continuing with firmware update"); } } } else { ESP_LOGI(TAG, "Skipping file updates (mode: firmware only)"); } // Update firmware based on update mode if (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) { ESP_LOGI(TAG, "Update mode includes firmware, updating firmware..."); firmwareUpdated = updater->updateApp(); if (!firmwareUpdated) { ESP_LOGE(TAG, "Failed to update firmware"); updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update firmware"); // Skip to cleanup since firmware update failed goto cleanup; } } else { ESP_LOGI(TAG, "Skipping firmware update (mode: files only)"); } // Determine if we need to restart bool needsRestart = (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) && firmwareUpdated; if (needsRestart) { ESP_LOGI(TAG, "Firmware update successful, restarting..."); sendUpdateMessage("Restarting... ", true, 100); vTaskDelay(2000); ESP.restart(); } else { ESP_LOGI(TAG, "Update completed successfully (no restart required)"); updateProgress(AppUpdater::UpdateStatus::COMPLETE, 100, "Update completed successfully"); } } } catch (const std::exception& e) { ESP_LOGE(TAG, "Update failed with exception: %s", e.what()); updateProgress(AppUpdater::UpdateStatus::ERROR, 0, e.what()); } catch (...) { ESP_LOGE(TAG, "Update failed with unknown exception"); updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Unknown error during update"); } cleanup: // Clean up watchdog before exit esp_task_wdt_delete(NULL); Update_Task_Handle = NULL; vTaskDelete(NULL); } void startVersionCheckTask() { if(versionCheckTask_Handle != NULL) { ESP_LOGW(TAG, "Version Check Tak already running"); return; } // Create task with higher priority (3) and optimized stack size xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*6, NULL, 3, &versionCheckTask_Handle); } void versionCheckTask(void* parameter){ if(updateUrl == ""){ loadUpdateJson(); } AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin"); auto manifestResult = updater.checkManifest(); if (manifestResult == AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE || manifestResult == AppUpdater::ManifestCheckResult::VERSION_CURRENT) { otaVersion = updater.otaVersion; // capture remote ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str()); } else { ESP_LOGE(TAG, "Version check: manifest check failed with code %d", static_cast(manifestResult)); } versionCheckTask_Handle = NULL; vTaskDelete(NULL); } void loadUpdateJson(void) { try { ESP_LOGD(TAG, "loadUpdateJaon function..."); if(updateUrl == "") { String updateJsonPath = "/system/update.json"; // Read and parse update.json File file = LittleFS.open(updateJsonPath); if (!file) { throw std::runtime_error("Failed to open update.json"); } JsonDocument doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { throw std::runtime_error("Failed to parse update.json"); } // Get update configuration JsonObject jObj = doc.as(); String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/"); String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/"); updateUrl = baseUrl + folderName; ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str()); } } catch (const std::exception& e) { ESP_LOGE(TAG, "Update failed: %s", e.what()); } } void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) { char buffer[128]; const char* msg; bool isComplete = false; const char* safeMsg = message ? message : ""; switch (newStatus) { case AppUpdater::UpdateStatus::IDLE: snprintf(buffer, sizeof(buffer), "Update idle"); msg = buffer; break; case AppUpdater::UpdateStatus::MESSAGE: msg = message ? message : ""; break; case AppUpdater::UpdateStatus::DOWNLOADING: snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage); msg = buffer; break; case AppUpdater::UpdateStatus::VERIFYING: snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage); msg = buffer; break; case AppUpdater::UpdateStatus::FILE_SKIPPED: snprintf(buffer, sizeof(buffer), "%s: File Skipped, up to date", safeMsg); msg = buffer; break; case AppUpdater::UpdateStatus::FILE_SAVED: snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg); msg = buffer; break; case AppUpdater::UpdateStatus::MD5_FAILED: snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg); msg = buffer; break; case AppUpdater::UpdateStatus::COMPLETE: snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!"); msg = buffer; isComplete = true; break; case AppUpdater::UpdateStatus::ERROR: snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg); msg = buffer; break; default: snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus); msg = buffer; break; } ESP_LOGI(TAG, "%s", msg); sendUpdateMessage(msg, isComplete, percentage); } void sendUpdateMessage(const char* message, bool complete, int progress = -1) { if(eventProgress && eventProgress->count() > 0) { // This is for the web client and not the BLE client JsonDocument jsonDoc; jsonDoc["message"] = message; jsonDoc["complete"] = complete; jsonDoc["progress"] = progress; String strMessage; serializeJson(jsonDoc, strMessage); eventProgress->send(strMessage.c_str(), "update", millis()); } else{ ESP_LOGW(TAG, "No clients connected to event source"); } bleUpgrade_send_message(message); } // Convenience functions for setting update mode void setGlobalUpdateMode(UpdateMode mode) { g_UpdateMode = mode; ESP_LOGI(TAG, "Global update mode set to: %s", mode == UpdateMode::UPDATE_FILES_ONLY ? "UPDATE_FILES_ONLY" : mode == UpdateMode::UPDATE_FIRMWARE_ONLY ? "UPDATE_FIRMWARE_ONLY" : "UPDATE_BOTH"); } UpdateMode getGlobalUpdateMode() { return g_UpdateMode; } void setUpdateModeFilesOnly() { setGlobalUpdateMode(UpdateMode::UPDATE_FILES_ONLY); } void setUpdateModeFirmwareOnly() { setGlobalUpdateMode(UpdateMode::UPDATE_FIRMWARE_ONLY); } void setUpdateModeBoth() { setGlobalUpdateMode(UpdateMode::UPDATE_BOTH); }