chore: initial commit

This commit is contained in:
CSBOOK\44744
2026-06-25 00:34:34 +08:00
commit 3cce1be162
295 changed files with 30971 additions and 0 deletions
@@ -0,0 +1,224 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane.h"
#include "control_plane_internal.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#ifndef CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS
#define CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS 10000
#endif
#define CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS 3
#define CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS 300
static const char *TAG = "control_plane_rt";
static int elapsed_ms_since(TickType_t start_tick)
{
TickType_t elapsed_ticks = xTaskGetTickCount() - start_tick;
return (int)(elapsed_ticks * portTICK_PERIOD_MS);
}
static int remaining_timeout_ms(TickType_t start_tick, int timeout_ms)
{
int elapsed_ms = elapsed_ms_since(start_tick);
if (elapsed_ms >= timeout_ms) {
return 0;
}
return timeout_ms - elapsed_ms;
}
esp_err_t control_plane_prepare_runtime_storage(void)
{
return domain_runtime_prepare_storage();
}
static void control_plane_wifi_set_keep_awake(bool keep_awake)
{
domain_runtime_behavior_set_keep_awake(keep_awake);
}
static esp_err_t control_plane_wifi_play_prompt(domain_wifi_prompt_id_t prompt)
{
return domain_runtime_behavior_play_prompt(prompt);
}
static esp_err_t control_plane_wifi_play_prompt_file(const char *path)
{
return domain_runtime_behavior_play_prompt_file(path);
}
static int control_plane_normalize_connect_timeout_ms(int timeout_ms, int min_timeout_ms)
{
int effective_timeout = timeout_ms;
if (effective_timeout <= 0) {
effective_timeout = 10000;
}
if (min_timeout_ms > 0 && effective_timeout < min_timeout_ms) {
effective_timeout = min_timeout_ms;
}
return effective_timeout;
}
static void control_plane_play_wifi_success_prompt(void)
{
bool played = false;
#ifdef CONFIG_VOICE_PROMPT_WIFI_SUCCESS_FILE
if (CONFIG_VOICE_PROMPT_WIFI_SUCCESS_FILE[0] != '\0') {
played = (control_plane_wifi_play_prompt_file(CONFIG_VOICE_PROMPT_WIFI_SUCCESS_FILE) == ESP_OK);
}
#endif
if (!played) {
(void)control_plane_wifi_play_prompt(DOMAIN_WIFI_PROMPT_WIFI_SUCCESS);
}
}
static esp_err_t control_plane_connect_sta_with_feedback(int timeout_ms,
bool play_connecting_prompt)
{
esp_err_t err = ESP_ERR_TIMEOUT;
TickType_t start_tick = xTaskGetTickCount();
if (play_connecting_prompt) {
(void)control_plane_wifi_play_prompt(DOMAIN_WIFI_PROMPT_WIFI_CONNECTING);
}
control_plane_wifi_set_keep_awake(true);
for (int attempt = 1; attempt <= CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS; ++attempt) {
int attempt_timeout_ms = remaining_timeout_ms(start_tick, timeout_ms);
if (attempt_timeout_ms <= 0) {
err = ESP_ERR_TIMEOUT;
break;
}
if (domain_runtime_multimodal_audio_is_initialized() &&
!domain_runtime_multimodal_audio_is_uplink_running()) {
domain_runtime_multimodal_audio_deinit();
ESP_LOGI(TAG, "Released multimodal audio before STA connect attempt %d", attempt);
}
err = domain_runtime_wifi_portal_connect_sta(attempt_timeout_ms);
if (err == ESP_OK) {
break;
}
if (attempt >= CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS) {
break;
}
if (err != ESP_ERR_TIMEOUT && err != ESP_FAIL) {
break;
}
int retry_delay_ms = CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS * attempt;
int remaining_ms = remaining_timeout_ms(start_tick, timeout_ms);
if (remaining_ms <= 0) {
err = ESP_ERR_TIMEOUT;
break;
}
if (retry_delay_ms > remaining_ms) {
retry_delay_ms = remaining_ms;
}
ESP_LOGW(TAG,
"STA connect attempt %d/%d failed (%s), retry in %d ms",
attempt,
CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS,
esp_err_to_name(err),
retry_delay_ms);
vTaskDelay(pdMS_TO_TICKS(retry_delay_ms));
}
control_plane_wifi_set_keep_awake(false);
if (err != ESP_OK) {
return err;
}
control_plane_play_wifi_success_prompt();
return ESP_OK;
}
static bool control_plane_wifi_connect_error_keeps_credentials(esp_err_t err)
{
return domain_runtime_wifi_connect_error_keeps_credentials(err);
}
static void control_plane_register_wifi_behavior_hooks(void)
{
domain_wifi_behavior_hooks_t hooks = {
.set_keep_awake = control_plane_wifi_set_keep_awake,
.play_prompt = control_plane_wifi_play_prompt,
.play_prompt_file = control_plane_wifi_play_prompt_file,
};
domain_runtime_wifi_portal_set_behavior_hooks(&hooks);
}
esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid,
const char *seed_password,
int connect_timeout_ms,
const char *config_ap_ssid_prefix)
{
esp_err_t err = control_plane_prepare_runtime_storage();
if (err != ESP_OK) {
return err;
}
control_plane_register_wifi_behavior_hooks();
err = domain_runtime_wifi_portal_init_defaults(seed_ssid, seed_password);
if (err != ESP_OK) {
return err;
}
if (domain_runtime_wifi_portal_has_saved_credentials()) {
int timeout_ms = control_plane_normalize_connect_timeout_ms(connect_timeout_ms, 0);
err = control_plane_connect_sta_with_feedback(timeout_ms, true);
if (err == ESP_OK) {
return ESP_OK;
}
if (control_plane_wifi_connect_error_keeps_credentials(err)) {
ESP_LOGW(TAG,
"STA connect failed, keep credentials for runtime retry: %s",
esp_err_to_name(err));
} else {
ESP_LOGW(TAG, "STA connect failed, clear credentials and fallback: %s",
esp_err_to_name(err));
(void)domain_runtime_wifi_portal_clear_saved_credentials();
}
}
control_plane_wifi_set_keep_awake(true);
err = domain_runtime_wifi_portal_start_config_ap(
(config_ap_ssid_prefix != NULL && config_ap_ssid_prefix[0] != '\0')
? config_ap_ssid_prefix
: "TQ-CAT");
if (err != ESP_OK) {
control_plane_wifi_set_keep_awake(false);
}
return err;
}
bool control_plane_is_sta_connected(void)
{
return domain_runtime_wifi_portal_sta_is_connected();
}
esp_err_t control_plane_init_lcd_device(void)
{
return domain_runtime_init_lcd_device();
}
void control_plane_start_lcd_render_loop(void)
{
domain_runtime_start_lcd_render_loop();
}
esp_err_t control_plane_stop_lcd_render_loop(int timeout_ms)
{
return domain_runtime_stop_lcd_render_loop(timeout_ms);
}
@@ -0,0 +1,539 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Factory provisioning serial CLI. Owns command line parsing, provisioning
* command handlers, and UART replies in the control-plane layer. Reads
* `\r\n`-delimited commands from the console UART through the domain console
* bridge and replies with `OK`, `ERROR <reason>`, or one JSON line.
*/
#include "control_plane.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "domain.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/task.h"
#include "multimodal/control_plane_multimodal_device_auth.h"
#define FACTORY_CLI_MAX_LINE_LEN 512
#define FACTORY_CLI_MAX_ARGC 8
#define FACTORY_CLI_DEVICE_ID_MAX_LEN 64
#define FACTORY_CLI_SERIAL_MAX_LEN 64
#define FACTORY_CLI_JSON_MAX_LEN 640
#define FACTORY_CLI_READ_TIMEOUT_MS 100
#define FACTORY_CLI_WORKER_STACK_BYTES (12U * 1024U)
#define FACTORY_CLI_WORKER_PRIORITY 4
#define FACTORY_CLI_STOP_WAIT_STEPS 100
#define FACTORY_CLI_STOP_WAIT_STEP_MS 20
#define FACTORY_CLI_START_WAIT_STEPS 25
#define FACTORY_CLI_START_WAIT_STEP_MS 20
#define FACTORY_CLI_START_GATE_WAIT_MS 5000
static const char *TAG = "cp_factory_cli";
static TaskHandle_t s_worker_task;
static bool s_stop_requested;
static bool s_worker_running;
static bool s_worker_starting;
static portMUX_TYPE s_worker_lock = portMUX_INITIALIZER_UNLOCKED;
/* Touched only by the single worker task; no extra lock required. */
static bool s_factory_mode_enabled;
static char s_pending_device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN];
static char s_pending_serial_number[FACTORY_CLI_SERIAL_MAX_LEN];
static TaskHandle_t factory_cli_get_worker_task(void)
{
TaskHandle_t task = NULL;
portENTER_CRITICAL(&s_worker_lock);
task = s_worker_task;
portEXIT_CRITICAL(&s_worker_lock);
return task;
}
static bool factory_cli_get_stop_requested(void)
{
bool stop_requested = false;
portENTER_CRITICAL(&s_worker_lock);
stop_requested = s_stop_requested;
portEXIT_CRITICAL(&s_worker_lock);
return stop_requested;
}
static void factory_cli_set_stop_requested(bool stop_requested)
{
portENTER_CRITICAL(&s_worker_lock);
s_stop_requested = stop_requested;
portEXIT_CRITICAL(&s_worker_lock);
}
static void factory_cli_set_worker_running(bool worker_running)
{
portENTER_CRITICAL(&s_worker_lock);
s_worker_running = worker_running;
portEXIT_CRITICAL(&s_worker_lock);
}
static bool factory_cli_claim_start(void)
{
bool claimed = false;
portENTER_CRITICAL(&s_worker_lock);
if (s_worker_task == NULL && !s_worker_starting) {
s_worker_starting = true;
s_stop_requested = false;
s_worker_running = false;
claimed = true;
}
portEXIT_CRITICAL(&s_worker_lock);
return claimed;
}
static void factory_cli_finish_start(TaskHandle_t task)
{
portENTER_CRITICAL(&s_worker_lock);
s_worker_task = task;
s_worker_starting = false;
portEXIT_CRITICAL(&s_worker_lock);
}
static bool factory_cli_get_worker_starting(void)
{
bool worker_starting = false;
portENTER_CRITICAL(&s_worker_lock);
worker_starting = s_worker_starting;
portEXIT_CRITICAL(&s_worker_lock);
return worker_starting;
}
static void factory_cli_finish_worker_exit(void)
{
portENTER_CRITICAL(&s_worker_lock);
s_worker_task = NULL;
s_stop_requested = false;
s_worker_running = false;
s_worker_starting = false;
portEXIT_CRITICAL(&s_worker_lock);
}
static bool is_text_empty(const char *value)
{
if (!value) {
return true;
}
while (*value != '\0') {
if (!isspace((unsigned char)(*value))) {
return false;
}
value++;
}
return true;
}
static void trim_text(char *value)
{
if (!value || value[0] == '\0') {
return;
}
char *start = value;
while (*start != '\0' && isspace((unsigned char)(*start))) {
start++;
}
if (start != value) {
memmove(value, start, strlen(start) + 1);
}
size_t len = strlen(value);
while (len > 0 && isspace((unsigned char)value[len - 1])) {
value[len - 1] = '\0';
len--;
}
}
static size_t split_args(char *line, char *argv[], size_t max_args)
{
size_t argc = 0;
char *cursor = line;
while (*cursor != '\0' && argc < max_args) {
while (*cursor != '\0' && isspace((unsigned char)(*cursor))) {
cursor++;
}
if (*cursor == '\0') {
break;
}
argv[argc++] = cursor;
while (*cursor != '\0' && !isspace((unsigned char)(*cursor))) {
cursor++;
}
if (*cursor == '\0') {
break;
}
*cursor = '\0';
cursor++;
}
return argc;
}
static void cli_write_line(const char *line)
{
domain_runtime_console_write_line(line);
}
static void cli_write_ok(void)
{
cli_write_line("OK");
}
static void cli_write_error(const char *message)
{
if (!message || message[0] == '\0') {
cli_write_line("ERROR");
return;
}
char line[192] = {0};
snprintf(line, sizeof(line), "ERROR %s", message);
cli_write_line(line);
}
static esp_err_t resolve_device_identity(char *device_id, size_t device_id_len)
{
char serial_number[FACTORY_CLI_SERIAL_MAX_LEN] = {0};
device_id[0] = '\0';
domain_runtime_read_device_identity(device_id, device_id_len, serial_number, sizeof(serial_number));
return device_id[0] != '\0' ? ESP_OK : ESP_ERR_INVALID_STATE;
}
static void handle_get_device_info(void)
{
char device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN] = {0};
char serial_number[FACTORY_CLI_SERIAL_MAX_LEN] = {0};
domain_runtime_read_device_identity(device_id, sizeof(device_id),
serial_number, sizeof(serial_number));
char json[FACTORY_CLI_JSON_MAX_LEN] = {0};
snprintf(json, sizeof(json),
"{\"factory_mode\":%s,\"device_id\":\"%s\",\"serial_number\":\"%s\"}",
s_factory_mode_enabled ? "true" : "false",
device_id,
serial_number);
cli_write_line(json);
cli_write_ok();
}
static void handle_save_config(void)
{
char current_id[FACTORY_CLI_DEVICE_ID_MAX_LEN] = {0};
char current_sn[FACTORY_CLI_SERIAL_MAX_LEN] = {0};
domain_runtime_read_device_identity(current_id, sizeof(current_id),
current_sn, sizeof(current_sn));
const char *id_source = s_pending_device_id[0] != '\0' ? s_pending_device_id : current_id;
const char *sn_source = s_pending_serial_number[0] != '\0' ? s_pending_serial_number : current_sn;
if (is_text_empty(id_source) || is_text_empty(sn_source)) {
cli_write_error("identity_not_ready");
return;
}
char device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN] = {0};
char serial_number[FACTORY_CLI_SERIAL_MAX_LEN] = {0};
strlcpy(device_id, id_source, sizeof(device_id));
strlcpy(serial_number, sn_source, sizeof(serial_number));
trim_text(device_id);
trim_text(serial_number);
if (device_id[0] == '\0' || serial_number[0] == '\0') {
cli_write_error("identity_not_ready");
return;
}
esp_err_t err = domain_runtime_write_device_identity(device_id, serial_number);
if (err != ESP_OK) {
cli_write_error("save_config_failed");
return;
}
s_pending_device_id[0] = '\0';
s_pending_serial_number[0] = '\0';
/* Force a fresh challenge/response against the new identity on next use. */
control_plane_multimodal_device_auth_invalidate(0);
ESP_LOGI(TAG, "Factory identity committed");
cli_write_ok();
}
static void handle_get_auth_material(void)
{
char pub_key[160] = {0};
char pub_key_jkt[96] = {0};
uint32_t key_version = 0;
esp_err_t err = control_plane_multimodal_device_auth_get_key_material(
pub_key, sizeof(pub_key), pub_key_jkt, sizeof(pub_key_jkt), &key_version);
if (err != ESP_OK) {
cli_write_error("auth_material_unavailable");
return;
}
char json[FACTORY_CLI_JSON_MAX_LEN] = {0};
snprintf(json, sizeof(json),
"{\"pub_key\":\"%s\",\"pub_key_jkt\":\"%s\",\"key_version\":%u}",
pub_key, pub_key_jkt, (unsigned int)key_version);
cli_write_line(json);
cli_write_ok();
}
static void handle_sign_challenge(const char *challenge_id_b64u, const char *challenge_b64u)
{
char device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN] = {0};
if (resolve_device_identity(device_id, sizeof(device_id)) != ESP_OK) {
cli_write_error("identity_not_ready");
return;
}
char signature[160] = {0};
esp_err_t err = control_plane_multimodal_device_auth_sign_challenge(
device_id, challenge_id_b64u, challenge_b64u, signature, sizeof(signature));
if (err != ESP_OK) {
cli_write_error(err == ESP_ERR_INVALID_STATE ? "identity_not_ready" : "sign_challenge_failed");
return;
}
char json[FACTORY_CLI_JSON_MAX_LEN] = {0};
snprintf(json, sizeof(json), "{\"signature\":\"%s\"}", signature);
cli_write_line(json);
cli_write_ok();
}
static void handle_prepare_rotate(uint32_t key_version)
{
char device_id[FACTORY_CLI_DEVICE_ID_MAX_LEN] = {0};
if (resolve_device_identity(device_id, sizeof(device_id)) != ESP_OK) {
cli_write_error("identity_not_ready");
return;
}
char new_pub_key[160] = {0};
char new_pub_key_jkt[96] = {0};
char signature[160] = {0};
esp_err_t err = control_plane_multimodal_device_auth_prepare_rotate(
device_id, key_version,
new_pub_key, sizeof(new_pub_key),
new_pub_key_jkt, sizeof(new_pub_key_jkt),
signature, sizeof(signature),
true);
if (err != ESP_OK) {
cli_write_error(err == ESP_ERR_INVALID_STATE ? "identity_not_ready" : "prepare_rotate_failed");
return;
}
char json[FACTORY_CLI_JSON_MAX_LEN] = {0};
snprintf(json, sizeof(json),
"{\"new_pub_key\":\"%s\",\"new_pub_key_jkt\":\"%s\",\"signature\":\"%s\"}",
new_pub_key, new_pub_key_jkt, signature);
cli_write_line(json);
cli_write_ok();
}
static void handle_command_line(char *line)
{
trim_text(line);
if (line[0] == '\0') {
return;
}
char *argv[FACTORY_CLI_MAX_ARGC] = {0};
size_t argc = split_args(line, argv, FACTORY_CLI_MAX_ARGC);
if (argc == 0) {
return;
}
const char *cmd = argv[0];
if (strcmp(cmd, "factory_mode") == 0) {
s_factory_mode_enabled = true;
cli_write_ok();
} else if (strcmp(cmd, "get_device_info") == 0) {
handle_get_device_info();
} else if (strcmp(cmd, "set_device_id") == 0) {
if (argc < 2 || is_text_empty(argv[1])) {
cli_write_error("missing_device_id");
return;
}
strlcpy(s_pending_device_id, argv[1], sizeof(s_pending_device_id));
trim_text(s_pending_device_id);
if (s_pending_device_id[0] == '\0') {
cli_write_error("invalid_device_id");
return;
}
cli_write_ok();
} else if (strcmp(cmd, "set_serial_number") == 0) {
if (argc < 2 || is_text_empty(argv[1])) {
cli_write_error("missing_serial_number");
return;
}
strlcpy(s_pending_serial_number, argv[1], sizeof(s_pending_serial_number));
trim_text(s_pending_serial_number);
if (s_pending_serial_number[0] == '\0') {
cli_write_error("invalid_serial_number");
return;
}
cli_write_ok();
} else if (strcmp(cmd, "save_config") == 0) {
handle_save_config();
} else if (strcmp(cmd, "get_auth_material") == 0 ||
strcmp(cmd, "auth_material") == 0 ||
strcmp(cmd, "get_device_auth_material") == 0 ||
strcmp(cmd, "get_device_auth") == 0) {
handle_get_auth_material();
} else if (strcmp(cmd, "auth_sign_challenge") == 0 ||
strcmp(cmd, "sign_auth_challenge") == 0 ||
strcmp(cmd, "sign_challenge") == 0) {
if (argc < 3 || is_text_empty(argv[1]) || is_text_empty(argv[2])) {
cli_write_error("missing_challenge");
return;
}
handle_sign_challenge(argv[1], argv[2]);
} else if (strcmp(cmd, "auth_prepare_rotate") == 0 ||
strcmp(cmd, "prepare_auth_rotate") == 0 ||
strcmp(cmd, "auth_rotate_prepare") == 0) {
if (argc < 2 || is_text_empty(argv[1])) {
cli_write_error("missing_key_version");
return;
}
uint32_t key_version = (uint32_t)strtoul(argv[1], NULL, 10);
if (key_version == 0) {
cli_write_error("invalid_key_version");
return;
}
handle_prepare_rotate(key_version);
} else {
cli_write_error("unknown_command");
}
}
static void factory_cli_worker(void *arg)
{
(void)arg;
factory_cli_set_worker_running(true);
(void)domain_runtime_prepare_storage();
if (domain_runtime_console_install() != ESP_OK) {
ESP_LOGE(TAG, "Console install failed; factory CLI inactive");
factory_cli_finish_worker_exit();
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "Factory serial CLI ready on console UART");
char line[FACTORY_CLI_MAX_LINE_LEN];
size_t line_len = 0;
while (!factory_cli_get_stop_requested()) {
uint8_t ch = 0;
int got = domain_runtime_console_read_bytes(&ch, 1, FACTORY_CLI_READ_TIMEOUT_MS);
if (got <= 0) {
continue;
}
if (ch == '\r' || ch == '\n') {
if (line_len > 0) {
line[line_len] = '\0';
handle_command_line(line);
line_len = 0;
}
continue;
}
if (ch < 0x20 || ch == 0x7F) {
continue;
}
if (line_len < sizeof(line) - 1U) {
line[line_len++] = (char)ch;
} else {
line_len = 0; /* overlong line: drop and resync */
}
}
factory_cli_finish_worker_exit();
vTaskDelete(NULL);
}
static void factory_cli_worker_entry(void *arg)
{
(void)arg;
TickType_t wait_ticks = pdMS_TO_TICKS(FACTORY_CLI_START_GATE_WAIT_MS);
if (wait_ticks == 0) {
wait_ticks = 1;
}
if (ulTaskNotifyTake(pdTRUE, wait_ticks) == 0) {
ESP_LOGE(TAG, "Factory CLI worker start gate timed out");
factory_cli_finish_worker_exit();
vTaskDelete(NULL);
return;
}
factory_cli_worker(NULL);
}
esp_err_t control_plane_factory_cli_start(void)
{
if (!factory_cli_claim_start()) {
return ESP_OK;
}
TaskHandle_t worker_task = NULL;
BaseType_t ok = xTaskCreate(factory_cli_worker_entry,
"factory_cli",
FACTORY_CLI_WORKER_STACK_BYTES,
NULL,
FACTORY_CLI_WORKER_PRIORITY,
&worker_task);
if (ok != pdPASS) {
factory_cli_finish_start(NULL);
ESP_LOGE(TAG, "Failed to start factory CLI worker");
return ESP_ERR_NO_MEM;
}
factory_cli_finish_start(worker_task);
xTaskNotifyGive(worker_task);
return ESP_OK;
}
void control_plane_factory_cli_stop(void)
{
for (int i = 0; i < FACTORY_CLI_START_WAIT_STEPS && factory_cli_get_worker_starting(); ++i) {
vTaskDelay(pdMS_TO_TICKS(FACTORY_CLI_START_WAIT_STEP_MS));
}
TaskHandle_t worker_task = factory_cli_get_worker_task();
if (worker_task == NULL) {
return;
}
factory_cli_set_stop_requested(true);
for (int i = 0; i < FACTORY_CLI_STOP_WAIT_STEPS && factory_cli_get_worker_task() != NULL; ++i) {
vTaskDelay(pdMS_TO_TICKS(FACTORY_CLI_STOP_WAIT_STEP_MS));
}
worker_task = factory_cli_get_worker_task();
if (worker_task == NULL) {
return;
}
ESP_LOGW(TAG, "Factory CLI stop timeout, force delete");
vTaskDelete(worker_task);
factory_cli_finish_worker_exit();
}
@@ -0,0 +1,293 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane_internal.h"
#include "multimodal/control_plane_multimodal_device_auth.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "cJSON.h"
#include "domain.h"
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "soc/soc_caps.h"
#ifndef CONFIG_FORTUNE_API_URL
#define CONFIG_FORTUNE_API_URL "https://gz.api.talkingq.com/token/api/device/fortune/daily-wealth"
#endif
#ifndef CONFIG_FORTUNE_TIMEZONE
#define CONFIG_FORTUNE_TIMEZONE "Asia/Shanghai"
#endif
#ifndef CONFIG_FORTUNE_CONNECT_TIMEOUT_MS
#define CONFIG_FORTUNE_CONNECT_TIMEOUT_MS 10000
#endif
#ifndef CONFIG_FORTUNE_USE_DEVICE_JWT
#define CONFIG_FORTUNE_USE_DEVICE_JWT 0
#endif
#ifndef CONFIG_MULTIMODAL_DEVICE_ID
#define CONFIG_MULTIMODAL_DEVICE_ID ""
#endif
static const char *TAG = "cp_fortune";
static const int kMaxHttpBody = 4096;
static void *fortune_psram_first_calloc(size_t count, size_t size)
{
if (count == 0U || size == 0U || count > (SIZE_MAX / size)) {
return NULL;
}
#if SOC_SPIRAM_SUPPORTED
void *ptr = heap_caps_calloc(count, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr != NULL) {
return ptr;
}
#endif
return calloc(count, size);
}
static bool copy_json_string(const cJSON *obj, const char *key, char *out, size_t out_len)
{
if (obj == NULL || key == NULL || out == NULL || out_len == 0U) {
return false;
}
const cJSON *item = cJSON_GetObjectItemCaseSensitive(obj, key);
if (!cJSON_IsString(item) || item->valuestring == NULL || item->valuestring[0] == '\0') {
return false;
}
strlcpy(out, item->valuestring, out_len);
return true;
}
static void resolve_device_id(char *device_id, size_t device_len)
{
char serial_number[64] = {0};
device_id[0] = '\0';
domain_runtime_read_device_identity(device_id, device_len, serial_number, sizeof(serial_number));
if (device_id[0] == '\0' && CONFIG_MULTIMODAL_DEVICE_ID[0] != '\0') {
strlcpy(device_id, CONFIG_MULTIMODAL_DEVICE_ID, device_len);
}
if (device_id[0] == '\0') {
strlcpy(device_id, "unknown", device_len);
}
}
static esp_err_t http_post_json(const char *url,
const char *payload,
const char *device_id,
const char *bearer_token,
char **out_body,
int *out_status)
{
if (out_status != NULL) {
*out_status = -1;
}
if (url == NULL || url[0] == '\0' || payload == NULL || out_body == NULL) {
return ESP_ERR_INVALID_ARG;
}
char *body = (char *)fortune_psram_first_calloc(1, (size_t)kMaxHttpBody + 1U);
if (body == NULL) {
return ESP_ERR_NO_MEM;
}
size_t body_len = 0U;
int status = -1;
esp_err_t err = domain_runtime_post_json(url,
payload,
device_id,
bearer_token,
CONFIG_FORTUNE_CONNECT_TIMEOUT_MS,
2048,
1024,
body,
(size_t)kMaxHttpBody + 1U,
&body_len,
&status);
if (err != ESP_OK) {
free(body);
ESP_LOGE(TAG, "HTTP POST failed: %s", esp_err_to_name(err));
return err;
}
if (body_len == 0U) {
free(body);
ESP_LOGE(TAG, "HTTP response empty (status=%d)", status);
return ESP_ERR_INVALID_RESPONSE;
}
if (status < 200 || status >= 300) {
ESP_LOGE(TAG, "Fortune request failed (status=%d): %.120s", status, body);
free(body);
return ESP_FAIL;
}
if (out_status != NULL) {
*out_status = status;
}
*out_body = body;
return ESP_OK;
}
static char *build_request_payload(const domain_birth_profile_t *profile, const char *device_id, const char *target_date)
{
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return NULL;
}
cJSON_AddStringToObject(root, "deviceId", device_id);
cJSON_AddNumberToObject(root, "birthYear", profile->year);
cJSON_AddNumberToObject(root, "birthMonth", profile->month);
cJSON_AddNumberToObject(root, "birthDay", profile->day);
cJSON_AddNumberToObject(root, "birthHour", profile->hour);
cJSON_AddNumberToObject(root, "birthMinute", profile->minute);
cJSON_AddStringToObject(root, "birthPlace", profile->birth_place);
cJSON_AddStringToObject(root, "timezone",
profile->timezone[0] != '\0' ? profile->timezone : CONFIG_FORTUNE_TIMEZONE);
if (target_date != NULL && target_date[0] != '\0') {
cJSON_AddStringToObject(root, "targetDate", target_date);
}
if (profile->current_residence[0] != '\0') {
cJSON_AddStringToObject(root, "currentResidence", profile->current_residence);
}
if (profile->mbti[0] != '\0') {
cJSON_AddStringToObject(root, "mbti", profile->mbti);
}
char *payload = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return payload;
}
static esp_err_t parse_response(const char *response, const char *target_date, domain_fortune_result_t *out)
{
cJSON *root = cJSON_Parse(response);
if (root == NULL) {
ESP_LOGE(TAG, "Fortune response parse failed");
return ESP_ERR_INVALID_RESPONSE;
}
const cJSON *code = cJSON_GetObjectItemCaseSensitive(root, "code");
if (!cJSON_IsNumber(code) || code->valueint != 200) {
const cJSON *message = cJSON_GetObjectItemCaseSensitive(root, "message");
ESP_LOGE(TAG, "Fortune service error (code=%d): %s",
cJSON_IsNumber(code) ? code->valueint : -1,
(cJSON_IsString(message) && message->valuestring) ? message->valuestring : "");
cJSON_Delete(root);
return ESP_FAIL;
}
const cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
if (!cJSON_IsObject(data)) {
cJSON_Delete(root);
return ESP_ERR_INVALID_RESPONSE;
}
memset(out, 0, sizeof(*out));
bool has_bazi = copy_json_string(data, "生辰八字", out->bazi, sizeof(out->bazi));
bool has_element = copy_json_string(data, "五行属性", out->element, sizeof(out->element));
bool has_color = copy_json_string(data, "今日喜色", out->lucky_color, sizeof(out->lucky_color));
bool has_direction = copy_json_string(data, "今日财位", out->wealth_direction, sizeof(out->wealth_direction));
if (target_date != NULL && target_date[0] != '\0') {
strlcpy(out->target_date, target_date, sizeof(out->target_date));
}
cJSON_Delete(root);
if (!has_bazi || !has_element || !has_color || !has_direction ||
out->target_date[0] == '\0') {
ESP_LOGE(TAG, "Fortune response is missing required result fields");
return ESP_ERR_INVALID_RESPONSE;
}
size_t index = 0;
if (domain_find_direction_by_name(out->wealth_direction, &index)) {
out->wealth_direction_index = index;
} else {
ESP_LOGE(TAG, "Unmapped wealth direction '%s'", out->wealth_direction);
return ESP_ERR_INVALID_RESPONSE;
}
out->valid = true;
return ESP_OK;
}
esp_err_t control_plane_fortune_fetch(const char *target_date, domain_fortune_result_t *out_result)
{
if (target_date == NULL || target_date[0] == '\0' || out_result == NULL) {
return ESP_ERR_INVALID_ARG;
}
domain_birth_profile_t profile = {0};
if (!domain_birth_profile_store_get(&profile)) {
return ESP_ERR_NOT_FOUND;
}
char device_id[64] = {0};
resolve_device_id(device_id, sizeof(device_id));
char *payload = build_request_payload(&profile, device_id, target_date);
if (payload == NULL) {
return ESP_ERR_NO_MEM;
}
char *response = NULL;
int status = 0;
const char *bearer = NULL;
#if CONFIG_FORTUNE_USE_DEVICE_JWT
char *device_jwt = (char *)fortune_psram_first_calloc(1, CONTROL_PLANE_DEVICE_AUTH_JWT_MAX_LEN);
if (device_jwt == NULL) {
free(payload);
return ESP_ERR_NO_MEM;
}
esp_err_t jwt_err = control_plane_multimodal_device_auth_copy_jwt(
device_jwt,
CONTROL_PLANE_DEVICE_AUTH_JWT_MAX_LEN);
if (jwt_err != ESP_OK || device_jwt[0] == '\0') {
ESP_LOGW(TAG, "Configured fortune JWT is unavailable: %s",
esp_err_to_name(jwt_err));
free(device_jwt);
free(payload);
return ESP_ERR_INVALID_STATE;
}
bearer = device_jwt;
#endif
ESP_LOGI(TAG, "Fetch daily fortune (date=%s, place=%s)",
target_date ? target_date : "<today>", profile.birth_place);
esp_err_t err = http_post_json(CONFIG_FORTUNE_API_URL,
payload,
device_id,
bearer,
&response,
&status);
#if CONFIG_FORTUNE_USE_DEVICE_JWT
free(device_jwt);
#endif
free(payload);
if (err != ESP_OK) {
return err;
}
err = parse_response(response, target_date, out_result);
free(response);
return err;
}
File diff suppressed because it is too large Load Diff