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
@@ -390,6 +390,9 @@ esp_err_t control_plane_local_wake_start(void)
#if !CONFIG_TQ_LOCAL_WAKE_ENABLE
return ESP_OK;
#else
if (!control_plane_ota_voice_services_allowed()) {
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_local_wake_lock);
if (s_local_wake.task != NULL || s_local_wake.started) {
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
static bool control_plane_runtime_stop_requested(void)
bool control_plane_runtime_is_stopping(void)
{
bool stop_requested = false;
portENTER_CRITICAL(&s_runtime_lock);
@@ -447,6 +447,11 @@ static bool control_plane_runtime_stop_requested(void)
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)
{
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)
{
if (!control_plane_ota_voice_services_allowed()) {
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_runtime_lock);
if (!s_runtime_stop_requested) {
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,
bool foreground)
{
if (!control_plane_ota_voice_services_allowed()) {
return false;
}
control_plane_multimodal_runtime_state_t state = {0};
if (control_plane_multimodal_get_runtime_state(&state) == ESP_OK) {
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();
#if CONFIG_TQ_SCREEN_ENABLE
@@ -2318,13 +2334,17 @@ esp_err_t control_plane_start_runtime_workers(void)
return key_input_err;
}
esp_err_t local_wake_err = control_plane_local_wake_start();
if (local_wake_err != ESP_OK) {
ESP_LOGE(TAG,
"Start local wake worker failed: %s",
esp_err_to_name(local_wake_err));
control_plane_stop_runtime_workers();
return local_wake_err;
if (control_plane_ota_voice_services_allowed()) {
esp_err_t local_wake_err = control_plane_local_wake_start();
if (local_wake_err != ESP_OK) {
ESP_LOGE(TAG,
"Start local wake worker failed: %s",
esp_err_to_name(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();
@@ -2392,6 +2412,7 @@ void control_plane_stop_event_observer_runtime(void)
void control_plane_stop_runtime_workers(void)
{
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(
CONTROL_PLANE_TASK_STOP_WAIT_MS);
control_plane_stop_user_config_server();