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:
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1.0.0
|
||||||
Reference in New Issue
Block a user