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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user