chore: initial commit
This commit is contained in:
@@ -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
Reference in New Issue
Block a user