wifi-densepose/firmware/esp32-csi-node/main/ota_update.c

347 lines
12 KiB
C

/**
* @file ota_update.c
* @brief HTTP OTA firmware update for ESP32-S3 CSI Node.
*
* Uses ESP-IDF's native OTA API with rollback support.
* The HTTP server runs on port 8032 and accepts:
* POST /ota — firmware binary payload (application/octet-stream)
* GET /ota/status — current firmware version and partition info
*/
#include "ota_update.h"
#include <string.h>
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_http_server.h"
#include "esp_app_desc.h"
#include "nvs_flash.h"
#include "nvs.h"
static const char *TAG = "ota_update";
/** OTA HTTP server port. */
#define OTA_PORT 8032
/** Maximum firmware size (900 KB — matches CI binary size gate). */
#define OTA_MAX_SIZE (900 * 1024)
/** NVS namespace and key for the OTA pre-shared key. */
#define OTA_NVS_NAMESPACE "security"
#define OTA_NVS_KEY "ota_psk"
/** Maximum PSK length (hex-encoded SHA-256). */
#define OTA_PSK_MAX_LEN 65
/** Cached PSK loaded from NVS at init time. Empty = auth disabled. */
static char s_ota_psk[OTA_PSK_MAX_LEN] = {0};
/**
* ADR-050: Verify the Authorization header contains the correct PSK.
* Returns true if auth is disabled (no PSK provisioned) or if the
* Bearer token matches the stored PSK.
*/
static bool ota_check_auth(httpd_req_t *req)
{
if (s_ota_psk[0] == '\0') {
/* No PSK provisioned — auth disabled (permissive for dev). */
return true;
}
char auth_header[128] = {0};
if (httpd_req_get_hdr_value_str(req, "Authorization", auth_header,
sizeof(auth_header)) != ESP_OK) {
return false;
}
/* Expect "Bearer <psk>" */
const char *prefix = "Bearer ";
if (strncmp(auth_header, prefix, strlen(prefix)) != 0) {
return false;
}
const char *token = auth_header + strlen(prefix);
/* Constant-time comparison to prevent timing attacks. */
size_t psk_len = strlen(s_ota_psk);
size_t tok_len = strlen(token);
if (psk_len != tok_len) return false;
volatile uint8_t result = 0;
for (size_t i = 0; i < psk_len; i++) {
result |= (uint8_t)(s_ota_psk[i] ^ token[i]);
}
return result == 0;
}
/**
* GET /ota/status — return firmware version and partition info.
*/
static esp_err_t ota_status_handler(httpd_req_t *req)
{
const esp_app_desc_t *app = esp_app_get_description();
const esp_partition_t *running = esp_ota_get_running_partition();
const esp_partition_t *update = esp_ota_get_next_update_partition(NULL);
char response[512];
int len = snprintf(response, sizeof(response),
"{\"version\":\"%s\",\"date\":\"%s\",\"time\":\"%s\","
"\"running_partition\":\"%s\",\"next_partition\":\"%s\","
"\"max_size\":%d}",
app->version, app->date, app->time,
running ? running->label : "unknown",
update ? update->label : "none",
OTA_MAX_SIZE);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, response, len);
return ESP_OK;
}
/**
* POST /ota/recalibrate — clear cached gain-lock NVS keys and reboot.
*
* ADR-109: lets the operator force a full gain-lock re-calibration from
* the server without a USB connection. Erases csi_cfg/gl_agc, gl_fft, and
* gl_ap_mac (ADR-111), then calls esp_restart(). Next boot finds no NVS
* cache and runs the 300-packet calibration as if it were a fresh device.
*/
static esp_err_t ota_recalibrate_handler(httpd_req_t *req)
{
if (!ota_check_auth(req)) {
ESP_LOGW(TAG, "/ota/recalibrate rejected: authentication failed");
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
"Authentication required. Use: Authorization: Bearer <psk>");
return ESP_FAIL;
}
nvs_handle_t h;
esp_err_t err = nvs_open("csi_cfg", NVS_READWRITE, &h);
if (err != ESP_OK) {
ESP_LOGE(TAG, "/ota/recalibrate: nvs_open(csi_cfg) failed: %s",
esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"NVS open failed");
return ESP_FAIL;
}
/* Erase all three keys defensively — ignore individual ESP_ERR_NVS_NOT_FOUND
* (key already absent on a never-calibrated device). */
(void)nvs_erase_key(h, "gl_agc");
(void)nvs_erase_key(h, "gl_fft");
(void)nvs_erase_key(h, "gl_ap_mac");
err = nvs_commit(h);
nvs_close(h);
if (err != ESP_OK) {
ESP_LOGE(TAG, "/ota/recalibrate: nvs_commit failed: %s",
esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"NVS commit failed");
return ESP_FAIL;
}
ESP_LOGI(TAG, "/ota/recalibrate: gain-lock NVS cleared; rebooting in 1s");
const char *resp =
"{\"status\":\"ok\",\"message\":\"gain-lock NVS cleared; rebooting\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK; /* unreachable */
}
/**
* POST /ota — receive and flash firmware binary.
*/
static esp_err_t ota_upload_handler(httpd_req_t *req)
{
/* ADR-050: Authenticate before accepting firmware upload. */
if (!ota_check_auth(req)) {
ESP_LOGW(TAG, "OTA upload rejected: authentication failed");
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
"Authentication required. Use: Authorization: Bearer <psk>");
return ESP_FAIL;
}
ESP_LOGI(TAG, "OTA update started, content_length=%d", req->content_len);
if (req->content_len <= 0 || req->content_len > OTA_MAX_SIZE) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Invalid firmware size (must be 1B - 900KB)");
return ESP_FAIL;
}
const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"No OTA partition available");
return ESP_FAIL;
}
esp_ota_handle_t ota_handle;
/* Issue #556: use OTA_SIZE_UNKNOWN (full partition erase) instead of
* OTA_WITH_SEQUENTIAL_WRITES. When the new image is smaller than the
* one previously written to the target slot, sequential writes leave
* the tail of the old code in place. The image header SHA covers
* only the declared image span, but residual code at stale offsets
* can still be reached via IRAM jump tables / .literal pools on some
* v5.2 ABIs and crash the new app on first boot, which then looks
* like "OTA didn't take". Full erase up-front avoids this entirely
* at the cost of one extra ~1.5 s erase before write starts. */
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &ota_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"OTA begin failed");
return ESP_FAIL;
}
/* Read firmware in chunks. */
char buf[1024];
int received = 0;
int total = 0;
while (total < req->content_len) {
received = httpd_req_recv(req, buf, sizeof(buf));
if (received <= 0) {
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
continue; /* Retry on timeout. */
}
ESP_LOGE(TAG, "OTA receive error at byte %d", total);
esp_ota_abort(ota_handle);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Receive error");
return ESP_FAIL;
}
err = esp_ota_write(ota_handle, buf, received);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_write failed at byte %d: %s",
total, esp_err_to_name(err));
esp_ota_abort(ota_handle);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"OTA write failed");
return ESP_FAIL;
}
total += received;
if ((total % (64 * 1024)) == 0) {
ESP_LOGI(TAG, "OTA progress: %d / %d bytes (%.0f%%)",
total, req->content_len,
(float)total * 100.0f / (float)req->content_len);
}
}
err = esp_ota_end(ota_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"OTA validation failed");
return ESP_FAIL;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Set boot partition failed");
return ESP_FAIL;
}
ESP_LOGI(TAG, "OTA update successful! Rebooting to partition '%s'...",
update_partition->label);
const char *resp = "{\"status\":\"ok\",\"message\":\"OTA update successful. Rebooting...\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
/* Delay briefly to let the response flush, then reboot. */
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK; /* Never reached. */
}
/** Internal: start the HTTP server and register OTA endpoints. */
static esp_err_t ota_start_server(httpd_handle_t *out_handle)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = OTA_PORT;
config.max_uri_handlers = 12; /* Extra slots for WASM endpoints (ADR-040). */
/* Increase receive timeout for large uploads. */
config.recv_wait_timeout = 30;
/* Issue #556: httpd default stack is 4096 B, which overflows during
* esp_ota_end()'s image-verify (SHA256 streaming + mmap segment walk
* eats ~3 KB on top of the request handler frame). Empirically observed
* "***ERROR*** A stack overflow in task httpd has been detected"
* immediately after esp_image: segment dumps when OTA reaches verify.
* 8 KB gives a clean margin without hurting the typical idle case. */
config.stack_size = 8192;
httpd_handle_t server = NULL;
esp_err_t err = httpd_start(&server, &config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start OTA HTTP server on port %d: %s",
OTA_PORT, esp_err_to_name(err));
if (out_handle) *out_handle = NULL;
return err;
}
httpd_uri_t status_uri = {
.uri = "/ota/status",
.method = HTTP_GET,
.handler = ota_status_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(server, &status_uri);
httpd_uri_t upload_uri = {
.uri = "/ota",
.method = HTTP_POST,
.handler = ota_upload_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(server, &upload_uri);
/* ADR-109: REST trigger for full gain-lock re-calibration. */
httpd_uri_t recalibrate_uri = {
.uri = "/ota/recalibrate",
.method = HTTP_POST,
.handler = ota_recalibrate_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(server, &recalibrate_uri);
ESP_LOGI(TAG, "OTA HTTP server started on port %d", OTA_PORT);
ESP_LOGI(TAG, " GET /ota/status — firmware version info");
ESP_LOGI(TAG, " POST /ota — upload new firmware binary");
ESP_LOGI(TAG, " POST /ota/recalibrate — clear gain-lock NVS + reboot");
if (out_handle) *out_handle = server;
return ESP_OK;
}
esp_err_t ota_update_init(void)
{
/* ADR-050: Load OTA PSK from NVS if provisioned. */
nvs_handle_t nvs;
if (nvs_open(OTA_NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(s_ota_psk);
if (nvs_get_str(nvs, OTA_NVS_KEY, s_ota_psk, &len) == ESP_OK) {
ESP_LOGI(TAG, "OTA PSK loaded from NVS (%d chars) — authentication enabled", (int)len - 1);
} else {
ESP_LOGW(TAG, "No OTA PSK in NVS — OTA authentication DISABLED (provision with nvs_set)");
}
nvs_close(nvs);
} else {
ESP_LOGW(TAG, "NVS namespace '%s' not found — OTA authentication DISABLED", OTA_NVS_NAMESPACE);
}
return ota_start_server(NULL);
}
esp_err_t ota_update_init_ex(void **out_server)
{
return ota_start_server((httpd_handle_t *)out_server);
}