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
+1
View File
@@ -16,6 +16,7 @@ idf_component_register(
"src/runtime/control_plane_key_input.c" "src/runtime/control_plane_key_input.c"
"src/runtime/control_plane_remote_log_service.c" "src/runtime/control_plane_remote_log_service.c"
"src/runtime/control_plane_runtime_workers.c" "src/runtime/control_plane_runtime_workers.c"
"src/runtime/control_plane_ota.c"
"src/runtime/control_plane_factory_cli.c" "src/runtime/control_plane_factory_cli.c"
INCLUDE_DIRS INCLUDE_DIRS
"include" "include"
@@ -60,6 +60,32 @@ typedef struct {
const char *parameters_json; const char *parameters_json;
} control_plane_multimodal_update_info_request_t; } control_plane_multimodal_update_info_request_t;
typedef enum {
CONTROL_PLANE_OTA_STATE_DISABLED = 0,
CONTROL_PLANE_OTA_STATE_IDLE,
CONTROL_PLANE_OTA_STATE_WAITING_FOR_NETWORK,
CONTROL_PLANE_OTA_STATE_WAITING_FOR_RESOURCES,
CONTROL_PLANE_OTA_STATE_CHECKING,
CONTROL_PLANE_OTA_STATE_WAITING_FOR_CONFIRMATION,
CONTROL_PLANE_OTA_STATE_DOWNLOADING,
CONTROL_PLANE_OTA_STATE_READY_TO_REBOOT,
CONTROL_PLANE_OTA_STATE_ERROR,
CONTROL_PLANE_OTA_STATE_STOPPED,
} control_plane_ota_state_t;
typedef struct {
control_plane_ota_state_t state;
bool running;
bool forced;
bool confirmed;
char target_version[32];
uint64_t bytes_received;
uint64_t total_bytes;
uint32_t consecutive_failures;
int last_http_status;
esp_err_t last_error;
} control_plane_ota_snapshot_t;
esp_err_t control_plane_init(void); esp_err_t control_plane_init(void);
/** /**
@@ -196,6 +222,10 @@ esp_err_t control_plane_multimodal_update_info(
esp_err_t control_plane_start_runtime_workers(void); esp_err_t control_plane_start_runtime_workers(void);
void control_plane_stop_runtime_workers(void); void control_plane_stop_runtime_workers(void);
esp_err_t control_plane_start_ota_service(void);
esp_err_t control_plane_stop_ota_service(int timeout_ms);
esp_err_t control_plane_get_ota_snapshot(control_plane_ota_snapshot_t *out);
/** /**
* @brief Start the staged event observer runtime only. * @brief Start the staged event observer runtime only.
* *
@@ -143,6 +143,8 @@ void control_plane_local_wake_resume_after_foreground(void);
bool control_plane_local_wake_prompt_gate_active(void); bool control_plane_local_wake_prompt_gate_active(void);
void control_plane_ptt_reset_capture_window(void); void control_plane_ptt_reset_capture_window(void);
bool control_plane_voice_turn_is_idle(void); bool control_plane_voice_turn_is_idle(void);
bool control_plane_ota_voice_services_allowed(void);
bool control_plane_runtime_is_stopping(void);
esp_err_t control_plane_start_system_event_task(void); esp_err_t control_plane_start_system_event_task(void);
void control_plane_stop_system_event_task(void); void control_plane_stop_system_event_task(void);
@@ -390,6 +390,9 @@ esp_err_t control_plane_local_wake_start(void)
#if !CONFIG_TQ_LOCAL_WAKE_ENABLE #if !CONFIG_TQ_LOCAL_WAKE_ENABLE
return ESP_OK; return ESP_OK;
#else #else
if (!control_plane_ota_voice_services_allowed()) {
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_local_wake_lock); portENTER_CRITICAL(&s_local_wake_lock);
if (s_local_wake.task != NULL || s_local_wake.started) { if (s_local_wake.task != NULL || s_local_wake.started) {
portEXIT_CRITICAL(&s_local_wake_lock); portEXIT_CRITICAL(&s_local_wake_lock);
@@ -0,0 +1,821 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "control_plane_internal.h"
#include "multimodal/control_plane_multimodal_device_auth.h"
#include "domain.h"
#include "esp_attr.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "soc/soc_caps.h"
#ifndef CONFIG_TQ_OTA_ENABLE
#define CONFIG_TQ_OTA_ENABLE 0
#endif
#ifndef CONFIG_TQ_OTA_REPORT_URL
#define CONFIG_TQ_OTA_REPORT_URL "https://gz.api.talkingq.com/ota/api/ota/versionInfoReport"
#endif
#ifndef CONFIG_TQ_OTA_INTERNAL_AUTH_SECRET
#define CONFIG_TQ_OTA_INTERNAL_AUTH_SECRET ""
#endif
#ifndef CONFIG_TQ_OTA_POLL_INTERVAL_MS
#define CONFIG_TQ_OTA_POLL_INTERVAL_MS 21600000
#endif
#ifndef CONFIG_TQ_OTA_CONFIRMATION_POLL_MS
#define CONFIG_TQ_OTA_CONFIRMATION_POLL_MS 21600000
#endif
#ifndef CONFIG_TQ_OTA_HTTP_TIMEOUT_MS
#define CONFIG_TQ_OTA_HTTP_TIMEOUT_MS 15000
#endif
#ifndef CONFIG_TQ_OTA_INSTALL_TIMEOUT_MS
#define CONFIG_TQ_OTA_INSTALL_TIMEOUT_MS 600000
#endif
#ifndef CONFIG_TQ_OTA_RETRY_BASE_MS
#define CONFIG_TQ_OTA_RETRY_BASE_MS 21600000
#endif
#ifndef CONFIG_TQ_OTA_RETRY_MAX_MS
#define CONFIG_TQ_OTA_RETRY_MAX_MS 21600000
#endif
#ifndef CONFIG_TQ_OTA_TASK_STACK_BYTES
#define CONFIG_TQ_OTA_TASK_STACK_BYTES 16384
#endif
#ifndef CONFIG_TQ_OTA_MIN_FREE_INTERNAL_BYTES
#define CONFIG_TQ_OTA_MIN_FREE_INTERNAL_BYTES 32768
#endif
#ifndef CONFIG_TQ_OTA_MIN_LARGEST_DMA_BYTES
#define CONFIG_TQ_OTA_MIN_LARGEST_DMA_BYTES 4096
#endif
#ifndef CONFIG_TQ_NETWORK_TASK_CORE
#define CONFIG_TQ_NETWORK_TASK_CORE 0
#endif
#ifndef CONFIG_TQ_LOCAL_WAKE_ENABLE
#define CONFIG_TQ_LOCAL_WAKE_ENABLE 0
#endif
#define CONTROL_PLANE_OTA_STOP_POLL_MS 20
#define CONTROL_PLANE_OTA_SLEEP_SLICE_MS 200
#define CONTROL_PLANE_OTA_START_GATE_MS 5000
#define CONTROL_PLANE_OTA_INITIAL_GATE_DELAY_MS 500
#define CONTROL_PLANE_OTA_TRANSPORT_RELEASE_WAIT_MS 100
#define CONTROL_PLANE_OTA_TASK_STACK_WARN_BYTES 4096U
#define CONTROL_PLANE_OTA_TASK_STACK_WORDS \
((CONFIG_TQ_OTA_TASK_STACK_BYTES + sizeof(StackType_t) - 1U) / sizeof(StackType_t))
typedef struct {
bool acquired;
bool restore_multimodal;
bool restore_local_wake;
} control_plane_ota_voice_window_t;
typedef struct {
char device_id[96];
char serial_number[96];
char version[DOMAIN_OTA_VERSION_MAX_LEN];
char jwt[CONTROL_PLANE_DEVICE_AUTH_JWT_MAX_LEN];
domain_ota_report_request_t request;
} control_plane_ota_report_context_t;
static const char *TAG = "cp_ota";
static portMUX_TYPE s_ota_lock = portMUX_INITIALIZER_UNLOCKED;
static TaskHandle_t s_ota_task;
static bool s_ota_stop_requested;
static bool s_ota_voice_services_reserved;
static DRAM_ATTR StaticTask_t s_ota_task_buffer;
static DRAM_ATTR StackType_t s_ota_task_stack[CONTROL_PLANE_OTA_TASK_STACK_WORDS];
static control_plane_ota_snapshot_t s_ota_snapshot = {
.state = CONTROL_PLANE_OTA_STATE_DISABLED,
.last_http_status = -1,
};
static void control_plane_ota_worker(void *arg);
static bool control_plane_ota_start_default_local_wake(void)
{
#if CONFIG_TQ_LOCAL_WAKE_ENABLE
return control_plane_local_wake_start() == ESP_OK;
#endif
return false;
}
static bool control_plane_ota_stop_is_requested(void)
{
bool requested;
portENTER_CRITICAL(&s_ota_lock);
requested = s_ota_stop_requested;
portEXIT_CRITICAL(&s_ota_lock);
return requested;
}
bool control_plane_ota_voice_services_allowed(void)
{
bool allowed;
portENTER_CRITICAL(&s_ota_lock);
allowed = !s_ota_voice_services_reserved;
portEXIT_CRITICAL(&s_ota_lock);
return allowed;
}
static void control_plane_ota_set_state(control_plane_ota_state_t state,
esp_err_t error,
int http_status)
{
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.state = state;
s_ota_snapshot.last_error = error;
s_ota_snapshot.last_http_status = http_status;
portEXIT_CRITICAL(&s_ota_lock);
}
static void control_plane_ota_set_candidate(const domain_ota_firmware_t *firmware)
{
portENTER_CRITICAL(&s_ota_lock);
strlcpy(s_ota_snapshot.target_version,
firmware->version,
sizeof(s_ota_snapshot.target_version));
s_ota_snapshot.forced = firmware->forced;
s_ota_snapshot.confirmed = firmware->confirmed;
s_ota_snapshot.bytes_received = 0U;
s_ota_snapshot.total_bytes = firmware->size_bytes;
portEXIT_CRITICAL(&s_ota_lock);
}
static void control_plane_ota_clear_candidate(void)
{
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.target_version[0] = '\0';
s_ota_snapshot.forced = false;
s_ota_snapshot.confirmed = false;
s_ota_snapshot.bytes_received = 0U;
s_ota_snapshot.total_bytes = 0U;
portEXIT_CRITICAL(&s_ota_lock);
}
static bool control_plane_ota_wait_interruptible(uint32_t wait_ms)
{
uint32_t waited_ms = 0U;
while (waited_ms < wait_ms && !control_plane_ota_stop_is_requested()) {
uint32_t slice_ms = wait_ms - waited_ms;
if (slice_ms > CONTROL_PLANE_OTA_SLEEP_SLICE_MS) {
slice_ms = CONTROL_PLANE_OTA_SLEEP_SLICE_MS;
}
TickType_t ticks = pdMS_TO_TICKS(slice_ms);
vTaskDelay(ticks > 0 ? ticks : 1);
waited_ms += slice_ms;
}
return !control_plane_ota_stop_is_requested();
}
static uint32_t control_plane_ota_retry_delay(uint32_t failures)
{
uint64_t delay_ms = CONFIG_TQ_OTA_RETRY_BASE_MS;
for (uint32_t i = 1U; i < failures && delay_ms < CONFIG_TQ_OTA_RETRY_MAX_MS; ++i) {
delay_ms *= 2U;
}
return delay_ms > CONFIG_TQ_OTA_RETRY_MAX_MS ?
CONFIG_TQ_OTA_RETRY_MAX_MS : (uint32_t)delay_ms;
}
static void control_plane_ota_note_failure(esp_err_t error, int http_status)
{
portENTER_CRITICAL(&s_ota_lock);
if (s_ota_snapshot.consecutive_failures < UINT32_MAX) {
s_ota_snapshot.consecutive_failures++;
}
s_ota_snapshot.state = CONTROL_PLANE_OTA_STATE_ERROR;
s_ota_snapshot.last_error = error;
s_ota_snapshot.last_http_status = http_status;
portEXIT_CRITICAL(&s_ota_lock);
}
static void control_plane_ota_clear_failures(void)
{
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.consecutive_failures = 0U;
s_ota_snapshot.last_error = ESP_OK;
portEXIT_CRITICAL(&s_ota_lock);
}
static uint32_t control_plane_ota_failure_count(void)
{
uint32_t failures;
portENTER_CRITICAL(&s_ota_lock);
failures = s_ota_snapshot.consecutive_failures;
portEXIT_CRITICAL(&s_ota_lock);
return failures;
}
static void control_plane_ota_progress(uint64_t received,
uint64_t total,
void *user_data)
{
(void)user_data;
uint32_t old_percent;
uint32_t new_percent = total > 0U ? (uint32_t)((received * 100U) / total) : 0U;
portENTER_CRITICAL(&s_ota_lock);
old_percent = s_ota_snapshot.total_bytes > 0U ?
(uint32_t)((s_ota_snapshot.bytes_received * 100U) /
s_ota_snapshot.total_bytes) : 0U;
s_ota_snapshot.bytes_received = received;
s_ota_snapshot.total_bytes = total;
portEXIT_CRITICAL(&s_ota_lock);
if (new_percent == 100U || new_percent / 10U != old_percent / 10U) {
ESP_LOGI(TAG, "OTA download progress: %u%%", (unsigned)new_percent);
}
}
static void control_plane_ota_log_stack_headroom(const char *phase)
{
UBaseType_t high_water_bytes = uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t);
ESP_LOGI(TAG,
"OTA task stack phase=%s headroom=%uB configured=%uB",
phase != NULL ? phase : "unknown",
(unsigned)high_water_bytes,
(unsigned)CONFIG_TQ_OTA_TASK_STACK_BYTES);
if (high_water_bytes < CONTROL_PLANE_OTA_TASK_STACK_WARN_BYTES) {
ESP_LOGW(TAG,
"OTA task stack margin low: headroom=%uB required=%uB configured=%uB",
(unsigned)high_water_bytes,
(unsigned)CONTROL_PLANE_OTA_TASK_STACK_WARN_BYTES,
(unsigned)CONFIG_TQ_OTA_TASK_STACK_BYTES);
}
}
static bool control_plane_ota_cancel(void *user_data)
{
(void)user_data;
return control_plane_ota_stop_is_requested();
}
static bool control_plane_ota_runtime_is_idle(void)
{
if (!control_plane_voice_turn_is_idle()) {
return false;
}
control_plane_multimodal_runtime_state_t state = {0};
if (control_plane_multimodal_get_runtime_state(&state) != ESP_OK) {
return true;
}
return !state.has_active_task && !state.client_speech_active &&
!state.remote_responding &&
!state.local_responding_started;
}
static void control_plane_ota_set_voice_services_reserved(bool reserved)
{
portENTER_CRITICAL(&s_ota_lock);
s_ota_voice_services_reserved = reserved;
portEXIT_CRITICAL(&s_ota_lock);
}
static BaseType_t control_plane_ota_create_task(TaskHandle_t *out_task)
{
if (out_task == NULL) {
return pdFAIL;
}
*out_task = NULL;
const BaseType_t core_id = CONFIG_TQ_NETWORK_TASK_CORE < 0 ?
tskNO_AFFINITY : CONFIG_TQ_NETWORK_TASK_CORE;
const uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
ESP_LOGI(TAG,
"OTA task create: static_stack=%u internal_free=%u internal_largest=%u",
(unsigned)CONFIG_TQ_OTA_TASK_STACK_BYTES,
(unsigned)heap_caps_get_free_size(internal_caps),
(unsigned)heap_caps_get_largest_free_block(internal_caps));
TaskHandle_t task = xTaskCreateStaticPinnedToCore(
control_plane_ota_worker,
"cp_ota",
CONFIG_TQ_OTA_TASK_STACK_BYTES,
NULL,
4,
s_ota_task_stack,
&s_ota_task_buffer,
core_id);
if (task == NULL) {
return pdFAIL;
}
*out_task = task;
return pdPASS;
}
static esp_err_t control_plane_ota_acquire_voice_window(
bool initial_window,
bool allow_active_teardown,
control_plane_ota_voice_window_t *window)
{
if (window == NULL) {
return ESP_ERR_INVALID_ARG;
}
memset(window, 0, sizeof(*window));
if (!initial_window && !allow_active_teardown && !control_plane_ota_runtime_is_idle()) {
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_ota_lock);
if (!initial_window && s_ota_voice_services_reserved) {
portEXIT_CRITICAL(&s_ota_lock);
return ESP_ERR_INVALID_STATE;
}
s_ota_voice_services_reserved = true;
portEXIT_CRITICAL(&s_ota_lock);
if ((!allow_active_teardown && !control_plane_ota_runtime_is_idle())) {
control_plane_ota_set_voice_services_reserved(false);
return ESP_ERR_INVALID_STATE;
}
control_plane_multimodal_runtime_state_t multimodal_state = {0};
bool have_multimodal_state =
control_plane_multimodal_get_runtime_state(&multimodal_state) == ESP_OK;
window->restore_multimodal = initial_window ||
(have_multimodal_state &&
(multimodal_state.connected || multimodal_state.starting));
/* Every OTA HTTPS window stops the local wake worker, so restore it after
* both the initial check and later idle-time checks. */
window->restore_local_wake = true;
window->acquired = true;
{
esp_err_t wake_stop_err = control_plane_local_wake_stop(1000);
if (wake_stop_err != ESP_OK) {
ESP_LOGW(TAG,
"OTA voice window deferred until local wake teardown completes: %s",
esp_err_to_name(wake_stop_err));
control_plane_ota_set_voice_services_reserved(false);
window->acquired = false;
return wake_stop_err;
}
}
if (have_multimodal_state &&
(multimodal_state.connected || multimodal_state.starting ||
multimodal_state.stopping || multimodal_state.has_active_task)) {
esp_err_t stop_err = control_plane_stop_multimodal_client();
if (stop_err != ESP_OK) {
ESP_LOGW(TAG,
"OTA voice window deferred until multimodal teardown completes: %s",
esp_err_to_name(stop_err));
control_plane_ota_set_voice_services_reserved(false);
window->acquired = false;
return stop_err;
}
}
if (!control_plane_ota_wait_interruptible(
CONTROL_PLANE_OTA_TRANSPORT_RELEASE_WAIT_MS)) {
return ESP_ERR_INVALID_STATE;
}
domain_ota_memory_headroom_t headroom = {0};
esp_err_t err = domain_runtime_ota_get_memory_headroom(&headroom);
if (err != ESP_OK) {
return err;
}
ESP_LOGI(TAG,
"OTA HTTPS window memory: free_internal=%u largest_internal=%u largest_dma=%u",
(unsigned)headroom.free_internal,
(unsigned)headroom.largest_internal,
(unsigned)headroom.largest_dma);
if (headroom.free_internal < CONFIG_TQ_OTA_MIN_FREE_INTERNAL_BYTES ||
headroom.largest_dma < CONFIG_TQ_OTA_MIN_LARGEST_DMA_BYTES) {
ESP_LOGW(TAG,
"OTA HTTPS window deferred: need free_internal>=%u largest_dma>=%u",
(unsigned)CONFIG_TQ_OTA_MIN_FREE_INTERNAL_BYTES,
(unsigned)CONFIG_TQ_OTA_MIN_LARGEST_DMA_BYTES);
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
static void control_plane_ota_release_voice_window(control_plane_ota_voice_window_t *window)
{
if (window == NULL || !window->acquired) {
return;
}
control_plane_ota_set_voice_services_reserved(false);
window->acquired = false;
ESP_LOGI(TAG,
"OTA HTTPS window released: restore_multimodal=%d restore_local_wake=%d",
window->restore_multimodal ? 1 : 0,
window->restore_local_wake ? 1 : 0);
if (control_plane_runtime_is_stopping() || !control_plane_is_sta_connected()) {
return;
}
if (window->restore_local_wake && control_plane_voice_turn_is_idle() &&
control_plane_ota_start_default_local_wake()) {
ESP_LOGI(TAG, "Local wake restore requested after OTA HTTPS window");
}
if (window->restore_multimodal || !control_plane_voice_turn_is_idle()) {
(void)control_plane_schedule_websocket_wait_task();
}
}
static void control_plane_ota_restore_default_voice_services(void)
{
if (control_plane_runtime_is_stopping() || !control_plane_is_sta_connected()) {
return;
}
if (control_plane_voice_turn_is_idle() &&
control_plane_ota_start_default_local_wake()) {
ESP_LOGI(TAG, "Local wake restore requested as the default voice listener");
}
(void)control_plane_schedule_websocket_wait_task();
}
static esp_err_t control_plane_ota_prepare_report(domain_ota_report_request_t *request,
char *device_id,
size_t device_id_len,
char *serial_number,
size_t serial_number_len,
char *version,
size_t version_len,
char *jwt,
size_t jwt_len,
bool force_jwt_refresh)
{
domain_runtime_read_device_identity(device_id,
device_id_len,
serial_number,
serial_number_len);
if (device_id[0] == '\0') {
return ESP_ERR_NOT_FOUND;
}
esp_err_t err = domain_runtime_read_firmware_version(version, version_len);
if (err != ESP_OK || version[0] == '\0') {
return err != ESP_OK ? err : ESP_ERR_INVALID_STATE;
}
control_plane_device_auth_result_t auth_result =
control_plane_multimodal_device_auth_ensure(
CONTROL_PLANE_DEVICE_AUTH_SCOPE_OTA_REPORT,
force_jwt_refresh);
if (auth_result != CONTROL_PLANE_DEVICE_AUTH_OK) {
return auth_result == CONTROL_PLANE_DEVICE_AUTH_FATAL ?
ESP_FAIL : ESP_ERR_INVALID_STATE;
}
err = control_plane_multimodal_device_auth_copy_jwt(jwt, jwt_len);
if (err != ESP_OK) {
return err;
}
*request = (domain_ota_report_request_t) {
.report_url = CONFIG_TQ_OTA_REPORT_URL,
.device_id = device_id,
.serial_number = serial_number,
.current_version = version,
.device_jwt = jwt,
.internal_auth_secret = CONFIG_TQ_OTA_INTERNAL_AUTH_SECRET,
.timeout_ms = CONFIG_TQ_OTA_HTTP_TIMEOUT_MS,
};
return ESP_OK;
}
static esp_err_t control_plane_ota_check_once(domain_ota_report_response_t *response,
bool force_initial_jwt_refresh)
{
if (response == NULL) {
return ESP_ERR_INVALID_ARG;
}
control_plane_ota_report_context_t *ctx = NULL;
#if SOC_SPIRAM_SUPPORTED
ctx = (control_plane_ota_report_context_t *)heap_caps_calloc(
1,
sizeof(*ctx),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
#endif
if (ctx == NULL) {
ctx = (control_plane_ota_report_context_t *)calloc(1, sizeof(*ctx));
}
if (ctx == NULL) {
return ESP_ERR_NO_MEM;
}
esp_err_t err = ESP_FAIL;
for (int attempt = 0; attempt < 2; ++attempt) {
bool force_jwt_refresh = force_initial_jwt_refresh || attempt > 0;
err = control_plane_ota_prepare_report(&ctx->request,
ctx->device_id,
sizeof(ctx->device_id),
ctx->serial_number,
sizeof(ctx->serial_number),
ctx->version,
sizeof(ctx->version),
ctx->jwt,
sizeof(ctx->jwt),
force_jwt_refresh);
if (err != ESP_OK) {
break;
}
err = domain_runtime_ota_report(&ctx->request, response);
bool unauthorized = err == ESP_OK &&
(response->http_status == 401 || response->http_status == 403);
if (!unauthorized) {
break;
}
control_plane_multimodal_device_auth_invalidate(response->http_status);
if (attempt == 0) {
ESP_LOGW(TAG,
"OTA report authorization rejected (http=%d); refreshing JWT and retrying once",
response->http_status);
}
}
memset(ctx, 0, sizeof(*ctx));
heap_caps_free(ctx);
return err;
}
static void control_plane_ota_worker(void *arg)
{
(void)arg;
if (ulTaskNotifyTake(pdTRUE,
pdMS_TO_TICKS(CONTROL_PLANE_OTA_START_GATE_MS)) == 0U) {
ESP_LOGE(TAG, "OTA worker start gate timed out");
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.running = false;
s_ota_snapshot.state = CONTROL_PLANE_OTA_STATE_ERROR;
s_ota_snapshot.last_error = ESP_ERR_TIMEOUT;
s_ota_voice_services_reserved = false;
s_ota_task = NULL;
portEXIT_CRITICAL(&s_ota_lock);
control_plane_ota_restore_default_voice_services();
vTaskDelete(NULL);
return;
}
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_IDLE, ESP_OK, -1);
bool initial_check_pending = true;
if (!control_plane_ota_wait_interruptible(CONTROL_PLANE_OTA_INITIAL_GATE_DELAY_MS)) {
goto stopped;
}
while (!control_plane_ota_stop_is_requested()) {
if (!control_plane_is_sta_connected()) {
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_WAITING_FOR_NETWORK,
ESP_OK,
-1);
control_plane_ota_wait_interruptible(1000U);
continue;
}
control_plane_ota_voice_window_t voice_window = {0};
esp_err_t err = control_plane_ota_acquire_voice_window(initial_check_pending,
false,
&voice_window);
if (err == ESP_ERR_INVALID_STATE) {
control_plane_ota_release_voice_window(&voice_window);
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_WAITING_FOR_RESOURCES,
ESP_OK,
-1);
control_plane_ota_wait_interruptible(1000U);
continue;
}
if (err != ESP_OK) {
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
control_plane_ota_note_failure(err, -1);
control_plane_ota_wait_interruptible(
control_plane_ota_retry_delay(control_plane_ota_failure_count()));
continue;
}
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_CHECKING, ESP_OK, -1);
domain_ota_report_response_t *response = NULL;
#if SOC_SPIRAM_SUPPORTED
response = (domain_ota_report_response_t *)heap_caps_calloc(
1,
sizeof(*response),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
#endif
if (response == NULL) {
response = (domain_ota_report_response_t *)calloc(1, sizeof(*response));
}
if (response == NULL) {
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
control_plane_ota_note_failure(ESP_ERR_NO_MEM, -1);
control_plane_ota_wait_interruptible(
control_plane_ota_retry_delay(control_plane_ota_failure_count()));
continue;
}
err = control_plane_ota_check_once(response, initial_check_pending);
control_plane_ota_log_stack_headroom("version_report");
if (err != ESP_OK || response->outcome == DOMAIN_OTA_REPORT_SERVER_ERROR) {
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
esp_err_t report_err = err != ESP_OK ? err : ESP_ERR_INVALID_RESPONSE;
control_plane_ota_note_failure(report_err, response->http_status);
ESP_LOGW(TAG,
"OTA version report failed: err=%s http=%d business=%d",
esp_err_to_name(report_err),
response->http_status,
response->business_code);
heap_caps_free(response);
control_plane_ota_wait_interruptible(
control_plane_ota_retry_delay(control_plane_ota_failure_count()));
continue;
}
control_plane_ota_clear_failures();
if (response->outcome == DOMAIN_OTA_REPORT_NO_UPDATE) {
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
control_plane_ota_clear_candidate();
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_IDLE,
ESP_OK,
response->http_status);
ESP_LOGI(TAG,
"OTA version report complete: running firmware is current (http=%d)",
response->http_status);
heap_caps_free(response);
control_plane_ota_wait_interruptible(CONFIG_TQ_OTA_POLL_INTERVAL_MS);
continue;
}
control_plane_ota_set_candidate(&response->firmware);
if (!response->firmware.forced && !response->firmware.confirmed) {
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
ESP_LOGI(TAG,
"OTA %s available; waiting for mini-program confirmation",
response->firmware.version);
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_WAITING_FOR_CONFIRMATION,
ESP_OK,
response->http_status);
heap_caps_free(response);
control_plane_ota_wait_interruptible(CONFIG_TQ_OTA_CONFIRMATION_POLL_MS);
continue;
}
if (!control_plane_ota_runtime_is_idle()) {
ESP_LOGI(TAG, "OTA approved; waiting for active interaction to finish");
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
heap_caps_free(response);
control_plane_ota_wait_interruptible(2000U);
continue;
}
ESP_LOGI(TAG,
"Starting %s OTA update to %s",
response->firmware.forced ? "forced" : "confirmed",
response->firmware.version);
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_DOWNLOADING,
ESP_OK,
response->http_status);
domain_runtime_behavior_set_keep_awake(true);
err = domain_runtime_ota_install(&response->firmware,
CONFIG_TQ_OTA_INSTALL_TIMEOUT_MS,
control_plane_ota_progress,
control_plane_ota_cancel,
NULL);
control_plane_ota_log_stack_headroom("firmware_install");
domain_runtime_behavior_set_keep_awake(false);
if (err != ESP_OK) {
initial_check_pending = false;
control_plane_ota_release_voice_window(&voice_window);
control_plane_ota_note_failure(err, response->http_status);
ESP_LOGE(TAG, "OTA install failed: %s", esp_err_to_name(err));
heap_caps_free(response);
if (!control_plane_ota_stop_is_requested()) {
control_plane_ota_wait_interruptible(
control_plane_ota_retry_delay(control_plane_ota_failure_count()));
}
continue;
}
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_READY_TO_REBOOT,
ESP_OK,
response->http_status);
ESP_LOGI(TAG,
"OTA image ready; restarting into version %s",
response->firmware.version);
heap_caps_free(response);
vTaskDelay(pdMS_TO_TICKS(1000));
domain_runtime_restart();
}
stopped:
control_plane_ota_set_voice_services_reserved(false);
control_plane_ota_restore_default_voice_services();
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.running = false;
s_ota_snapshot.state = CONTROL_PLANE_OTA_STATE_STOPPED;
s_ota_task = NULL;
portEXIT_CRITICAL(&s_ota_lock);
vTaskDelete(NULL);
}
esp_err_t control_plane_start_ota_service(void)
{
#if !CONFIG_TQ_OTA_ENABLE
control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_DISABLED, ESP_OK, -1);
return ESP_OK;
#else
esp_err_t confirm_err = domain_runtime_ota_confirm_running_image();
if (confirm_err != ESP_OK) {
ESP_LOGE(TAG, "Unable to confirm running OTA image: %s", esp_err_to_name(confirm_err));
return confirm_err;
}
portENTER_CRITICAL(&s_ota_lock);
if (s_ota_task != NULL) {
portEXIT_CRITICAL(&s_ota_lock);
return ESP_OK;
}
s_ota_stop_requested = false;
/* The initial version report owns the startup network window. Voice is
* released after the first OTA result (including a bounded failure). */
s_ota_voice_services_reserved = true;
s_ota_snapshot.running = true;
s_ota_snapshot.state = CONTROL_PLANE_OTA_STATE_IDLE;
s_ota_snapshot.last_error = ESP_OK;
portEXIT_CRITICAL(&s_ota_lock);
TaskHandle_t task = NULL;
BaseType_t created = control_plane_ota_create_task(&task);
if (created != pdPASS) {
portENTER_CRITICAL(&s_ota_lock);
s_ota_snapshot.running = false;
s_ota_snapshot.state = CONTROL_PLANE_OTA_STATE_ERROR;
s_ota_snapshot.last_error = ESP_ERR_NO_MEM;
s_ota_voice_services_reserved = false;
portEXIT_CRITICAL(&s_ota_lock);
return ESP_ERR_NO_MEM;
}
portENTER_CRITICAL(&s_ota_lock);
s_ota_task = task;
portEXIT_CRITICAL(&s_ota_lock);
xTaskNotifyGive(task);
ESP_LOGI(TAG, "OTA service started");
return ESP_OK;
#endif
}
esp_err_t control_plane_stop_ota_service(int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
portENTER_CRITICAL(&s_ota_lock);
s_ota_stop_requested = true;
TaskHandle_t task = s_ota_task;
portEXIT_CRITICAL(&s_ota_lock);
if (task == NULL || timeout_ms == 0) {
return ESP_OK;
}
TickType_t start = xTaskGetTickCount();
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
while (true) {
portENTER_CRITICAL(&s_ota_lock);
task = s_ota_task;
portEXIT_CRITICAL(&s_ota_lock);
if (task == NULL) {
return ESP_OK;
}
if ((xTaskGetTickCount() - start) >= timeout_ticks) {
break;
}
vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_OTA_STOP_POLL_MS));
}
ESP_LOGW(TAG, "OTA service stop timeout; forcing worker teardown");
vTaskDelete(task);
portENTER_CRITICAL(&s_ota_lock);
if (s_ota_task == task) {
s_ota_task = NULL;
}
s_ota_snapshot.running = false;
s_ota_snapshot.state = CONTROL_PLANE_OTA_STATE_STOPPED;
s_ota_voice_services_reserved = false;
portEXIT_CRITICAL(&s_ota_lock);
control_plane_ota_restore_default_voice_services();
return ESP_ERR_TIMEOUT;
}
esp_err_t control_plane_get_ota_snapshot(control_plane_ota_snapshot_t *out)
{
if (out == NULL) {
return ESP_ERR_INVALID_ARG;
}
portENTER_CRITICAL(&s_ota_lock);
*out = s_ota_snapshot;
portEXIT_CRITICAL(&s_ota_lock);
return ESP_OK;
}
@@ -438,7 +438,7 @@ static void control_plane_memory_monitor_sample(TickType_t now_tick, bool force)
} }
#endif #endif
static bool control_plane_runtime_stop_requested(void) bool control_plane_runtime_is_stopping(void)
{ {
bool stop_requested = false; bool stop_requested = false;
portENTER_CRITICAL(&s_runtime_lock); portENTER_CRITICAL(&s_runtime_lock);
@@ -447,6 +447,11 @@ static bool control_plane_runtime_stop_requested(void)
return stop_requested; return stop_requested;
} }
static bool control_plane_runtime_stop_requested(void)
{
return control_plane_runtime_is_stopping();
}
static void control_plane_set_runtime_stop_requested(bool stop_requested) static void control_plane_set_runtime_stop_requested(bool stop_requested)
{ {
portENTER_CRITICAL(&s_runtime_lock); portENTER_CRITICAL(&s_runtime_lock);
@@ -1111,6 +1116,9 @@ static void control_plane_model_command_task(void *arg)
esp_err_t control_plane_schedule_websocket_wait_task(void) esp_err_t control_plane_schedule_websocket_wait_task(void)
{ {
if (!control_plane_ota_voice_services_allowed()) {
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_runtime_lock); portENTER_CRITICAL(&s_runtime_lock);
if (!s_runtime_stop_requested) { if (!s_runtime_stop_requested) {
s_ws_start_requested = true; s_ws_start_requested = true;
@@ -1394,6 +1402,9 @@ static esp_err_t control_plane_ensure_user_config_server(void)
static bool control_plane_try_start_multimodal_from_state_machine(TickType_t *last_attempt_tick, static bool control_plane_try_start_multimodal_from_state_machine(TickType_t *last_attempt_tick,
bool foreground) bool foreground)
{ {
if (!control_plane_ota_voice_services_allowed()) {
return false;
}
control_plane_multimodal_runtime_state_t state = {0}; control_plane_multimodal_runtime_state_t state = {0};
if (control_plane_multimodal_get_runtime_state(&state) == ESP_OK) { if (control_plane_multimodal_get_runtime_state(&state) == ESP_OK) {
if (state.connected) { if (state.connected) {
@@ -2266,6 +2277,11 @@ esp_err_t control_plane_start_runtime_workers(void)
} }
} }
esp_err_t ota_err = control_plane_start_ota_service();
if (ota_err != ESP_OK) {
ESP_LOGW(TAG, "OTA service start failed: %s", esp_err_to_name(ota_err));
}
(void)control_plane_schedule_websocket_wait_task(); (void)control_plane_schedule_websocket_wait_task();
#if CONFIG_TQ_SCREEN_ENABLE #if CONFIG_TQ_SCREEN_ENABLE
@@ -2318,13 +2334,17 @@ esp_err_t control_plane_start_runtime_workers(void)
return key_input_err; return key_input_err;
} }
esp_err_t local_wake_err = control_plane_local_wake_start(); if (control_plane_ota_voice_services_allowed()) {
if (local_wake_err != ESP_OK) { esp_err_t local_wake_err = control_plane_local_wake_start();
ESP_LOGE(TAG, if (local_wake_err != ESP_OK) {
"Start local wake worker failed: %s", ESP_LOGE(TAG,
esp_err_to_name(local_wake_err)); "Start local wake worker failed: %s",
control_plane_stop_runtime_workers(); esp_err_to_name(local_wake_err));
return local_wake_err; control_plane_stop_runtime_workers();
return local_wake_err;
}
} else {
ESP_LOGI(TAG, "Local wake startup deferred for OTA HTTPS window");
} }
esp_err_t remote_log_err = control_plane_remote_log_start(); esp_err_t remote_log_err = control_plane_remote_log_start();
@@ -2392,6 +2412,7 @@ void control_plane_stop_event_observer_runtime(void)
void control_plane_stop_runtime_workers(void) void control_plane_stop_runtime_workers(void)
{ {
control_plane_set_runtime_stop_requested(true); control_plane_set_runtime_stop_requested(true);
(void)control_plane_stop_ota_service(CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
(void)control_plane_stop_mini_program_provisioning( (void)control_plane_stop_mini_program_provisioning(
CONTROL_PLANE_TASK_STOP_WAIT_MS); CONTROL_PLANE_TASK_STOP_WAIT_MS);
control_plane_stop_user_config_server(); control_plane_stop_user_config_server();
+2
View File
@@ -8,6 +8,7 @@ idf_component_register(
"src/fortune/domain_fortune_store.c" "src/fortune/domain_fortune_store.c"
"src/fortune/domain_user_config_server.c" "src/fortune/domain_user_config_server.c"
"src/runtime/domain_runtime_adapters.c" "src/runtime/domain_runtime_adapters.c"
"src/runtime/domain_ota.c"
"src/runtime/domain_local_command.c" "src/runtime/domain_local_command.c"
"src/runtime/domain_mini_program_provisioning.c" "src/runtime/domain_mini_program_provisioning.c"
INCLUDE_DIRS INCLUDE_DIRS
@@ -19,6 +20,7 @@ idf_component_register(
esp-sr esp-sr
log log
json json
mbedtls
EMBED_TXTFILES EMBED_TXTFILES
"assets/user_config.html" "assets/user_config.html"
) )
+59
View File
@@ -248,6 +248,7 @@ void domain_runtime_read_device_identity(char *device_id,
size_t device_id_len, size_t device_id_len,
char *serial_number, char *serial_number,
size_t serial_number_len); size_t serial_number_len);
esp_err_t domain_runtime_read_firmware_version(char *out, size_t out_len);
esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number); esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number);
@@ -404,6 +405,64 @@ esp_err_t domain_runtime_post_json(const char *url,
size_t *out_response_body_len, size_t *out_response_body_len,
int *out_status); int *out_status);
#define DOMAIN_OTA_VERSION_MAX_LEN 32
#define DOMAIN_OTA_URL_MAX_LEN 2048
#define DOMAIN_OTA_SHA256_HEX_LEN 64
typedef enum {
DOMAIN_OTA_REPORT_NO_UPDATE = 0,
DOMAIN_OTA_REPORT_UPDATE_AVAILABLE,
DOMAIN_OTA_REPORT_SERVER_ERROR,
} domain_ota_report_outcome_t;
typedef struct {
const char *report_url;
const char *device_id;
const char *serial_number;
const char *current_version;
const char *device_jwt;
const char *internal_auth_secret;
int timeout_ms;
} domain_ota_report_request_t;
typedef struct {
char version[DOMAIN_OTA_VERSION_MAX_LEN];
char url[DOMAIN_OTA_URL_MAX_LEN];
char sha256[DOMAIN_OTA_SHA256_HEX_LEN + 1];
uint64_t size_bytes;
bool forced;
bool confirmed;
} domain_ota_firmware_t;
typedef struct {
domain_ota_report_outcome_t outcome;
int http_status;
int business_code;
domain_ota_firmware_t firmware;
} domain_ota_report_response_t;
typedef struct {
uint32_t free_internal;
uint32_t largest_internal;
uint32_t largest_dma;
} domain_ota_memory_headroom_t;
typedef void (*domain_ota_progress_callback_t)(uint64_t bytes_received,
uint64_t total_bytes,
void *user_data);
typedef bool (*domain_ota_cancel_callback_t)(void *user_data);
esp_err_t domain_runtime_ota_report(const domain_ota_report_request_t *request,
domain_ota_report_response_t *response);
esp_err_t domain_runtime_ota_install(const domain_ota_firmware_t *firmware,
int timeout_ms,
domain_ota_progress_callback_t progress_cb,
domain_ota_cancel_callback_t cancel_cb,
void *user_data);
esp_err_t domain_runtime_ota_confirm_running_image(void);
esp_err_t domain_runtime_ota_get_memory_headroom(domain_ota_memory_headroom_t *out);
void domain_runtime_restart(void);
/** /**
* @brief Console (UART) line-transport primitives, bridged to the platform. * @brief Console (UART) line-transport primitives, bridged to the platform.
* *
+471
View File
@@ -0,0 +1,471 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "cJSON.h"
#include "esp_log.h"
#include "esp_random.h"
#include "mbedtls/base64.h"
#include "mbedtls/md.h"
#include "mbedtls/sha256.h"
#include "platform.h"
#define DOMAIN_OTA_RESPONSE_CAPACITY 4096
#define DOMAIN_OTA_NONCE_LEN 36
#define DOMAIN_OTA_AUTH_VALUE_MAX_LEN 128
#define DOMAIN_OTA_MIN_VALID_EPOCH 1700000000LL
static const char *TAG = "domain_ota";
typedef struct {
domain_ota_progress_callback_t progress_cb;
domain_ota_cancel_callback_t cancel_cb;
void *user_data;
} domain_ota_install_bridge_t;
static esp_err_t domain_ota_copy_json_string(cJSON *object,
const char *name,
char *out,
size_t out_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(object, name);
if (!cJSON_IsString(item) || item->valuestring == NULL ||
strlcpy(out, item->valuestring, out_len) >= out_len) {
return ESP_ERR_INVALID_RESPONSE;
}
return ESP_OK;
}
static esp_err_t domain_ota_base64(const uint8_t *input,
size_t input_len,
char *out,
size_t out_len)
{
size_t encoded_len = 0U;
int ret = mbedtls_base64_encode((uint8_t *)out,
out_len,
&encoded_len,
input,
input_len);
if (ret != 0 || encoded_len >= out_len) {
return ESP_FAIL;
}
out[encoded_len] = '\0';
return ESP_OK;
}
static esp_err_t domain_ota_parse_url_target(const char *url,
char *path,
size_t path_len,
char *query,
size_t query_len)
{
const char *authority = strstr(url, "://");
if (authority == NULL) {
return ESP_ERR_INVALID_ARG;
}
const char *target = strchr(authority + 3, '/');
if (target == NULL) {
target = "/";
}
const char *query_begin = strchr(target, '?');
size_t target_len = query_begin == NULL ? strlen(target) :
(size_t)(query_begin - target);
if (target_len == 0U || target_len >= path_len) {
return ESP_ERR_INVALID_SIZE;
}
memcpy(path, target, target_len);
path[target_len] = '\0';
query[0] = '\0';
if (query_begin != NULL && strlcpy(query, query_begin + 1, query_len) >= query_len) {
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
static void domain_ota_generate_nonce(char out[DOMAIN_OTA_NONCE_LEN + 1])
{
uint8_t random_bytes[16];
esp_fill_random(random_bytes, sizeof(random_bytes));
snprintf(out,
DOMAIN_OTA_NONCE_LEN + 1,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
static esp_err_t domain_ota_build_internal_auth(const char *url,
const char *payload,
const char *secret,
char *timestamp,
size_t timestamp_len,
char *nonce,
size_t nonce_len,
char *signature,
size_t signature_len)
{
time_t now = time(NULL);
if ((int64_t)now < DOMAIN_OTA_MIN_VALID_EPOCH) {
return ESP_ERR_INVALID_STATE;
}
if (snprintf(timestamp, timestamp_len, "%lld", (long long)now) >= (int)timestamp_len) {
return ESP_ERR_INVALID_SIZE;
}
if (nonce_len < DOMAIN_OTA_NONCE_LEN + 1U) {
return ESP_ERR_INVALID_SIZE;
}
domain_ota_generate_nonce(nonce);
uint8_t body_digest[32];
if (mbedtls_sha256((const uint8_t *)payload,
strlen(payload),
body_digest,
0) != 0) {
return ESP_FAIL;
}
char body_hash[DOMAIN_OTA_AUTH_VALUE_MAX_LEN];
esp_err_t err = domain_ota_base64(body_digest,
sizeof(body_digest),
body_hash,
sizeof(body_hash));
if (err != ESP_OK) {
return err;
}
char path[256];
char query[256];
err = domain_ota_parse_url_target(url, path, sizeof(path), query, sizeof(query));
if (err != ESP_OK) {
return err;
}
int payload_len = snprintf(NULL,
0,
"POST\n%s\n%s\n%s\n%s\n%s",
path,
query,
body_hash,
timestamp,
nonce);
if (payload_len <= 0) {
return ESP_FAIL;
}
char *signature_payload = (char *)malloc((size_t)payload_len + 1U);
if (signature_payload == NULL) {
return ESP_ERR_NO_MEM;
}
snprintf(signature_payload,
(size_t)payload_len + 1U,
"POST\n%s\n%s\n%s\n%s\n%s",
path,
query,
body_hash,
timestamp,
nonce);
uint8_t digest[32];
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
int hmac_ret = md_info == NULL ? -1 :
mbedtls_md_hmac(md_info,
(const uint8_t *)secret,
strlen(secret),
(const uint8_t *)signature_payload,
(size_t)payload_len,
digest);
free(signature_payload);
if (hmac_ret != 0) {
return ESP_FAIL;
}
return domain_ota_base64(digest, sizeof(digest), signature, signature_len);
}
static esp_err_t domain_ota_build_report_payload(const domain_ota_report_request_t *request,
char **out_payload)
{
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return ESP_ERR_NO_MEM;
}
bool ok = cJSON_AddStringToObject(root, "deviceId", request->device_id) != NULL &&
cJSON_AddStringToObject(root, "version", request->current_version) != NULL;
if (ok && request->serial_number != NULL && request->serial_number[0] != '\0') {
ok = cJSON_AddStringToObject(root, "serialNumber", request->serial_number) != NULL;
}
char *payload = ok ? cJSON_PrintUnformatted(root) : NULL;
cJSON_Delete(root);
if (payload == NULL) {
return ESP_ERR_NO_MEM;
}
*out_payload = payload;
return ESP_OK;
}
static esp_err_t domain_ota_parse_report_response(const char *body,
domain_ota_report_response_t *response)
{
cJSON *root = cJSON_Parse(body);
if (root == NULL) {
return ESP_ERR_INVALID_RESPONSE;
}
cJSON *code = cJSON_GetObjectItemCaseSensitive(root, "code");
response->business_code = cJSON_IsNumber(code) ? code->valueint : 0;
cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
cJSON *not_uptodate = cJSON_IsObject(data) ?
cJSON_GetObjectItemCaseSensitive(data, "notUptodate") : NULL;
if (response->business_code != 200 || !cJSON_IsNumber(not_uptodate) ||
not_uptodate->valueint != 1) {
response->outcome = DOMAIN_OTA_REPORT_SERVER_ERROR;
cJSON_Delete(root);
return ESP_OK;
}
domain_ota_firmware_t *firmware = &response->firmware;
esp_err_t err = domain_ota_copy_json_string(data,
"version",
firmware->version,
sizeof(firmware->version));
if (err == ESP_OK) {
err = domain_ota_copy_json_string(data,
"url",
firmware->url,
sizeof(firmware->url));
}
if (err == ESP_OK) {
err = domain_ota_copy_json_string(data,
"sha256",
firmware->sha256,
sizeof(firmware->sha256));
}
cJSON *size_bytes = cJSON_GetObjectItemCaseSensitive(data, "sizeBytes");
cJSON *forced = cJSON_GetObjectItemCaseSensitive(data, "force");
cJSON *confirmed = cJSON_GetObjectItemCaseSensitive(data, "update");
if (err != ESP_OK || !cJSON_IsNumber(size_bytes) || size_bytes->valuedouble <= 0 ||
size_bytes->valuedouble > (double)UINT32_MAX ||
(double)(uint64_t)size_bytes->valuedouble != size_bytes->valuedouble ||
strlen(firmware->sha256) != 64U) {
cJSON_Delete(root);
return ESP_ERR_INVALID_RESPONSE;
}
firmware->size_bytes = (uint64_t)size_bytes->valuedouble;
firmware->forced = cJSON_IsNumber(forced) && forced->valueint == 1;
firmware->confirmed = cJSON_IsNumber(confirmed) && confirmed->valueint == 1;
response->outcome = DOMAIN_OTA_REPORT_UPDATE_AVAILABLE;
cJSON_Delete(root);
return ESP_OK;
}
esp_err_t domain_runtime_ota_report(const domain_ota_report_request_t *request,
domain_ota_report_response_t *response)
{
if (response != NULL) {
memset(response, 0, sizeof(*response));
response->http_status = -1;
}
if (request == NULL || response == NULL || request->report_url == NULL ||
strncmp(request->report_url, "https://", 8) != 0 ||
request->device_id == NULL || request->device_id[0] == '\0' ||
request->current_version == NULL || request->current_version[0] == '\0' ||
request->device_jwt == NULL || request->device_jwt[0] == '\0' ||
request->timeout_ms <= 0) {
return ESP_ERR_INVALID_ARG;
}
char *payload = NULL;
esp_err_t err = domain_ota_build_report_payload(request, &payload);
if (err != ESP_OK) {
return err;
}
char bearer[1600];
if (snprintf(bearer, sizeof(bearer), "Bearer %s", request->device_jwt) >=
(int)sizeof(bearer)) {
cJSON_free(payload);
return ESP_ERR_INVALID_SIZE;
}
platform_https_header_t headers[4] = {
{.name = "Authorization", .value = bearer},
};
size_t header_count = 1U;
char timestamp[32] = {0};
char nonce[DOMAIN_OTA_NONCE_LEN + 1] = {0};
char signature[DOMAIN_OTA_AUTH_VALUE_MAX_LEN] = {0};
if (request->internal_auth_secret != NULL && request->internal_auth_secret[0] != '\0') {
err = domain_ota_build_internal_auth(request->report_url,
payload,
request->internal_auth_secret,
timestamp,
sizeof(timestamp),
nonce,
sizeof(nonce),
signature,
sizeof(signature));
if (err != ESP_OK) {
cJSON_free(payload);
return err;
}
headers[header_count++] = (platform_https_header_t) {
.name = "X-Internal-Timestamp", .value = timestamp,
};
headers[header_count++] = (platform_https_header_t) {
.name = "X-Internal-Nonce", .value = nonce,
};
headers[header_count++] = (platform_https_header_t) {
.name = "X-Internal-Signature", .value = signature,
};
}
char *body = (char *)calloc(1, DOMAIN_OTA_RESPONSE_CAPACITY);
if (body == NULL) {
cJSON_free(payload);
return ESP_ERR_NO_MEM;
}
platform_https_post_json_request_t platform_request = {
.url = request->report_url,
.payload = payload,
.headers = headers,
.header_count = header_count,
.timeout_ms = request->timeout_ms,
.tx_buffer_size = 2048,
.rx_buffer_size = 2048,
};
platform_https_response_t platform_response = {
.body = body,
.body_capacity = DOMAIN_OTA_RESPONSE_CAPACITY,
};
err = platform_https_post_json(&platform_request, &platform_response);
cJSON_free(payload);
if (err != ESP_OK) {
free(body);
return err;
}
response->http_status = platform_response.status_code;
ESP_LOGI(TAG,
"OTA version report response: http=%d body_len=%u body=%.*s",
platform_response.status_code,
(unsigned)platform_response.body_len,
(int)platform_response.body_len,
body);
if (platform_response.status_code == 204) {
response->outcome = DOMAIN_OTA_REPORT_NO_UPDATE;
free(body);
return ESP_OK;
}
if (platform_response.status_code != 200) {
response->outcome = DOMAIN_OTA_REPORT_SERVER_ERROR;
free(body);
return ESP_OK;
}
err = domain_ota_parse_report_response(body, response);
free(body);
return err;
}
static int domain_ota_hex_value(char value)
{
if (value >= '0' && value <= '9') {
return value - '0';
}
if (value >= 'a' && value <= 'f') {
return value - 'a' + 10;
}
if (value >= 'A' && value <= 'F') {
return value - 'A' + 10;
}
return -1;
}
static esp_err_t domain_ota_parse_sha256(const char *hex, uint8_t out[32])
{
if (hex == NULL || strlen(hex) != 64U) {
return ESP_ERR_INVALID_ARG;
}
for (size_t i = 0; i < 32U; ++i) {
int high = domain_ota_hex_value(hex[i * 2U]);
int low = domain_ota_hex_value(hex[i * 2U + 1U]);
if (high < 0 || low < 0) {
return ESP_ERR_INVALID_ARG;
}
out[i] = (uint8_t)((high << 4) | low);
}
return ESP_OK;
}
static void domain_ota_progress_bridge(uint64_t received, uint64_t total, void *user_data)
{
domain_ota_install_bridge_t *bridge = (domain_ota_install_bridge_t *)user_data;
if (bridge->progress_cb != NULL) {
bridge->progress_cb(received, total, bridge->user_data);
}
}
static bool domain_ota_cancel_bridge(void *user_data)
{
domain_ota_install_bridge_t *bridge = (domain_ota_install_bridge_t *)user_data;
return bridge->cancel_cb != NULL && bridge->cancel_cb(bridge->user_data);
}
esp_err_t domain_runtime_ota_install(const domain_ota_firmware_t *firmware,
int timeout_ms,
domain_ota_progress_callback_t progress_cb,
domain_ota_cancel_callback_t cancel_cb,
void *user_data)
{
if (firmware == NULL || strncmp(firmware->url, "https://", 8) != 0 ||
firmware->size_bytes == 0U || timeout_ms <= 0) {
return ESP_ERR_INVALID_ARG;
}
uint8_t expected_sha256[32];
esp_err_t err = domain_ota_parse_sha256(firmware->sha256, expected_sha256);
if (err != ESP_OK) {
return err;
}
domain_ota_install_bridge_t bridge = {
.progress_cb = progress_cb,
.cancel_cb = cancel_cb,
.user_data = user_data,
};
return platform_ota_install_image(firmware->url,
expected_sha256,
firmware->size_bytes,
timeout_ms,
domain_ota_progress_bridge,
domain_ota_cancel_bridge,
&bridge);
}
esp_err_t domain_runtime_ota_confirm_running_image(void)
{
return platform_ota_confirm_running_image();
}
esp_err_t domain_runtime_ota_get_memory_headroom(domain_ota_memory_headroom_t *out)
{
if (out == NULL) {
return ESP_ERR_INVALID_ARG;
}
return platform_ota_get_memory_headroom(&out->free_internal,
&out->largest_internal,
&out->largest_dma);
}
void domain_runtime_restart(void)
{
platform_restart();
}
@@ -859,6 +859,11 @@ void domain_runtime_read_device_identity(char *device_id,
platform_read_device_identity(device_id, device_id_len, serial_number, serial_number_len); platform_read_device_identity(device_id, device_id_len, serial_number, serial_number_len);
} }
esp_err_t domain_runtime_read_firmware_version(char *out, size_t out_len)
{
return platform_read_firmware_version(out, out_len);
}
esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number) esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number)
{ {
return platform_write_device_identity(device_id, serial_number); return platform_write_device_identity(device_id, serial_number);
+3
View File
@@ -3,6 +3,7 @@ set(platform_srcs
"src/runtime/platform_storage.c" "src/runtime/platform_storage.c"
"src/runtime/platform_time.c" "src/runtime/platform_time.c"
"src/runtime/platform_https_client.c" "src/runtime/platform_https_client.c"
"src/runtime/platform_ota.c"
"src/runtime/platform_lan_http_server.c" "src/runtime/platform_lan_http_server.c"
"src/runtime/platform_ble_gatt_server.c" "src/runtime/platform_ble_gatt_server.c"
"src/platform_console_driver.c" "src/platform_console_driver.c"
@@ -37,11 +38,13 @@ set(platform_priv_requires
esp_driver_spi esp_driver_spi
esp_event esp_event
esp_app_format esp_app_format
app_update
esp_http_client esp_http_client
esp_http_server esp_http_server
esp_lcd esp_lcd
esp_netif esp_netif
esp_psram esp_psram
heap
esp_system esp_system
esp_timer esp_timer
esp_wifi esp_wifi
+18
View File
@@ -137,6 +137,11 @@ typedef struct {
int status_code; int status_code;
} platform_https_response_t; } 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_UUID128_LEN 16
#define PLATFORM_BLE_GATT_NAME_MAX_LEN 31 #define PLATFORM_BLE_GATT_NAME_MAX_LEN 31
#define PLATFORM_BLE_GATT_VALUE_MAX_LEN 512 #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, esp_err_t platform_https_post_json(const platform_https_post_json_request_t *request,
platform_https_response_t *response); 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); bool platform_https_is_connect_error(esp_err_t err);
/** Start a generic single-service BLE GATT peripheral. */ /** 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();
}
+31
View File
@@ -0,0 +1,31 @@
# OTA Firmware Integration
The control plane owns a managed OTA worker. After Wi-Fi is available, it
obtains a device JWT with the `ota:report` scope and posts the running device
identity and firmware version to `CONFIG_TQ_OTA_REPORT_URL`.
The service returns `204` when the running image is current. A `200` response
with `data.notUptodate=1` includes the target version, HTTPS download URL,
exact byte size, SHA-256, and the `force`/`update` decision. Non-forced images
wait for the mini-program confirmation (`update=1`); forced images proceed
without that confirmation.
Before an HTTPS exchange, the worker quiesces local wake and multimodal voice
services, waits for transport teardown, and checks internal/DMA memory
headroom. The platform layer streams the image into the inactive OTA
partition, verifies the exact byte count and raw-file SHA-256, validates the
ESP-IDF image, selects the new boot partition, and restarts. Any failure
aborts the OTA handle and leaves the current partition active.
The first normal runtime startup of a pending image confirms it with the
ESP-IDF rollback API. A failed or unhealthy boot therefore rolls back to the
previous image.
The running version is embedded in the application descriptor at build time.
Update the repository-root `version.txt` before publishing a release; the
device reads it at runtime through `esp_app_get_description()->version`.
The device report route should normally be exempted from service-to-service
HMAC authentication while retaining device JWT authentication. The optional
`CONFIG_TQ_OTA_INTERNAL_AUTH_SECRET` exists only for deployments that require
the documented `X-Internal-*` headers.
+74
View File
@@ -43,6 +43,80 @@ menu "Example Configuration"
firmware uses a longer timeout to tolerate brief beacon loss on firmware uses a longer timeout to tolerate brief beacon loss on
crowded 2.4 GHz networks. crowded 2.4 GHz networks.
menu "OTA Firmware Update"
config TQ_OTA_ENABLE
bool "Enable managed OTA firmware updates"
default y
depends on AUTH_V2_ENABLE
select BOOTLOADER_APP_ROLLBACK_ENABLE
config TQ_OTA_REPORT_URL
string "OTA version report URL"
default "https://gz.api.talkingq.com/ota/api/ota/versionInfoReport"
depends on TQ_OTA_ENABLE
config TQ_OTA_INTERNAL_AUTH_SECRET
string "OTA internal-auth compatibility secret"
default ""
depends on TQ_OTA_ENABLE
config TQ_OTA_POLL_INTERVAL_MS
int "Normal OTA poll interval (ms)"
default 21600000
range 10000 86400000
depends on TQ_OTA_ENABLE
config TQ_OTA_CONFIRMATION_POLL_MS
int "OTA confirmation recheck interval (ms)"
default 21600000
range 2000 86400000
depends on TQ_OTA_ENABLE
config TQ_OTA_HTTP_TIMEOUT_MS
int "OTA report HTTP timeout (ms)"
default 15000
range 1000 60000
depends on TQ_OTA_ENABLE
config TQ_OTA_INSTALL_TIMEOUT_MS
int "OTA download/install deadline (ms)"
default 600000
range 30000 1800000
depends on TQ_OTA_ENABLE
config TQ_OTA_RETRY_BASE_MS
int "OTA retry backoff base (ms)"
default 21600000
range 1000 86400000
depends on TQ_OTA_ENABLE
config TQ_OTA_RETRY_MAX_MS
int "OTA retry backoff cap (ms)"
default 21600000
range 10000 86400000
depends on TQ_OTA_ENABLE
config TQ_OTA_TASK_STACK_BYTES
int "OTA worker stack (bytes)"
default 16384
range 8192 32768
depends on TQ_OTA_ENABLE
config TQ_OTA_MIN_FREE_INTERNAL_BYTES
int "OTA HTTPS minimum free internal memory (bytes)"
default 32768
range 16384 131072
depends on TQ_OTA_ENABLE
config TQ_OTA_MIN_LARGEST_DMA_BYTES
int "OTA HTTPS minimum largest DMA block (bytes)"
default 4096
range 2048 32768
depends on TQ_OTA_ENABLE
endmenu
endmenu endmenu
menu "Mini Program Wi-Fi Provisioning" menu "Mini Program Wi-Fi Provisioning"
+12
View File
@@ -1,6 +1,7 @@
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB" CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
# Mini-program BLE provisioning. # Mini-program BLE provisioning.
CONFIG_BT_ENABLED=y CONFIG_BT_ENABLED=y
@@ -85,6 +86,17 @@ CONFIG_TQ_PTT_MODE_CLICK_TO_TALK=y
# CONFIG_TQ_PTT_MODE_HOLD_TO_TALK is not set # CONFIG_TQ_PTT_MODE_HOLD_TO_TALK is not set
CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS=8000 CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS=8000
CONFIG_TQ_LOCAL_WAKE_ENABLE=y CONFIG_TQ_LOCAL_WAKE_ENABLE=y
CONFIG_TQ_OTA_ENABLE=y
CONFIG_TQ_OTA_REPORT_URL="https://gz.api.talkingq.com/ota/api/ota/versionInfoReport"
CONFIG_TQ_OTA_POLL_INTERVAL_MS=21600000
CONFIG_TQ_OTA_CONFIRMATION_POLL_MS=21600000
CONFIG_TQ_OTA_HTTP_TIMEOUT_MS=15000
CONFIG_TQ_OTA_INSTALL_TIMEOUT_MS=600000
CONFIG_TQ_OTA_RETRY_BASE_MS=21600000
CONFIG_TQ_OTA_RETRY_MAX_MS=21600000
CONFIG_TQ_OTA_TASK_STACK_BYTES=16384
CONFIG_TQ_OTA_MIN_FREE_INTERNAL_BYTES=32768
CONFIG_TQ_OTA_MIN_LARGEST_DMA_BYTES=4096
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE="local_wake_response.opus" CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE="local_wake_response.opus"
CONFIG_TQ_LOCAL_WAKE_RETRY_MS=5000 CONFIG_TQ_LOCAL_WAKE_RETRY_MS=5000
+1
View File
@@ -0,0 +1 @@
1.0.0