Files
SmartSpeaker/main/ota_manager.c
T

249 lines
8.1 KiB
C

#include <string.h>
#include <stdio.h>
#include <time.h>
#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;
}
}