249 lines
8.1 KiB
C
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;
|
|
}
|
|
}
|