spaxel/firmware/main/websocket.c
jedarden 0aa6046f82 feat: implement firmware LED blink handler for identify command
- Add led_init() call during firmware initialization in app_main()
- Add led_stop_blink() call on WebSocket disconnect
- LED GPIO configurable via CONFIG_SPAXEL_LED_GPIO (default GPIO8)
- Blink runs in FreeRTOS task at ~5Hz (100ms on/100ms off)
- Any running blink is cancelled when new identify message received

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 07:01:42 -04:00

803 lines
25 KiB
C

#include "websocket.h"
#include "spaxel.h"
#include "csi.h"
#include "wifi.h"
#include "ntp.h"
#include "led.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_system.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "mbedtls/sha256.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semaphore.h"
#include "cJSON.h"
#include <string.h>
#include <strings.h>
// ESP-IDF WebSocket client
#include "esp_websocket_client.h"
static const char *TAG = "ws";
static esp_websocket_client_handle_t s_ws = NULL;
static SemaphoreHandle_t s_tx_mutex = NULL;
static volatile bool s_connected = false;
// OTA state
static char s_ota_url[256] = {0};
static char s_ota_sha256[65] = {0};
static char s_ota_version[32] = {0};
// OTA rollback confirmation — set once we receive a role message after connecting.
// Cancels the automatic rollback timer in the ESP-IDF OTA framework.
static bool s_ota_confirmed = false;
// One-shot timer: if role is not received within 60 s of connection,
// the new OTA partition stays unconfirmed and the bootloader will
// roll back to the previous partition on the next reset.
static esp_timer_handle_t s_ota_valid_timer = NULL;
#define SPAXEL_OTA_VALID_TIMEOUT_S 60
static void ota_validation_timeout_cb(void *arg) {
if (!s_ota_confirmed) {
ESP_LOGW(TAG, "OTA validation: timed out, rollback on next reset");
}
}
static bool is_running_ota_partition(void) {
const esp_partition_t *running = esp_ota_get_running_partition();
return running != NULL &&
running->type == ESP_PARTITION_TYPE_APP &&
running->subtype != ESP_PARTITION_SUBTYPE_APP_FACTORY;
}
static void start_ota_validation_timer(void) {
if (s_ota_valid_timer == NULL) {
esp_timer_create_args_t timer_args = {
.callback = ota_validation_timeout_cb,
.name = "ota_valid",
};
esp_timer_create(&timer_args, &s_ota_valid_timer);
}
esp_timer_start_once(s_ota_valid_timer, SPAXEL_OTA_VALID_TIMEOUT_S * 1000000ULL);
ESP_LOGI(TAG, "OTA validation: waiting for role message (timeout %ds)",
SPAXEL_OTA_VALID_TIMEOUT_S);
}
static void stop_ota_validation_timer(void) {
if (s_ota_valid_timer != NULL) {
esp_timer_stop(s_ota_valid_timer);
}
}
// Forward declarations
static void ws_event_handler(void *args, esp_event_base_t base,
int32_t id, void *data);
static void handle_role_msg(cJSON *root);
static void handle_config_msg(cJSON *root);
static void handle_ota_msg(cJSON *root);
static void handle_reboot_msg(cJSON *root);
static void handle_identify_msg(cJSON *root);
static void ota_task(void *arg);
esp_err_t websocket_init(void) {
s_tx_mutex = xSemaphoreCreateMutex();
if (!s_tx_mutex) {
ESP_LOGE(TAG, "Failed to create TX mutex");
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
bool websocket_connect(const char *host, uint16_t port) {
if (s_ws) {
websocket_disconnect();
vTaskDelay(pdMS_TO_TICKS(100));
}
// Build WebSocket URI
char uri[128];
snprintf(uri, sizeof(uri), "ws://%s:%d%s", host, port, SPAXEL_WS_PATH);
ESP_LOGI(TAG, "Connecting to %s", uri);
// Configure WebSocket client
esp_websocket_client_config_t cfg = {
.uri = uri,
.reconnect_timeout_ms = 5000,
.network_timeout_ms = 30000,
.ping_interval_sec = 30,
.task_stack = 8192,
.buffer_size = 2048,
};
// Add auth header if we have a token
// Note: esp_websocket_client doesn't directly support custom headers,
// so we'd use the URI query param or implement a custom handshake
// For now, we'll use a simplified approach
s_ws = esp_websocket_client_init(&cfg);
if (!s_ws) {
ESP_LOGE(TAG, "Failed to init WebSocket client");
return false;
}
// Register event handlers
esp_websocket_register_events(s_ws, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL);
// Connect
esp_err_t err = esp_websocket_client_start(s_ws);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start WebSocket: %s", esp_err_to_name(err));
esp_websocket_client_destroy(s_ws);
s_ws = NULL;
return false;
}
// Wait for connection
int timeout = 50; // 5 seconds
while (!s_connected && timeout-- > 0) {
vTaskDelay(pdMS_TO_TICKS(100));
}
if (!s_connected) {
ESP_LOGE(TAG, "WebSocket connection timeout");
websocket_disconnect();
return false;
}
// Send hello message
websocket_send_hello();
// If booting from an unconfirmed OTA partition, start a 60 s timer.
// If the mothership doesn't send a role message within that window,
// the partition stays unconfirmed and the bootloader will roll back
// to the previous firmware on the next reset.
if (!s_ota_confirmed && is_running_ota_partition()) {
start_ota_validation_timer();
}
return true;
}
void websocket_disconnect(void) {
if (s_ws) {
esp_websocket_client_stop(s_ws);
esp_websocket_client_destroy(s_ws);
s_ws = NULL;
}
s_connected = false;
stop_ota_validation_timer();
// Stop any running LED blink on disconnect
led_stop_blink();
}
bool websocket_is_connected(void) {
return s_connected && s_ws != NULL;
}
static void ws_event_handler(void *args, esp_event_base_t base,
int32_t id, void *data) {
esp_websocket_event_data_t *event = (esp_websocket_event_data_t *)data;
switch (id) {
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "WebSocket connected");
s_connected = true;
break;
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "WebSocket disconnected");
s_connected = false;
xEventGroupSetBits(g_state.events, SPAXEL_EVENT_WS_DISCONNECTED);
break;
case WEBSOCKET_EVENT_DATA:
// Handle incoming message
if (event->data_len > 0 && event->op_code == 0x01) {
// Text frame (JSON)
char *json = malloc(event->data_len + 1);
if (json) {
memcpy(json, event->data_ptr, event->data_len);
json[event->data_len] = '\0';
websocket_handle_message(json, event->data_len);
free(json);
}
}
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGE(TAG, "WebSocket error");
break;
default:
break;
}
}
esp_err_t websocket_send_csi(const uint8_t *peer_mac, uint64_t timestamp_us,
int8_t rssi, int8_t noise_floor, uint8_t channel,
const int8_t *iq_data, uint8_t n_sub) {
if (!s_connected || !s_ws) {
return ESP_ERR_INVALID_STATE;
}
// Build binary frame
// Header: 24 bytes
// Payload: n_sub * 2 bytes
size_t frame_len = SPAXEL_FRAME_HEADER_SIZE + (n_sub * 2);
uint8_t *frame = malloc(frame_len);
if (!frame) {
return ESP_ERR_NO_MEM;
}
// Pack header (little-endian)
memcpy(frame + 0, g_state.mac, 6); // node_mac
memcpy(frame + 6, peer_mac, 6); // peer_mac
memcpy(frame + 12, &timestamp_us, 8); // timestamp_us
frame[20] = (uint8_t)rssi; // rssi (int8 as uint8)
frame[21] = (uint8_t)noise_floor; // noise_floor
frame[22] = channel; // channel
frame[23] = n_sub; // n_sub
// Pack I/Q payload
memcpy(frame + 24, iq_data, n_sub * 2);
// Send binary frame
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
int sent = esp_websocket_client_send_bin(s_ws, (char *)frame, frame_len,
portMAX_DELAY);
xSemaphoreGive(s_tx_mutex);
free(frame);
return (sent == frame_len) ? ESP_OK : ESP_FAIL;
}
esp_err_t websocket_send_hello(void) {
if (!s_connected || !s_ws) {
return ESP_ERR_INVALID_STATE;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "hello");
char mac_str[18];
mac_to_str(g_state.mac, mac_str, sizeof(mac_str));
cJSON_AddStringToObject(root, "mac", mac_str);
if (strlen(g_state.node_id) > 0) {
cJSON_AddStringToObject(root, "node_id", g_state.node_id);
}
// Firmware version (from build)
cJSON_AddStringToObject(root, "firmware_version", "1.0.0");
// Capabilities
cJSON *caps = cJSON_CreateArray();
cJSON_AddItemToArray(caps, cJSON_CreateString("csi"));
cJSON_AddItemToArray(caps, cJSON_CreateString("ble"));
cJSON_AddItemToArray(caps, cJSON_CreateString("tx"));
cJSON_AddItemToArray(caps, cJSON_CreateString("rx"));
cJSON_AddItemToObject(root, "capabilities", caps);
cJSON_AddStringToObject(root, "chip", "ESP32-S3");
cJSON_AddNumberToObject(root, "flash_mb", 16);
cJSON_AddNumberToObject(root, "uptime_ms", esp_timer_get_time() / 1000);
// AP BSSID and channel (for passive radar auto-detection)
uint8_t ap_bssid[6];
if (wifi_get_ap_bssid(ap_bssid)) {
char bssid_str[18];
mac_to_str(ap_bssid, bssid_str, sizeof(bssid_str));
cJSON_AddStringToObject(root, "ap_bssid", bssid_str);
cJSON_AddNumberToObject(root, "ap_channel", wifi_get_channel());
}
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) {
return ESP_ERR_NO_MEM;
}
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
int sent = esp_websocket_client_send_text(s_ws, json, strlen(json),
portMAX_DELAY);
xSemaphoreGive(s_tx_mutex);
free(json);
return (sent > 0) ? ESP_OK : ESP_FAIL;
}
esp_err_t websocket_send_health(void) {
if (!s_connected || !s_ws) {
return ESP_ERR_INVALID_STATE;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "health");
char mac_str[18];
mac_to_str(g_state.mac, mac_str, sizeof(mac_str));
cJSON_AddStringToObject(root, "mac", mac_str);
cJSON_AddNumberToObject(root, "timestamp_ms",
esp_timer_get_time() / 1000);
cJSON_AddNumberToObject(root, "free_heap_bytes",
esp_get_free_heap_size());
cJSON_AddNumberToObject(root, "wifi_rssi_dbm", wifi_get_rssi());
cJSON_AddNumberToObject(root, "uptime_ms",
esp_timer_get_time() / 1000);
// Temperature (if available)
extern float esp_temp_get_celsius(void);
cJSON_AddNumberToObject(root, "temperature_c", esp_temp_get_celsius());
cJSON_AddNumberToObject(root, "csi_rate_hz", g_state.packet_rate);
cJSON_AddNumberToObject(root, "wifi_channel", wifi_get_channel());
// Get IP address
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
char ip_str[16];
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
cJSON_AddStringToObject(root, "ip", ip_str);
}
}
// NTP sync status
cJSON_AddBoolToObject(root, "ntp_synced", ntp_is_synced());
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) {
return ESP_ERR_NO_MEM;
}
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
int sent = esp_websocket_client_send_text(s_ws, json, strlen(json),
portMAX_DELAY);
xSemaphoreGive(s_tx_mutex);
free(json);
return (sent > 0) ? ESP_OK : ESP_FAIL;
}
esp_err_t websocket_send_ble(const char *devices_json) {
if (!s_connected || !s_ws || !devices_json) {
return ESP_ERR_INVALID_STATE;
}
// Build BLE message
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "ble");
char mac_str[18];
mac_to_str(g_state.mac, mac_str, sizeof(mac_str));
cJSON_AddStringToObject(root, "mac", mac_str);
cJSON_AddNumberToObject(root, "timestamp_ms",
esp_timer_get_time() / 1000);
// Parse and add devices array
cJSON *devices = cJSON_Parse(devices_json);
if (devices) {
cJSON_AddItemToObject(root, "devices", devices);
}
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) {
return ESP_ERR_NO_MEM;
}
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
int sent = esp_websocket_client_send_text(s_ws, json, strlen(json),
portMAX_DELAY);
xSemaphoreGive(s_tx_mutex);
free(json);
return (sent > 0) ? ESP_OK : ESP_FAIL;
}
esp_err_t websocket_send_motion_hint(float variance) {
if (!s_connected || !s_ws) {
return ESP_ERR_INVALID_STATE;
}
// Rate-limit to at most 1 hint per second.
static int64_t s_last_hint_us = 0;
int64_t now_us = esp_timer_get_time();
if (now_us - s_last_hint_us < 1000000) {
return ESP_OK;
}
s_last_hint_us = now_us;
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "motion_hint");
char mac_str[18];
mac_to_str(g_state.mac, mac_str, sizeof(mac_str));
cJSON_AddStringToObject(root, "mac", mac_str);
cJSON_AddNumberToObject(root, "timestamp_ms", now_us / 1000);
cJSON_AddNumberToObject(root, "variance", variance);
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) {
return ESP_ERR_NO_MEM;
}
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
int sent = esp_websocket_client_send_text(s_ws, json, strlen(json),
portMAX_DELAY);
xSemaphoreGive(s_tx_mutex);
free(json);
return (sent > 0) ? ESP_OK : ESP_FAIL;
}
esp_err_t websocket_send_ota_status(const char *state, uint8_t progress_pct,
const char *error) {
if (!s_connected || !s_ws) {
return ESP_ERR_INVALID_STATE;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "ota_status");
char mac_str[18];
mac_to_str(g_state.mac, mac_str, sizeof(mac_str));
cJSON_AddStringToObject(root, "mac", mac_str);
cJSON_AddStringToObject(root, "state", state);
cJSON_AddNumberToObject(root, "progress_pct", progress_pct);
if (error) {
cJSON_AddStringToObject(root, "error", error);
}
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) {
return ESP_ERR_NO_MEM;
}
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
int sent = esp_websocket_client_send_text(s_ws, json, strlen(json),
portMAX_DELAY);
xSemaphoreGive(s_tx_mutex);
free(json);
return (sent > 0) ? ESP_OK : ESP_FAIL;
}
void websocket_handle_message(const char *json, size_t len) {
cJSON *root = cJSON_ParseWithLength(json, len);
if (!root) {
ESP_LOGW(TAG, "Failed to parse JSON message");
return;
}
cJSON *type = cJSON_GetObjectItem(root, "type");
if (!type || !cJSON_IsString(type)) {
cJSON_Delete(root);
return;
}
ESP_LOGD(TAG, "Received message type: %s", type->valuestring);
if (strcmp(type->valuestring, "role") == 0) {
handle_role_msg(root);
} else if (strcmp(type->valuestring, "config") == 0) {
handle_config_msg(root);
} else if (strcmp(type->valuestring, "ota") == 0) {
handle_ota_msg(root);
} else if (strcmp(type->valuestring, "reboot") == 0) {
handle_reboot_msg(root);
} else if (strcmp(type->valuestring, "identify") == 0) {
handle_identify_msg(root);
} else if (strcmp(type->valuestring, "reject") == 0) {
cJSON *reason = cJSON_GetObjectItem(root, "reason");
ESP_LOGE(TAG, "Rejected by mothership: %s",
reason ? reason->valuestring : "unknown");
websocket_disconnect();
}
// Unknown types are silently ignored (forward-compatible)
cJSON_Delete(root);
}
static void handle_role_msg(cJSON *root) {
cJSON *role = cJSON_GetObjectItem(root, "role");
if (!role || !cJSON_IsString(role)) {
return;
}
// Confirm OTA partition valid on first role message received after boot.
// This means we successfully connected and the mothership accepted us.
if (!s_ota_confirmed) {
s_ota_confirmed = true;
stop_ota_validation_timer();
if (is_running_ota_partition()) {
esp_err_t err = esp_ota_mark_app_valid_cancel_rollback();
if (err == ESP_OK) {
ESP_LOGI(TAG, "OTA validation: marked valid after role received");
} else {
ESP_LOGW(TAG, "OTA validation: failed to mark valid: %s",
esp_err_to_name(err));
}
}
}
const char *role_str = role->valuestring;
node_role_t new_role = g_state.role;
if (strcmp(role_str, "tx") == 0) {
new_role = NODE_ROLE_TX;
} else if (strcmp(role_str, "rx") == 0) {
new_role = NODE_ROLE_RX;
} else if (strcmp(role_str, "tx_rx") == 0) {
new_role = NODE_ROLE_TX_RX;
} else if (strcmp(role_str, "passive") == 0) {
new_role = NODE_ROLE_PASSIVE;
// Get passive BSSID
cJSON *bssid = cJSON_GetObjectItem(root, "passive_bssid");
if (bssid && cJSON_IsString(bssid)) {
str_to_mac(bssid->valuestring, g_state.passive_bssid);
}
} else if (strcmp(role_str, "idle") == 0) {
new_role = NODE_ROLE_IDLE;
}
if (new_role != g_state.role) {
g_state.role = new_role;
xEventGroupSetBits(g_state.events, SPAXEL_EVENT_ROLE_CHANGED);
ESP_LOGI(TAG, "Role changed to: %s", node_role_str(new_role));
}
}
static void handle_config_msg(cJSON *root) {
bool changed = false;
// Rate
cJSON *rate = cJSON_GetObjectItem(root, "rate_hz");
if (rate && cJSON_IsNumber(rate)) {
uint8_t new_rate = (uint8_t)rate->valueint;
if (new_rate >= 1 && new_rate <= 100 && new_rate != g_state.packet_rate) {
g_state.packet_rate = new_rate;
changed = true;
ESP_LOGI(TAG, "Rate changed to: %d Hz", new_rate);
}
}
// Variance threshold (for on-device motion hints)
cJSON *var_thresh = cJSON_GetObjectItem(root, "variance_threshold");
if (var_thresh && cJSON_IsNumber(var_thresh)) {
// Store in global for CSI module to use
extern float g_variance_threshold;
g_variance_threshold = (float)var_thresh->valuedouble;
}
// NTP server (runtime reconfiguration)
cJSON *ntp = cJSON_GetObjectItem(root, "ntp_server");
if (ntp && cJSON_IsString(ntp) && strlen(ntp->valuestring) > 0) {
strncpy(g_state.ntp_server, ntp->valuestring, sizeof(g_state.ntp_server) - 1);
g_state.ntp_server[sizeof(g_state.ntp_server) - 1] = '\0';
ESP_LOGI(TAG, "NTP server changed to: %s", g_state.ntp_server);
ntp_start_sync(g_state.ntp_server);
}
if (changed) {
csi_set_rate(g_state.packet_rate);
}
}
static void handle_ota_msg(cJSON *root) {
cJSON *url = cJSON_GetObjectItem(root, "url");
cJSON *sha256 = cJSON_GetObjectItem(root, "sha256");
cJSON *version = cJSON_GetObjectItem(root, "version");
if (!url || !cJSON_IsString(url)) {
ESP_LOGW(TAG, "OTA message missing URL");
return;
}
strncpy(s_ota_url, url->valuestring, sizeof(s_ota_url) - 1);
if (sha256 && cJSON_IsString(sha256)) {
strncpy(s_ota_sha256, sha256->valuestring, sizeof(s_ota_sha256) - 1);
}
if (version && cJSON_IsString(version)) {
strncpy(s_ota_version, version->valuestring, sizeof(s_ota_version) - 1);
}
ESP_LOGI(TAG, "OTA triggered: %s", s_ota_url);
// Start OTA task
xTaskCreate(ota_task, "ota", 16384, NULL, 5, NULL);
}
static void handle_reboot_msg(cJSON *root) {
cJSON *delay = cJSON_GetObjectItem(root, "delay_ms");
uint32_t delay_ms = 1000;
if (delay && cJSON_IsNumber(delay)) {
delay_ms = (uint32_t)delay->valueint;
}
ESP_LOGI(TAG, "Reboot requested in %lu ms", delay_ms);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
esp_restart();
}
static void handle_identify_msg(cJSON *root) {
cJSON *duration = cJSON_GetObjectItem(root, "duration_ms");
uint32_t duration_ms = 5000;
if (duration && cJSON_IsNumber(duration)) {
duration_ms = (uint32_t)duration->valueint;
}
ESP_LOGI(TAG, "Identify: blinking LED for %lu ms", duration_ms);
// Start LED blink
esp_err_t err = led_blink_identify(duration_ms);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start LED blink: %s", esp_err_to_name(err));
}
}
static void ota_task(void *arg) {
ESP_LOGI(TAG, "Starting OTA download: %s", s_ota_url);
websocket_send_ota_status("downloading", 0, NULL);
esp_http_client_config_t http_cfg = {
.url = s_ota_url,
.timeout_ms = 30000,
.buffer_size = 4096,
};
esp_http_client_handle_t http = esp_http_client_init(&http_cfg);
if (!http) {
websocket_send_ota_status("failed", 0, "download_failed");
vTaskDelete(NULL);
return;
}
esp_err_t err = esp_http_client_open(http, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(http);
websocket_send_ota_status("failed", 0, "download_failed");
vTaskDelete(NULL);
return;
}
int content_len = esp_http_client_fetch_headers(http);
if (content_len <= 0) {
esp_http_client_cleanup(http);
websocket_send_ota_status("failed", 0, "download_failed");
vTaskDelete(NULL);
return;
}
// Find next OTA partition
const esp_partition_t *update_part = esp_ota_get_next_update_partition(NULL);
if (!update_part) {
esp_http_client_cleanup(http);
websocket_send_ota_status("failed", 0, "no_partition");
vTaskDelete(NULL);
return;
}
// Begin OTA
esp_ota_handle_t ota_handle;
err = esp_ota_begin(update_part, OTA_SIZE_UNKNOWN, &ota_handle);
if (err != ESP_OK) {
esp_http_client_cleanup(http);
websocket_send_ota_status("failed", 0, "write_failed");
vTaskDelete(NULL);
return;
}
// Initialize SHA-256 for verification
mbedtls_sha256_context sha_ctx;
bool do_sha_verify = (strlen(s_ota_sha256) == 64);
if (do_sha_verify) {
mbedtls_sha256_init(&sha_ctx);
mbedtls_sha256_starts(&sha_ctx, 0); // 0 = SHA-256
}
// Download and write
char *buf = malloc(4096);
int total_read = 0;
int read;
while ((read = esp_http_client_read(http, buf, 4096)) > 0) {
// Update SHA-256 hash if verifying
if (do_sha_verify) {
mbedtls_sha256_update(&sha_ctx, (unsigned char *)buf, read);
}
err = esp_ota_write(ota_handle, buf, read);
if (err != ESP_OK) {
free(buf);
if (do_sha_verify) mbedtls_sha256_free(&sha_ctx);
esp_ota_abort(ota_handle);
esp_http_client_cleanup(http);
websocket_send_ota_status("failed", 0, "write_failed");
vTaskDelete(NULL);
return;
}
total_read += read;
uint8_t progress = (uint8_t)((total_read * 100) / content_len);
websocket_send_ota_status("downloading", progress, NULL);
}
free(buf);
esp_http_client_cleanup(http);
// Verify and complete
websocket_send_ota_status("verifying", 100, NULL);
// SHA-256 verification
if (do_sha_verify) {
unsigned char hash[32];
mbedtls_sha256_finish(&sha_ctx, hash);
mbedtls_sha256_free(&sha_ctx);
// Convert binary hash to hex string
char hash_hex[65];
for (int i = 0; i < 32; i++) {
sprintf(hash_hex + (i * 2), "%02x", hash[i]);
}
hash_hex[64] = '\0';
// Compare with expected hash (case-insensitive)
if (strcasecmp(hash_hex, s_ota_sha256) != 0) {
ESP_LOGE(TAG, "SHA-256 mismatch: expected %s, got %s", s_ota_sha256, hash_hex);
esp_ota_abort(ota_handle);
websocket_send_ota_status("failed", 0, "sha256_mismatch");
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "SHA-256 verified: %s", hash_hex);
}
err = esp_ota_end(ota_handle);
if (err != ESP_OK) {
websocket_send_ota_status("failed", 0, "verify_failed");
vTaskDelete(NULL);
return;
}
// Set boot partition
err = esp_ota_set_boot_partition(update_part);
if (err != ESP_OK) {
websocket_send_ota_status("failed", 0, "boot_partition_failed");
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "OTA complete, rebooting");
websocket_send_ota_status("rebooting", 100, NULL);
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
vTaskDelete(NULL);
}