#include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_ota_ops.h" #include "esp_http_client.h" #include "esp_https_ota.h" #include "esp_system.h" #include "esp_heap_caps.h" #include "esp_netif_sntp.h" #include "ota_manager.h" #include "socket_client.h" // for socket_send_raw() #include "state_manager.h" // for state_set_led() #include "esp_crt_bundle.h" // for esp_crt_bundle_attach() static const char *TAG = "ota_manager"; // Guard against concurrent OTA runs static volatile bool s_ota_running = false; // URL is copied here so the task owns its memory #define OTA_URL_MAX_LEN 256 static char s_ota_url[OTA_URL_MAX_LEN]; static esp_err_t ota_http_event(esp_http_client_event_t *evt) { if (evt->event_id == HTTP_EVENT_ERROR || evt->event_id == HTTP_EVENT_DISCONNECTED) { int tls_error = 0; int tls_flags = 0; esp_err_t tls_result = esp_http_client_get_and_clear_last_tls_error( evt->client, &tls_error, &tls_flags ); ESP_LOGE( "OTA", "HTTP failure: tls_result=%s (0x%x), " "tls_error=0x%x, tls_flags=0x%x, errno=%d", esp_err_to_name(tls_result), tls_result, tls_error, tls_flags, esp_http_client_get_errno(evt->client) ); } return ESP_OK; } /* ------------------------------------------------------------------ * OTA task * ------------------------------------------------------------------ */ static void ota_task(void *pvParam) { esp_err_t err; ESP_LOGI(TAG, "OTA task started. Firmware URL: %s", s_ota_url); // Pulse blue slowly during update state_set_led(LED_STATE_UPDATING); // ---- Configure HTTP client ---- esp_http_client_config_t http_cfg = { .url = s_ota_url, .event_handler = ota_http_event, .timeout_ms = 30000, .keep_alive_enable = true, .max_redirection_count = 5, .skip_cert_common_name_check = false, // Keep false to ensure SNI remains enabled for Cloudflare Host mapping .crt_bundle_attach = esp_crt_bundle_attach, // Use system root CAs to validate Cloudflare certificates }; esp_https_ota_config_t ota_cfg = { .http_config = &http_cfg, }; // Gate HTTPS OTA until system clock is synchronized (prevents TLS certificate validity window checks from failing) bool is_https = (strncmp(s_ota_url, "https", 5) == 0); time_t now; time(&now); if (is_https) { if (now < 1704067200) { ESP_LOGI(TAG, "Waiting for system clock synchronization (up to 15s)..."); esp_err_t sync_err = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(15000)); time(&now); if (sync_err != ESP_OK || now < 1704067200) { ESP_LOGE(TAG, "HTTPS OTA blocked: system clock not synchronized"); socket_send_ota_error("System clock not synchronized"); state_set_led(LED_STATE_CONNECTED); // restore LED state s_ota_running = false; vTaskDelete(NULL); return; } } } // Log diagnostics: exact URL, current Unix time, total heap, internal heap ESP_LOGI(TAG, "Diagnostics before OTA begin: URL=%s, unix_time=%ld, free_heap_total=%d, free_heap_internal=%d", s_ota_url, (long)now, (int)esp_get_free_heap_size(), (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); // Enable verbose TLS logging to pinpoint exactly where the handshake fails esp_log_level_set("esp-tls", ESP_LOG_VERBOSE); esp_log_level_set("mbedtls", ESP_LOG_VERBOSE); // ---- Begin OTA handle ---- esp_https_ota_handle_t ota_handle = NULL; err = esp_https_ota_begin(&ota_cfg, &ota_handle); if (err != ESP_OK || ota_handle == NULL) { char reason[64]; snprintf(reason, sizeof(reason), "esp_https_ota_begin failed: 0x%x", err); ESP_LOGE(TAG, "%s", reason); socket_send_ota_error(reason); state_set_led(LED_STATE_CONNECTED); // restore LED state s_ota_running = false; vTaskDelete(NULL); return; } // ---- Get image size for progress reporting ---- int image_size = esp_https_ota_get_image_size(ota_handle); if (image_size <= 0) { // Some servers don't send Content-Length; treat as unknown image_size = 0; } ESP_LOGI(TAG, "Firmware image size: %d bytes", image_size); // ---- Stream and flash ---- int bytes_written = 0; int last_reported_pct = -1; while (1) { err = esp_https_ota_perform(ota_handle); if (err == ESP_ERR_HTTPS_OTA_IN_PROGRESS) { bytes_written = esp_https_ota_get_image_len_read(ota_handle); // Report progress every 5% (or every ~32KB if size unknown) int pct = (image_size > 0) ? (int)((bytes_written * 100LL) / image_size) : -1; if (pct >= 0 && pct != last_reported_pct && (pct % 5 == 0)) { socket_send_ota_progress(pct, bytes_written, image_size); last_reported_pct = pct; ESP_LOGI(TAG, "OTA progress: %d%% (%d / %d bytes)", pct, bytes_written, image_size); } else if (pct < 0 && (bytes_written - (last_reported_pct * 32768)) >= 32768) { socket_send_ota_progress(0, bytes_written, 0); last_reported_pct = bytes_written / 32768; } continue; } if (err == ESP_OK) { // Download complete break; } // Any other error is fatal char reason[64]; snprintf(reason, sizeof(reason), "esp_https_ota_perform failed: 0x%x", err); ESP_LOGE(TAG, "%s", reason); socket_send_ota_error(reason); esp_https_ota_abort(ota_handle); state_set_led(LED_STATE_CONNECTED); // restore LED state s_ota_running = false; vTaskDelete(NULL); return; } bytes_written = esp_https_ota_get_image_len_read(ota_handle); // ---- Validate and commit ---- if (!esp_https_ota_is_complete_data_received(ota_handle)) { const char *reason = "Incomplete image received"; ESP_LOGE(TAG, "%s", reason); socket_send_ota_error(reason); esp_https_ota_abort(ota_handle); state_set_led(LED_STATE_CONNECTED); // restore LED state s_ota_running = false; vTaskDelete(NULL); return; } err = esp_https_ota_finish(ota_handle); if (err != ESP_OK) { char reason[64]; snprintf(reason, sizeof(reason), "esp_https_ota_finish failed: 0x%x", err); ESP_LOGE(TAG, "%s", reason); socket_send_ota_error(reason); state_set_led(LED_STATE_CONNECTED); // restore LED state s_ota_running = false; vTaskDelete(NULL); return; } // ---- Success ---- socket_send_ota_progress(100, bytes_written, bytes_written); ESP_LOGI(TAG, "OTA complete! %d bytes written. Rebooting in 500ms...", bytes_written); socket_send_ota_complete(); vTaskDelay(pdMS_TO_TICKS(500)); esp_restart(); } /* ------------------------------------------------------------------ * Public API * ------------------------------------------------------------------ */ void ota_start(const char *url) { if (s_ota_running) { ESP_LOGW(TAG, "OTA already in progress, ignoring request."); return; } if (url == NULL || strlen(url) == 0) { ESP_LOGE(TAG, "ota_start called with empty URL"); return; } // Copy URL before spawning task strlcpy(s_ota_url, url, sizeof(s_ota_url)); s_ota_running = true; ESP_LOGI(TAG, "Spawning OTA task for URL: %s", s_ota_url); BaseType_t ret = xTaskCreate( ota_task, "ota_task", 8192, // 8KB stack — enough for HTTP + flash ops NULL, 5, // Priority: slightly above normal tasks NULL ); if (ret != pdPASS) { ESP_LOGE(TAG, "Failed to create OTA task"); s_ota_running = false; } }