feat(ota): add authenticated firmware update workflow

- report firmware versions at startup and every six hours with a fresh device JWT
- support optional HMAC headers and retry once after authorization rejection
- handle forced and confirmed updates through managed OTA state
- stream images to the inactive partition and verify size, SHA-256, and format
- coordinate OTA HTTPS activity with voice teardown and restoration
- confirm pending images and expose configuration, status, and integration docs
This commit is contained in:
chenjintao
2026-08-14 15:25:34 +08:00
parent 0cfb10252c
commit 5a18036ec6
17 changed files with 1860 additions and 8 deletions
+3
View File
@@ -3,6 +3,7 @@ set(platform_srcs
"src/runtime/platform_storage.c"
"src/runtime/platform_time.c"
"src/runtime/platform_https_client.c"
"src/runtime/platform_ota.c"
"src/runtime/platform_lan_http_server.c"
"src/runtime/platform_ble_gatt_server.c"
"src/platform_console_driver.c"
@@ -37,11 +38,13 @@ set(platform_priv_requires
esp_driver_spi
esp_event
esp_app_format
app_update
esp_http_client
esp_http_server
esp_lcd
esp_netif
esp_psram
heap
esp_system
esp_timer
esp_wifi
+18
View File
@@ -137,6 +137,11 @@ typedef struct {
int status_code;
} platform_https_response_t;
typedef void (*platform_ota_progress_callback_t)(uint64_t bytes_received,
uint64_t total_bytes,
void *user_data);
typedef bool (*platform_ota_cancel_callback_t)(void *user_data);
#define PLATFORM_BLE_UUID128_LEN 16
#define PLATFORM_BLE_GATT_NAME_MAX_LEN 31
#define PLATFORM_BLE_GATT_VALUE_MAX_LEN 512
@@ -252,6 +257,19 @@ bool platform_time_is_synced(void);
*/
esp_err_t platform_https_post_json(const platform_https_post_json_request_t *request,
platform_https_response_t *response);
esp_err_t platform_ota_install_image(const char *url,
const uint8_t expected_sha256[32],
uint64_t expected_size,
int timeout_ms,
platform_ota_progress_callback_t progress_cb,
platform_ota_cancel_callback_t cancel_cb,
void *user_data);
esp_err_t platform_ota_confirm_running_image(void);
esp_err_t platform_ota_get_memory_headroom(uint32_t *free_internal,
uint32_t *largest_internal,
uint32_t *largest_dma);
void platform_restart(void);
bool platform_https_is_connect_error(esp_err_t err);
/** Start a generic single-service BLE GATT peripheral. */
@@ -0,0 +1,298 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "platform.h"
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include "esp_crt_bundle.h"
#include "esp_heap_caps.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "mbedtls/sha256.h"
#define PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE 4096
#define PLATFORM_OTA_MAX_REDIRECTS 5
#define PLATFORM_OTA_IO_TIMEOUT_MS 10000
static const char *TAG = "platform_ota";
static bool platform_ota_deadline_expired(int64_t deadline_us)
{
return esp_timer_get_time() >= deadline_us;
}
static int platform_ota_remaining_timeout_ms(int64_t deadline_us)
{
int64_t remaining_us = deadline_us - esp_timer_get_time();
if (remaining_us <= 0) {
return 0;
}
int64_t remaining_ms = (remaining_us + 999) / 1000;
return remaining_ms < PLATFORM_OTA_IO_TIMEOUT_MS ?
(int)remaining_ms : PLATFORM_OTA_IO_TIMEOUT_MS;
}
esp_err_t platform_ota_install_image(const char *url,
const uint8_t expected_sha256[32],
uint64_t expected_size,
int timeout_ms,
platform_ota_progress_callback_t progress_cb,
platform_ota_cancel_callback_t cancel_cb,
void *user_data)
{
if (url == NULL || strncmp(url, "https://", 8) != 0 ||
expected_sha256 == NULL || expected_size == 0U || timeout_ms <= 0) {
return ESP_ERR_INVALID_ARG;
}
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
if (partition == NULL || expected_size > partition->size) {
ESP_LOGE(TAG,
"OTA image does not fit inactive partition: image=%" PRIu64,
expected_size);
return ESP_ERR_INVALID_SIZE;
}
const int64_t deadline_us = esp_timer_get_time() + (int64_t)timeout_ms * 1000;
int io_timeout_ms = timeout_ms < PLATFORM_OTA_IO_TIMEOUT_MS ?
timeout_ms : PLATFORM_OTA_IO_TIMEOUT_MS;
esp_http_client_config_t http_config = {
.url = url,
.method = HTTP_METHOD_GET,
.timeout_ms = io_timeout_ms,
.crt_bundle_attach = esp_crt_bundle_attach,
.buffer_size = PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE,
.max_redirection_count = PLATFORM_OTA_MAX_REDIRECTS,
.keep_alive_enable = true,
};
esp_http_client_handle_t client = esp_http_client_init(&http_config);
if (client == NULL) {
return ESP_ERR_NO_MEM;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGW(TAG, "OTA HTTPS open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return err;
}
io_timeout_ms = platform_ota_remaining_timeout_ms(deadline_us);
if (io_timeout_ms <= 0) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_TIMEOUT;
}
esp_http_client_set_timeout_ms(client, io_timeout_ms);
int64_t content_length = esp_http_client_fetch_headers(client);
if (content_length < 0) {
int transport_errno = esp_http_client_get_errno(client);
ESP_LOGW(TAG,
"OTA response header fetch failed: result=%" PRIi64 " errno=%d",
content_length,
transport_errno);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_INVALID_RESPONSE;
}
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGW(TAG, "OTA download returned HTTP %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_INVALID_RESPONSE;
}
if (content_length >= 0 && (uint64_t)content_length != expected_size) {
ESP_LOGE(TAG,
"OTA Content-Length mismatch: expected=%" PRIu64 " actual=%" PRIi64,
expected_size,
content_length);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_INVALID_SIZE;
}
esp_ota_handle_t ota_handle = 0;
err = esp_ota_begin(partition, (size_t)expected_size, &ota_handle);
if (err != ESP_OK) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
return err;
}
uint8_t *buffer = (uint8_t *)malloc(PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE);
if (buffer == NULL) {
esp_ota_abort(ota_handle);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_ERR_NO_MEM;
}
mbedtls_sha256_context sha_ctx;
mbedtls_sha256_init(&sha_ctx);
if (mbedtls_sha256_starts(&sha_ctx, 0) != 0) {
err = ESP_FAIL;
goto cleanup;
}
uint64_t received = 0U;
while (received < expected_size) {
bool cancelled = cancel_cb != NULL && cancel_cb(user_data);
if (cancelled || platform_ota_deadline_expired(deadline_us)) {
err = cancelled ? ESP_ERR_INVALID_STATE : ESP_ERR_TIMEOUT;
goto cleanup_sha;
}
io_timeout_ms = platform_ota_remaining_timeout_ms(deadline_us);
if (io_timeout_ms <= 0) {
err = ESP_ERR_TIMEOUT;
goto cleanup_sha;
}
esp_http_client_set_timeout_ms(client, io_timeout_ms);
size_t remaining = (size_t)(expected_size - received);
int read_capacity = remaining < PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE ?
(int)remaining : PLATFORM_OTA_DOWNLOAD_BUFFER_SIZE;
int read_len = esp_http_client_read(client, (char *)buffer, read_capacity);
if (read_len == -ESP_ERR_HTTP_EAGAIN) {
continue;
}
if (read_len < 0) {
err = ESP_ERR_INVALID_RESPONSE;
goto cleanup_sha;
}
if (read_len == 0) {
err = esp_http_client_is_complete_data_received(client) ?
ESP_ERR_INVALID_SIZE : ESP_ERR_HTTP_EAGAIN;
goto cleanup_sha;
}
if (mbedtls_sha256_update(&sha_ctx, buffer, (size_t)read_len) != 0) {
err = ESP_FAIL;
goto cleanup_sha;
}
err = esp_ota_write(ota_handle, buffer, (size_t)read_len);
if (err != ESP_OK) {
goto cleanup_sha;
}
received += (uint64_t)read_len;
if (progress_cb != NULL) {
progress_cb(received, expected_size, user_data);
}
}
if (content_length < 0) {
while (!esp_http_client_is_complete_data_received(client)) {
io_timeout_ms = platform_ota_remaining_timeout_ms(deadline_us);
if (io_timeout_ms <= 0) {
err = ESP_ERR_TIMEOUT;
goto cleanup_sha;
}
esp_http_client_set_timeout_ms(client, io_timeout_ms);
int overflow_len = esp_http_client_read(client, (char *)buffer, 1);
if (overflow_len == -ESP_ERR_HTTP_EAGAIN) {
continue;
}
if (overflow_len < 0) {
err = ESP_ERR_INVALID_RESPONSE;
goto cleanup_sha;
}
if (overflow_len > 0) {
err = ESP_ERR_INVALID_SIZE;
goto cleanup_sha;
}
break;
}
}
uint8_t actual_sha256[32];
if (mbedtls_sha256_finish(&sha_ctx, actual_sha256) != 0) {
err = ESP_FAIL;
goto cleanup_sha;
}
mbedtls_sha256_free(&sha_ctx);
if (memcmp(actual_sha256, expected_sha256, sizeof(actual_sha256)) != 0) {
ESP_LOGE(TAG, "OTA SHA-256 mismatch");
err = ESP_ERR_INVALID_CRC;
goto cleanup;
}
err = esp_ota_end(ota_handle);
ota_handle = 0;
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA image validation failed: %s", esp_err_to_name(err));
goto cleanup_no_abort;
}
err = esp_ota_set_boot_partition(partition);
if (err == ESP_OK) {
ESP_LOGI(TAG,
"OTA image installed in %s (%" PRIu64 " bytes)",
partition->label,
received);
}
goto cleanup_no_abort;
cleanup_sha:
mbedtls_sha256_free(&sha_ctx);
cleanup:
if (ota_handle != 0) {
esp_ota_abort(ota_handle);
}
cleanup_no_abort:
free(buffer);
esp_http_client_close(client);
esp_http_client_cleanup(client);
return err;
}
esp_err_t platform_ota_confirm_running_image(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
if (running == NULL) {
return ESP_ERR_NOT_FOUND;
}
esp_ota_img_states_t state = ESP_OTA_IMG_UNDEFINED;
esp_err_t err = esp_ota_get_state_partition(running, &state);
if (err == ESP_ERR_NOT_FOUND || state != ESP_OTA_IMG_PENDING_VERIFY) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
return esp_ota_mark_app_valid_cancel_rollback();
}
esp_err_t platform_ota_get_memory_headroom(uint32_t *free_internal,
uint32_t *largest_internal,
uint32_t *largest_dma)
{
if (free_internal == NULL || largest_internal == NULL || largest_dma == NULL) {
return ESP_ERR_INVALID_ARG;
}
*free_internal = (uint32_t)heap_caps_get_free_size(
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
*largest_internal = (uint32_t)heap_caps_get_largest_free_block(
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
*largest_dma = (uint32_t)heap_caps_get_largest_free_block(MALLOC_CAP_DMA);
return ESP_OK;
}
void platform_restart(void)
{
esp_restart();
}