修复了WS连接稳定性问题

This commit is contained in:
CSBOOK\44744
2026-06-27 20:29:09 +08:00
parent b829cd8ebc
commit caa4a7eded
16 changed files with 2197 additions and 248 deletions
@@ -66,6 +66,8 @@ typedef void (*control_plane_multimodal_downstream_audio_callback_t)(const uint8
typedef struct {
bool connected;
bool starting;
bool stopping;
bool has_active_task;
bool server_listening;
bool remote_responding;
@@ -86,6 +88,7 @@ typedef enum {
CONTROL_PLANE_TRANSPORT_LOSS_SEND_BINARY_FAILURE,
CONTROL_PLANE_TRANSPORT_LOSS_LISTENING_TIMEOUT,
CONTROL_PLANE_TRANSPORT_LOSS_NO_RESPONSE_TIMEOUT,
CONTROL_PLANE_TRANSPORT_LOSS_PROTOCOL_ERROR,
} control_plane_transport_loss_reason_t;
esp_err_t control_plane_multimodal_start(void);
@@ -119,6 +119,8 @@
static const char *TAG = "cp_mm_session";
static bool s_started;
static bool s_starting;
static bool s_stopping;
static control_plane_multimodal_auth_info_t s_auth;
static char s_task_id[40];
@@ -167,6 +169,7 @@ static StaticSemaphore_t s_state_mutex_buf;
typedef enum {
LOCAL_RESPONDING_EVENT_STARTED = 0,
LOCAL_RESPONDING_EVENT_ENDED,
LOCAL_RESPONDING_EVENT_QUIT,
} local_responding_event_type_t;
typedef struct {
@@ -418,6 +421,8 @@ static const char *transport_loss_reason_name(control_plane_transport_loss_reaso
return "listening_timeout";
case CONTROL_PLANE_TRANSPORT_LOSS_NO_RESPONSE_TIMEOUT:
return "no_response_timeout";
case CONTROL_PLANE_TRANSPORT_LOSS_PROTOCOL_ERROR:
return "protocol_error";
default:
return "unknown";
}
@@ -444,6 +449,8 @@ void control_plane_multimodal_mark_transport_unavailable(
if (lock_state()) {
s_started = false;
s_starting = false;
s_stopping = false;
s_transport_available = false;
memset(&s_auth, 0, sizeof(s_auth));
reset_session_state_locked();
@@ -451,6 +458,19 @@ void control_plane_multimodal_mark_transport_unavailable(
}
control_plane_multimodal_stop_uplink();
bool close_transport = reason == CONTROL_PLANE_TRANSPORT_LOSS_PROTOCOL_ERROR ||
reason == CONTROL_PLANE_TRANSPORT_LOSS_SEND_TEXT_FAILURE ||
reason == CONTROL_PLANE_TRANSPORT_LOSS_LISTENING_TIMEOUT ||
reason == CONTROL_PLANE_TRANSPORT_LOSS_NO_RESPONSE_TIMEOUT;
if (close_transport) {
esp_err_t stop_err = domain_runtime_multimodal_transport_stop(0);
if (stop_err != ESP_OK) {
ESP_LOGW(TAG,
"Async transport stop after unavailable ended with %s",
esp_err_to_name(stop_err));
}
}
}
static void reset_session_state(void)
@@ -523,7 +543,19 @@ static void local_responding_event_worker(void *arg)
idle_ticks = 1;
}
if (xQueueReceive(s_local_responding_event_queue, &msg, idle_ticks) != pdTRUE) {
if (lock_state()) {
s_local_responding_event_task = NULL;
unlock_state();
}
vTaskDelete(NULL);
return;
}
if (msg.type == LOCAL_RESPONDING_EVENT_QUIT) {
if (lock_state()) {
s_local_responding_event_task = NULL;
unlock_state();
}
vTaskDelete(NULL);
return;
}
@@ -594,6 +626,50 @@ static esp_err_t ensure_local_responding_event_worker(void)
return ESP_OK;
}
static void stop_local_responding_event_worker(void)
{
QueueHandle_t queue = NULL;
TaskHandle_t task = NULL;
if (lock_state()) {
queue = s_local_responding_event_queue;
task = s_local_responding_event_task;
s_local_responding_started_send_pending = false;
s_local_responding_ended_send_pending = false;
unlock_state();
}
if (queue != NULL && task != NULL) {
local_responding_event_msg_t quit = {
.type = LOCAL_RESPONDING_EVENT_QUIT,
};
(void)xQueueSend(queue, &quit, 0);
TickType_t start_tick = xTaskGetTickCount();
TickType_t wait_ticks = pdMS_TO_TICKS(300);
if (wait_ticks == 0) {
wait_ticks = 1;
}
while ((xTaskGetTickCount() - start_tick) < wait_ticks) {
bool running = false;
if (lock_state()) {
running = s_local_responding_event_task != NULL;
unlock_state();
}
if (!running) {
break;
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
if (lock_state()) {
s_local_responding_event_task = NULL;
if (s_local_responding_event_queue != NULL) {
xQueueReset(s_local_responding_event_queue);
}
unlock_state();
}
}
static void queue_local_responding_event(local_responding_event_type_t type)
{
char task_id[sizeof(s_task_id)] = {0};
@@ -1198,7 +1274,12 @@ static void process_text_json(const char *json_text)
request_id,
dialog_id[0] ? dialog_id : "-");
if (cJSON_IsString(failed_task_id) &&
bool protocol_error = strcmp(error_code, "InvalidParameter") == 0 ||
strcmp(error_name, "InvalidParameter") == 0 ||
strstr(error_message, "Invalid action") != NULL;
if (!protocol_error &&
cJSON_IsString(failed_task_id) &&
failed_task_id->valuestring != NULL &&
failed_task_id->valuestring[0] != '\0') {
bool matches_active_task = false;
@@ -1218,7 +1299,13 @@ static void process_text_json(const char *json_text)
}
stop_local_downstream_playback();
if (protocol_error) {
control_plane_multimodal_mark_transport_unavailable(
CONTROL_PLANE_TRANSPORT_LOSS_PROTOCOL_ERROR,
__func__);
} else {
reset_session_state_if_task_matches(task_id);
}
notify_event(CONTROL_PLANE_MULTIMODAL_EVENT_TASK_FAILED);
} else if (strcmp(event->valuestring, "task-started") == 0) {
notify_event(CONTROL_PLANE_MULTIMODAL_EVENT_TASK_STARTED);
@@ -1378,8 +1465,10 @@ static void process_text_json(const char *json_text)
cJSON *error_message = cJSON_GetObjectItemCaseSensitive(output, "error_message");
cJSON *llm_request_id = cJSON_GetObjectItemCaseSensitive(output, "llm_request_id");
char dialog_id[sizeof(s_dialog_id)] = {0};
char task_id[sizeof(s_task_id)] = {0};
if (lock_state()) {
s_remote_responding = false;
strlcpy(task_id, s_task_id, sizeof(task_id));
strlcpy(dialog_id, s_dialog_id, sizeof(dialog_id));
unlock_state();
}
@@ -1390,6 +1479,11 @@ static void process_text_json(const char *json_text)
cJSON_IsString(error_message) && error_message->valuestring ? error_message->valuestring : "unknown",
cJSON_IsString(llm_request_id) && llm_request_id->valuestring ? llm_request_id->valuestring : "-",
dialog_id[0] ? dialog_id : "-");
control_plane_multimodal_stop_uplink();
stop_local_downstream_playback();
control_plane_multimodal_mark_transport_unavailable(
CONTROL_PLANE_TRANSPORT_LOSS_PROTOCOL_ERROR,
__func__);
notify_event(CONTROL_PLANE_MULTIMODAL_EVENT_OUTPUT_ERROR);
}
}
@@ -1409,6 +1503,9 @@ static void transport_event_bridge(const domain_multimodal_transport_event_t *ev
switch (event->type) {
case DOMAIN_MULTIMODAL_TRANSPORT_EVENT_CONNECTED:
if (lock_state()) {
s_started = true;
s_starting = false;
s_stopping = false;
s_transport_available = true;
s_last_heartbeat_tick = xTaskGetTickCount();
s_last_downstream_tick = s_last_heartbeat_tick;
@@ -1538,17 +1635,29 @@ esp_err_t control_plane_multimodal_start(void)
#endif
if (lock_state()) {
bool already_started = s_started;
unlock_state();
bool already_started = s_started || s_auth.token[0] != '\0';
bool starting = s_starting;
bool stopping = s_stopping;
if (already_started) {
unlock_state();
return ESP_OK;
}
if (starting || stopping) {
unlock_state();
return ESP_ERR_INVALID_STATE;
}
s_starting = true;
unlock_state();
}
control_plane_multimodal_auth_info_t auth = {0};
esp_err_t err = control_plane_multimodal_auth_fetch(&auth);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Auth fetch failed: %s", esp_err_to_name(err));
if (lock_state()) {
s_starting = false;
unlock_state();
}
return err;
}
@@ -1557,6 +1666,10 @@ esp_err_t control_plane_multimodal_start(void)
NULL);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Register downstream playback callback failed: %s", esp_err_to_name(err));
if (lock_state()) {
s_starting = false;
unlock_state();
}
return err;
}
@@ -1564,12 +1677,15 @@ esp_err_t control_plane_multimodal_start(void)
if (err != ESP_OK) {
ESP_LOGW(TAG, "Transport start failed: %s", esp_err_to_name(err));
(void)domain_runtime_multimodal_audio_register_downstream_playback_callback(NULL, NULL);
if (lock_state()) {
s_starting = false;
unlock_state();
}
return err;
}
if (lock_state()) {
s_auth = auth;
s_started = true;
unlock_state();
}
return ESP_OK;
@@ -1577,7 +1693,22 @@ esp_err_t control_plane_multimodal_start(void)
void control_plane_multimodal_stop(void)
{
bool already_stopping = false;
if (lock_state()) {
already_stopping = s_stopping;
if (!s_stopping) {
s_stopping = true;
}
s_starting = false;
unlock_state();
}
if (already_stopping) {
ESP_LOGI(TAG, "Multimodal stop already in progress");
return;
}
control_plane_multimodal_stop_uplink();
stop_local_responding_event_worker();
(void)control_plane_multimodal_register_downstream_audio_callback(NULL, NULL);
(void)control_plane_multimodal_register_event_callback(NULL, NULL);
(void)domain_runtime_multimodal_audio_register_downstream_playback_callback(NULL, NULL);
@@ -1589,6 +1720,9 @@ void control_plane_multimodal_stop(void)
if (lock_state()) {
s_started = false;
s_starting = false;
s_stopping = false;
s_transport_available = false;
memset(&s_auth, 0, sizeof(s_auth));
reset_session_state_locked();
unlock_state();
@@ -2123,6 +2257,8 @@ esp_err_t control_plane_multimodal_get_runtime_state(control_plane_multimodal_ru
return ESP_ERR_TIMEOUT;
}
out_state->connected = transport_available_locked();
out_state->starting = s_starting;
out_state->stopping = s_stopping;
out_state->has_active_task = s_task_id[0] != '\0';
out_state->server_listening = s_server_listening;
out_state->remote_responding = s_remote_responding;
@@ -2227,6 +2363,9 @@ esp_err_t control_plane_multimodal_start_uplink(void)
void control_plane_multimodal_stop_uplink(void)
{
if (!domain_runtime_multimodal_audio_is_uplink_running()) {
return;
}
esp_err_t err = domain_runtime_multimodal_audio_stop_uplink(CONTROL_PLANE_UPLINK_READY_WAIT_MS);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Mic uplink stop ended with %s", esp_err_to_name(err));
@@ -48,6 +48,10 @@
#define CONFIG_TQ_KEY_INPUT_POLL_MS 20
#endif
#ifndef CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS
#define CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS 120
#endif
#ifndef CONFIG_TQ_LOCAL_TASK_CORE
#define CONFIG_TQ_LOCAL_TASK_CORE 1
#endif
@@ -98,6 +102,7 @@ typedef struct {
bool long_press_sent;
bool click_pending;
int64_t debounce_deadline_ms;
int64_t inactive_since_ms;
int64_t press_start_ms;
int64_t click_deadline_ms;
} control_plane_key_state_t;
@@ -113,9 +118,9 @@ static const control_plane_key_config_t s_key_configs[] = {
.long_press_ms = CONFIG_TQ_KEY_LONG_PRESS_MS,
.double_click_ms = CONFIG_TQ_KEY_DOUBLE_CLICK_MS,
.emit_press_release = true,
.enable_short_press = true,
.enable_short_press = false,
.enable_long_press = false,
.enable_double_click = true,
.enable_double_click = false,
},
{
.key_id = CONTROL_PLANE_KEY_ID_SW2,
@@ -256,9 +261,14 @@ static void control_plane_handle_debounced_transition(const control_plane_key_co
if (state->wait_inactive_before_press) {
if (pressed) {
state->debounced_pressed = false;
state->inactive_since_ms = 0;
return;
}
state->wait_inactive_before_press = false;
state->inactive_since_ms = 0;
ESP_LOGI(TAG,
"key=%s initial active gate cleared by debounced inactive level",
control_plane_key_id_name(config->key_id));
}
if (pressed == state->debounced_pressed) {
@@ -334,6 +344,33 @@ static void control_plane_process_key_timers(void)
continue;
}
if (state->wait_inactive_before_press) {
const int level = gpio_get_level(config->gpio_num);
const bool pressed = control_plane_key_level_is_pressed(config, level);
if (!pressed) {
if (state->inactive_since_ms == 0) {
state->inactive_since_ms = now_ms;
}
uint32_t stable_ms = config->debounce_ms;
if (stable_ms < CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS) {
stable_ms = CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS;
}
if ((now_ms - state->inactive_since_ms) >= stable_ms) {
state->wait_inactive_before_press = false;
state->debounced_pressed = false;
state->debounce_pending = false;
state->press_start_ms = 0;
state->long_press_sent = false;
ESP_LOGI(TAG,
"key=%s initial active gate cleared by stable inactive poll (%" PRId64 "ms)",
control_plane_key_id_name(config->key_id),
now_ms - state->inactive_since_ms);
}
} else {
state->inactive_since_ms = 0;
}
}
if (state->debounce_pending && now_ms >= state->debounce_deadline_ms) {
state->debounce_pending = false;
const int level = gpio_get_level(config->gpio_num);
@@ -390,6 +427,7 @@ static void control_plane_key_input_task(void *arg)
"PTT initial level is active; wait for next stable press before emitting press event");
s_key_states[i].debounced_pressed = false;
s_key_states[i].wait_inactive_before_press = true;
s_key_states[i].inactive_since_ms = 0;
}
setup_err = control_plane_configure_key_gpio(config);
@@ -21,7 +21,6 @@
#include "sdkconfig.h"
#define CONTROL_PLANE_MODEL_CMD_TASK_STACK_SIZE (1024 * 4)
#define CONTROL_PLANE_DIRECTION_TASK_STACK_SIZE (1024 * 4)
#define CONTROL_PLANE_LCD_RENDER_LOOP_TASK_STACK_SIZE (1024 * 12)
#define CONTROL_PLANE_WIFI_STATE_TASK_STACK_SIZE (1024 * 12)
#define CONTROL_PLANE_TASK_MONITOR_TASK_STACK_SIZE (1024 * 6)
@@ -29,7 +28,6 @@
#define CONTROL_PLANE_MODEL_CMD_RECV_TIMEOUT_MS 200
#define CONTROL_PLANE_TASK_STOP_WAIT_MS 1500
#define CONTROL_PLANE_TASK_START_GATE_WAIT_MS 5000
#define CONTROL_PLANE_INPUT_BUF_SIZE 64
#define CONTROL_PLANE_WIFI_STATE_POLL_INTERVAL_MS 1000
#define CONTROL_PLANE_PTT_ACTIVE_POLL_INTERVAL_MS 80
#define CONTROL_PLANE_WIFI_DISCONNECTED_REFRESH_MS 5000
@@ -71,10 +69,6 @@
#define CONFIG_TQ_TASK_MONITOR_ENABLE 0
#endif
#ifndef CONFIG_TQ_DIRECTION_CONSOLE_ENABLE
#define CONFIG_TQ_DIRECTION_CONSOLE_ENABLE 0
#endif
#ifndef CONFIG_TQ_TASK_MONITOR_INTERVAL_MS
#define CONFIG_TQ_TASK_MONITOR_INTERVAL_MS 10000
#endif
@@ -157,6 +151,8 @@ typedef struct {
bool foreground_connect;
uint8_t foreground_attempts;
bool pending_press_after_teardown;
bool start_action_sent;
bool speech_action_sent;
} ptt_runtime_t;
typedef struct {
@@ -170,9 +166,6 @@ static TaskHandle_t s_model_cmd_task;
static bool s_ws_start_requested;
static bool s_ptt_active;
static ptt_runtime_t s_ptt_runtime;
#if CONFIG_TQ_DIRECTION_CONSOLE_ENABLE
static TaskHandle_t s_direction_console_task;
#endif
static TaskHandle_t s_lcd_render_loop_task;
static TaskHandle_t s_wifi_state_task;
#if CONFIG_TQ_TASK_MONITOR_ENABLE
@@ -240,10 +233,20 @@ static void control_plane_ptt_transition_locked(ptt_state_t next_state, const ch
(void)reason;
s_ptt_runtime.state = next_state;
s_ptt_runtime.state_enter_tick = xTaskGetTickCount();
if (next_state == PTT_STATE_WAIT_START_SENT) {
s_ptt_runtime.start_action_sent = false;
}
if (next_state == PTT_STATE_WAIT_SPEECH_STARTED) {
s_ptt_runtime.speech_action_sent = false;
}
if (next_state == PTT_STATE_IDLE ||
next_state == PTT_STATE_STREAMING ||
next_state == PTT_STATE_DENIED) {
s_ptt_runtime.foreground_connect = false;
if (next_state == PTT_STATE_IDLE || next_state == PTT_STATE_DENIED) {
s_ptt_runtime.start_action_sent = false;
s_ptt_runtime.speech_action_sent = false;
}
}
}
@@ -261,6 +264,20 @@ static void control_plane_ptt_log_transition(ptt_state_t old_state,
ptt_state_name(next_state),
(unsigned)generation,
reason != NULL ? reason : "-");
control_plane_multimodal_runtime_state_t mm_state = {0};
esp_err_t state_err = control_plane_multimodal_get_runtime_state(&mm_state);
ESP_LOGI(TAG,
"PTT snapshot generation=%u wifi=%d ws(conn=%d starting=%d stopping=%d) task=%d listening=%d speech=%d responding=%d uplink=%d",
(unsigned)generation,
control_plane_is_sta_connected() ? 1 : 0,
(state_err == ESP_OK && mm_state.connected) ? 1 : 0,
(state_err == ESP_OK && mm_state.starting) ? 1 : 0,
(state_err == ESP_OK && mm_state.stopping) ? 1 : 0,
(state_err == ESP_OK && mm_state.has_active_task) ? 1 : 0,
(state_err == ESP_OK && mm_state.server_listening) ? 1 : 0,
(state_err == ESP_OK && mm_state.client_speech_active) ? 1 : 0,
(state_err == ESP_OK && mm_state.remote_responding) ? 1 : 0,
control_plane_multimodal_is_uplink_running() ? 1 : 0);
}
static void control_plane_notify_wifi_state_task(void)
@@ -278,6 +295,7 @@ void control_plane_set_ptt_active(bool active)
ptt_state_t new_state = PTT_STATE_IDLE;
uint32_t generation = 0;
const char *transition_reason = active ? "press" : "release";
bool uplink_running = !active && control_plane_multimodal_is_uplink_running();
portENTER_CRITICAL(&s_runtime_lock);
old_state = s_ptt_runtime.state;
s_ptt_active = active;
@@ -288,6 +306,8 @@ void control_plane_set_ptt_active(bool active)
s_ptt_runtime.press_tick = xTaskGetTickCount();
s_ptt_runtime.foreground_connect = true;
s_ptt_runtime.foreground_attempts = 0;
s_ptt_runtime.start_action_sent = false;
s_ptt_runtime.speech_action_sent = false;
if (s_ptt_runtime.state == PTT_STATE_TEARDOWN_LOCAL ||
s_ptt_runtime.state == PTT_STATE_TEARDOWN_REMOTE) {
s_ptt_runtime.pending_press_after_teardown = true;
@@ -302,7 +322,10 @@ void control_plane_set_ptt_active(bool active)
s_ptt_runtime.foreground_attempts = 0;
s_ptt_runtime.pending_press_after_teardown = false;
s_ptt_runtime.generation++;
if (s_ptt_runtime.state == PTT_STATE_WAIT_WIFI ||
if (uplink_running || s_ptt_runtime.state == PTT_STATE_STREAMING) {
control_plane_ptt_transition_locked(PTT_STATE_TEARDOWN_LOCAL, "release_streaming");
transition_reason = "release_streaming";
} else if (s_ptt_runtime.state == PTT_STATE_WAIT_WIFI ||
s_ptt_runtime.state == PTT_STATE_WAIT_WS ||
s_ptt_runtime.state == PTT_STATE_WAIT_START_SENT ||
s_ptt_runtime.state == PTT_STATE_WAIT_LISTENING ||
@@ -310,9 +333,6 @@ void control_plane_set_ptt_active(bool active)
s_ptt_runtime.state == PTT_STATE_DENIED) {
control_plane_ptt_transition_locked(PTT_STATE_IDLE, "release_before_streaming");
transition_reason = "release_before_streaming";
} else if (s_ptt_runtime.state == PTT_STATE_STREAMING) {
control_plane_ptt_transition_locked(PTT_STATE_TEARDOWN_LOCAL, "release_streaming");
transition_reason = "release_streaming";
}
notify_worker = true;
}
@@ -631,90 +651,6 @@ static void control_plane_task_start_gate(void *arg)
control_plane_delete_task(NULL, task_with_caps);
}
#if CONFIG_TQ_DIRECTION_CONSOLE_ENABLE
static void control_plane_print_help(void)
{
printf("\n========================================\n");
printf(" Direction Control Console\n");
printf("========================================\n");
printf("Available directions:\n");
size_t count = domain_get_direction_count();
for (size_t i = 0; i < count; i++) {
const char *name_cn = NULL;
const char *name_en = NULL;
float angle = 0.0f;
if (!domain_get_direction_view(i, &name_cn, &name_en, &angle)) {
continue;
}
if (i == 0) {
printf(" %s/%s - %.1f deg [default]\n", name_cn, name_en, angle);
} else {
printf(" %s/%s - %.1f deg\n", name_cn, name_en, angle);
}
}
printf("----------------------------------------\n");
printf("Commands:\n");
printf(" help - show help\n");
printf(" pos - show current position\n");
printf(" cal - run calibration (if configured)\n");
printf("========================================\n\n");
}
static void control_plane_print_current_position(void)
{
float angle = domain_get_current_angle();
size_t index = 0;
if (domain_get_current_direction_index(&index)) {
const char *name_cn = NULL;
const char *name_en = NULL;
float direction_angle = 0.0f;
if (domain_get_direction_view(index, &name_cn, &name_en, &direction_angle)) {
printf("Current position: %s/%s (%.1f deg)\n", name_cn, name_en, angle);
return;
}
}
printf("Current position: %.1f deg (not a named direction)\n", angle);
}
static void control_plane_process_direction_command(char *input)
{
char *cmd = input;
while (*cmd == ' ') {
cmd++;
}
if (*cmd != '\0') {
char *end = cmd + strlen(cmd) - 1;
while (end > cmd && *end == ' ') {
*end-- = '\0';
}
}
if (*cmd == '\0') {
return;
}
control_plane_direction_command_t parsed = CONTROL_PLANE_DIRECTION_CMD_UNKNOWN;
esp_err_t err = control_plane_execute_direction_command(cmd, &parsed, NULL);
if (parsed == CONTROL_PLANE_DIRECTION_CMD_HELP) {
control_plane_print_help();
} else if (parsed == CONTROL_PLANE_DIRECTION_CMD_POSITION) {
control_plane_print_current_position();
} else if (parsed == CONTROL_PLANE_DIRECTION_CMD_CALIBRATE) {
if (err != ESP_OK) {
printf("Calibration failed, err=0x%x\n", err);
}
} else if (parsed == CONTROL_PLANE_DIRECTION_CMD_DIRECTION) {
if (err != ESP_OK) {
printf("Rotation failed, err=0x%x\n", err);
}
} else {
printf("Unknown command or direction: '%s'\n", cmd);
printf("Type 'help' to list available directions\n");
}
}
#endif
static void control_plane_on_model_command(control_plane_model_command_type_t command_type,
const char *value,
void *user_data)
@@ -962,74 +898,6 @@ static void control_plane_task_monitor_task(void *arg)
}
#endif
#if CONFIG_TQ_DIRECTION_CONSOLE_ENABLE
static void control_plane_direction_console_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Direction console task started");
control_plane_print_help();
char input_buf[CONTROL_PLANE_INPUT_BUF_SIZE] = {0};
size_t input_len = 0;
bool prompt_printed = false;
bool ignore_lf_after_cr = false;
while (!control_plane_runtime_stop_requested()) {
if (!prompt_printed) {
printf("Enter direction > ");
fflush(stdout);
prompt_printed = true;
}
int ch = fgetc(stdin);
if (ch == EOF) {
clearerr(stdin);
vTaskDelay(pdMS_TO_TICKS(20));
continue;
}
if (ch == '\n' && ignore_lf_after_cr) {
ignore_lf_after_cr = false;
continue;
}
if (ch == '\r' || ch == '\n') {
ignore_lf_after_cr = (ch == '\r');
input_buf[input_len] = '\0';
printf("\n");
fflush(stdout);
prompt_printed = false;
control_plane_process_direction_command(input_buf);
input_len = 0;
continue;
}
ignore_lf_after_cr = false;
if (ch == '\b' || ch == 0x7F) {
if (input_len > 0) {
input_len--;
printf("\b \b");
fflush(stdout);
}
continue;
}
if (ch >= 0x20 && ch <= 0x7E) {
if (input_len < CONTROL_PLANE_INPUT_BUF_SIZE - 1) {
input_buf[input_len++] = (char)ch;
putchar(ch);
fflush(stdout);
}
}
}
control_plane_set_task_handle(&s_direction_console_task, NULL);
return;
}
#endif
#if CONFIG_TQ_SCREEN_ENABLE
static void control_plane_lcd_render_loop_task(void *arg)
{
@@ -1130,6 +998,16 @@ static esp_err_t control_plane_ensure_user_config_server(void)
static bool control_plane_try_start_multimodal_from_state_machine(TickType_t *last_attempt_tick,
bool foreground)
{
control_plane_multimodal_runtime_state_t state = {0};
if (control_plane_multimodal_get_runtime_state(&state) == ESP_OK) {
if (state.connected) {
return true;
}
if (state.starting || state.stopping) {
return false;
}
}
TickType_t now = xTaskGetTickCount();
TickType_t retry_ticks = pdMS_TO_TICKS(CONTROL_PLANE_WS_START_RETRY_DELAY_MS);
if (retry_ticks == 0) {
@@ -1150,11 +1028,21 @@ static bool control_plane_try_start_multimodal_from_state_machine(TickType_t *la
ESP_LOGI(TAG, "Multimodal client started");
return true;
}
if (err == ESP_ERR_INVALID_STATE) {
return false;
}
ESP_LOGW(TAG, "Multimodal client start failed: %s", esp_err_to_name(err));
return false;
}
static bool control_plane_multimodal_start_busy(void)
{
control_plane_multimodal_runtime_state_t state = {0};
return control_plane_multimodal_get_runtime_state(&state) == ESP_OK &&
(state.starting || state.stopping);
}
static void control_plane_ptt_set_state(ptt_state_t next_state, const char *reason)
{
ptt_state_t old_state = PTT_STATE_IDLE;
@@ -1214,10 +1102,11 @@ static void control_plane_handle_ptt_teardown(void)
control_plane_ptt_transition_locked(PTT_STATE_IDLE, "teardown_complete");
if (pending_press && active) {
s_ws_start_requested = true;
s_ptt_runtime.generation++;
s_ptt_runtime.foreground_connect = true;
s_ptt_runtime.foreground_attempts = 0;
s_ptt_runtime.press_tick = xTaskGetTickCount();
s_ptt_runtime.start_action_sent = false;
s_ptt_runtime.speech_action_sent = false;
control_plane_ptt_transition_locked(PTT_STATE_WAIT_WIFI, "pending_press");
}
generation = s_ptt_runtime.generation;
@@ -1270,6 +1159,10 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte
}
if (!control_plane_multimodal_is_connected()) {
if (ptt.state == PTT_STATE_STREAMING || control_plane_multimodal_is_uplink_running()) {
control_plane_ptt_set_state(PTT_STATE_TEARDOWN_LOCAL, "ws_unavailable_abort_uplink");
return;
}
control_plane_ptt_set_state(PTT_STATE_WAIT_WS, "ws_unavailable");
bool try_foreground = ptt.foreground_connect &&
ptt.foreground_attempts <
@@ -1278,10 +1171,14 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte
bool started = control_plane_try_start_multimodal_from_state_machine(
last_ws_start_attempt_tick,
true);
bool busy = control_plane_multimodal_start_busy();
if (!busy) {
portENTER_CRITICAL(&s_runtime_lock);
s_ptt_runtime.foreground_attempts++;
portEXIT_CRITICAL(&s_runtime_lock);
}
if (!started &&
!busy &&
ptt.foreground_attempts + 1U >=
CONTROL_PLANE_PTT_FOREGROUND_RECONNECT_MAX_ATTEMPTS) {
control_plane_ptt_set_state(PTT_STATE_DENIED, "ws_foreground_attempts_exhausted");
@@ -1333,8 +1230,27 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte
return;
}
ptt = control_plane_ptt_snapshot();
bool should_run_press_step = true;
if (ptt.state == PTT_STATE_WAIT_START_SENT && ptt.start_action_sent) {
should_run_press_step = false;
}
if (ptt.state == PTT_STATE_WAIT_SPEECH_STARTED && ptt.speech_action_sent) {
should_run_press_step = false;
}
if (!should_run_press_step) {
return;
}
esp_err_t press_err = control_plane_multimodal_handle_ptt_press_step();
if (press_err == ESP_OK) {
portENTER_CRITICAL(&s_runtime_lock);
if (s_ptt_runtime.state == PTT_STATE_WAIT_START_SENT) {
s_ptt_runtime.start_action_sent = true;
} else if (s_ptt_runtime.state == PTT_STATE_WAIT_SPEECH_STARTED) {
s_ptt_runtime.speech_action_sent = true;
}
portEXIT_CRITICAL(&s_runtime_lock);
control_plane_multimodal_reset_uplink_failure_window();
} else if (press_err != ESP_ERR_INVALID_STATE &&
press_err != ESP_ERR_NOT_SUPPORTED) {
@@ -1502,7 +1418,9 @@ static void control_plane_wifi_state_task(void *arg)
if (connected) {
bool ws_start_due = control_plane_consume_ws_start_requested();
if (ws_start_due && !control_plane_multimodal_is_connected()) {
if (ws_start_due &&
!control_plane_get_ptt_active() &&
!control_plane_multimodal_is_connected()) {
control_plane_try_start_multimodal_from_state_machine(&last_ws_start_attempt_tick, false);
now_tick = xTaskGetTickCount();
}
@@ -1844,23 +1762,6 @@ esp_err_t control_plane_start_runtime_workers(void)
(void)control_plane_schedule_websocket_wait_task();
#if CONFIG_TQ_DIRECTION_CONSOLE_ENABLE
if (control_plane_get_task_handle(&s_direction_console_task) == NULL) {
esp_err_t task_err = control_plane_create_task(control_plane_direction_console_task,
"cp_direction",
CONTROL_PLANE_DIRECTION_TASK_STACK_SIZE,
5,
CONTROL_PLANE_LOCAL_TASK_CORE,
&s_direction_console_task);
if (task_err != ESP_OK) {
control_plane_stop_runtime_workers();
return task_err;
}
}
#else
ESP_LOGI(TAG, "Direction console disabled by configuration");
#endif
#if CONFIG_TQ_SCREEN_ENABLE
esp_err_t lcd_worker_err = control_plane_start_lcd_render_worker();
if (lcd_worker_err != ESP_OK) {
@@ -1988,11 +1889,6 @@ void control_plane_stop_runtime_workers(void)
ESP_LOGW(TAG, "LCD render loop stop ended with %s", esp_err_to_name(lcd_stop_err));
}
#if CONFIG_TQ_DIRECTION_CONSOLE_ENABLE
if (!control_plane_wait_task_exit(&s_direction_console_task, CONTROL_PLANE_TASK_STOP_WAIT_MS)) {
control_plane_force_delete_task(&s_direction_console_task, "cp_direction");
}
#endif
if (!control_plane_wait_task_exit(&s_lcd_render_loop_task, CONTROL_PLANE_TASK_STOP_WAIT_MS)) {
control_plane_force_delete_task(&s_lcd_render_loop_task, "cp_lcd_render");
}
@@ -29,9 +29,11 @@ static float s_current_angle = 0.0f;
static int s_rotate_speed_us = DOMAIN_DEFAULT_ROTATE_SPEED_US;
static bool s_initialized;
static SemaphoreHandle_t s_state_mutex;
static SemaphoreHandle_t s_rotation_mutex;
static portMUX_TYPE s_state_mutex_init_lock = portMUX_INITIALIZER_UNLOCKED;
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
static StaticSemaphore_t s_state_mutex_buf;
static StaticSemaphore_t s_rotation_mutex_buf;
#endif
static const domain_direction_axis_config_t s_default_axis_config = {
@@ -94,6 +96,55 @@ static bool ensure_state_mutex(void)
return (s_state_mutex != NULL);
}
static bool ensure_rotation_mutex(void)
{
if (s_rotation_mutex != NULL) {
return true;
}
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
portENTER_CRITICAL(&s_state_mutex_init_lock);
if (s_rotation_mutex == NULL) {
s_rotation_mutex = xSemaphoreCreateMutexStatic(&s_rotation_mutex_buf);
}
portEXIT_CRITICAL(&s_state_mutex_init_lock);
#else
SemaphoreHandle_t created_mutex = xSemaphoreCreateMutex();
portENTER_CRITICAL(&s_state_mutex_init_lock);
if (s_rotation_mutex == NULL) {
s_rotation_mutex = created_mutex;
created_mutex = NULL;
}
portEXIT_CRITICAL(&s_state_mutex_init_lock);
if (created_mutex != NULL) {
vSemaphoreDelete(created_mutex);
}
#endif
return (s_rotation_mutex != NULL);
}
static bool domain_direction_lock_rotation(int timeout_ms)
{
if (!ensure_rotation_mutex()) {
return false;
}
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_ROTATE_TIMEOUT_MS;
TickType_t timeout_ticks = pdMS_TO_TICKS(effective_timeout_ms);
if (timeout_ticks == 0) {
timeout_ticks = 1;
}
return (xSemaphoreTake(s_rotation_mutex, timeout_ticks) == pdTRUE);
}
static void domain_direction_unlock_rotation(void)
{
if (s_rotation_mutex != NULL) {
(void)xSemaphoreGive(s_rotation_mutex);
}
}
bool domain_direction_lock_state(int timeout_ms)
{
if (!ensure_state_mutex()) {
@@ -253,37 +304,62 @@ esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms
TickType_t start_tick = xTaskGetTickCount();
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_ROTATE_TIMEOUT_MS;
if (!domain_direction_lock_state(effective_timeout_ms)) {
if (!domain_direction_lock_rotation(effective_timeout_ms)) {
return ESP_ERR_TIMEOUT;
}
int state_timeout_ms = remaining_timeout_ms(start_tick, effective_timeout_ms);
if (state_timeout_ms <= 0) {
domain_direction_unlock_rotation();
return ESP_ERR_TIMEOUT;
}
if (!domain_direction_lock_state(state_timeout_ms)) {
domain_direction_unlock_rotation();
return ESP_ERR_TIMEOUT;
}
if (!s_initialized) {
domain_direction_unlock_state();
domain_direction_unlock_rotation();
return ESP_ERR_INVALID_STATE;
}
float normalized_target = domain_angle_normalize(target_angle);
float rotation = domain_angle_shortest_rotation(s_current_angle, normalized_target);
float current_angle = s_current_angle;
int rotate_speed_us = s_rotate_speed_us;
float rotation = domain_angle_shortest_rotation(current_angle, normalized_target);
if (rotation == 0.0f) {
domain_direction_unlock_state();
domain_direction_unlock_rotation();
return ESP_OK;
}
domain_direction_unlock_state();
int motor_timeout_ms = remaining_timeout_ms(start_tick, effective_timeout_ms);
if (motor_timeout_ms <= 0) {
domain_direction_unlock_state();
domain_direction_unlock_rotation();
return ESP_ERR_TIMEOUT;
}
esp_err_t motor_err = domain_runtime_stepper_rotate_angle_with_accel_timeout(
rotation,
s_rotate_speed_us,
rotate_speed_us,
motor_timeout_ms);
if (motor_err != ESP_OK) {
domain_direction_unlock_state();
domain_direction_unlock_rotation();
return motor_err;
}
s_current_angle = domain_angle_normalize(s_current_angle + rotation);
state_timeout_ms = remaining_timeout_ms(start_tick, effective_timeout_ms);
if (state_timeout_ms <= 0) {
domain_direction_unlock_rotation();
return ESP_ERR_TIMEOUT;
}
if (!domain_direction_lock_state(state_timeout_ms)) {
domain_direction_unlock_rotation();
return ESP_ERR_TIMEOUT;
}
s_current_angle = domain_angle_normalize(current_angle + rotation);
domain_direction_unlock_state();
domain_direction_unlock_rotation();
return ESP_OK;
}
@@ -139,7 +139,7 @@ static const domain_expression_catalog_entry_t s_expression_catalog[] = {
.priority = 100,
.interrupt_current = true,
.return_to_idle = true,
.asset_dir = "server_error",
.asset_dir = "jichubiaoqing",
.frame_order = s_server_error_frame_order,
.frame_order_count = sizeof(s_server_error_frame_order) / sizeof(s_server_error_frame_order[0]),
.repeat_count = 5,
@@ -867,43 +867,51 @@ static esp_err_t load_legacy_auth_state(void *out, size_t out_len, size_t *out_l
return ESP_ERR_INVALID_SIZE;
}
domain_auth_state_legacy_blob_t state = {0};
domain_auth_state_legacy_blob_t *state = calloc(1, sizeof(*state));
if (state == NULL) {
return ESP_ERR_NO_MEM;
}
bool found_any = false;
size_t jwt_len = 0U;
esp_err_t err = platform_storage_get_string(DOMAIN_AUTH_STATE_STORE_NAMESPACE,
DOMAIN_AUTH_LEGACY_STATE_JWT_KEY,
state.jwt,
sizeof(state.jwt),
state->jwt,
sizeof(state->jwt),
&jwt_len);
if (err == ESP_OK) {
found_any = true;
state.jwt_valid = jwt_len > 1U && state.jwt[0] != '\0';
state->jwt_valid = jwt_len > 1U && state->jwt[0] != '\0';
} else if (err != ESP_ERR_NOT_FOUND) {
free(state);
return err;
}
err = storage_get_optional_u64(DOMAIN_AUTH_STATE_STORE_NAMESPACE,
DOMAIN_AUTH_LEGACY_STATE_EXP_KEY,
&state.exp_epoch_s,
&state->exp_epoch_s,
&found_any);
if (err != ESP_OK) {
free(state);
return err;
}
err = storage_get_optional_u32(DOMAIN_AUTH_STATE_STORE_NAMESPACE,
DOMAIN_AUTH_LEGACY_STATE_SCOPE_KEY,
&state.scope_mask,
&state->scope_mask,
&found_any);
if (err != ESP_OK) {
free(state);
return err;
}
err = storage_get_optional_u32(DOMAIN_AUTH_STATE_STORE_NAMESPACE,
DOMAIN_AUTH_LEGACY_STATE_KEY_VERSION_KEY,
&state.key_version,
&state->key_version,
&found_any);
if (err != ESP_OK) {
free(state);
return err;
}
@@ -913,34 +921,39 @@ static esp_err_t load_legacy_auth_state(void *out, size_t out_len, size_t *out_l
&wait_factory,
&found_any);
if (err != ESP_OK) {
free(state);
return err;
}
state.wait_factory_persisted = wait_factory != 0U;
state->wait_factory_persisted = wait_factory != 0U;
err = storage_get_optional_u32(DOMAIN_AUTH_STATE_STORE_NAMESPACE,
DOMAIN_AUTH_LEGACY_STATE_FAIL_COUNT_KEY,
&state.fail_count,
&state->fail_count,
&found_any);
if (err != ESP_OK) {
free(state);
return err;
}
err = storage_get_optional_u64(DOMAIN_AUTH_STATE_STORE_NAMESPACE,
DOMAIN_AUTH_LEGACY_STATE_NEXT_RETRY_KEY,
&state.next_retry_epoch_s,
&state->next_retry_epoch_s,
&found_any);
if (err != ESP_OK) {
free(state);
return err;
}
if (!found_any) {
free(state);
return ESP_ERR_NOT_FOUND;
}
memcpy(out, &state, sizeof(state));
memcpy(out, state, sizeof(*state));
if (out_len_actual != NULL) {
*out_len_actual = sizeof(state);
*out_len_actual = sizeof(*state);
}
free(state);
return ESP_OK;
}
@@ -156,7 +156,7 @@ static bool s_connected;
static bool s_stop_requested;
static portMUX_TYPE s_client_state_lock = portMUX_INITIALIZER_UNLOCKED;
static char s_bearer_token[MULTIMODAL_WS_TOKEN_MAX_LEN];
static char s_headers[384];
static char s_headers[768];
static multimodal_ws_event_callback_t s_event_cb;
static void *s_event_cb_ctx;
@@ -32,7 +32,9 @@ static void websocket_event_handler(void *handler_args,
stop_requested ? "WebSocket disconnected (local stop)" :
"WebSocket disconnected");
ws_set_stop_requested(false);
if (!stop_requested) {
emit_event(MULTIMODAL_WS_EVENT_DISCONNECTED, NULL, 0U);
}
break;
}
@@ -253,6 +253,7 @@ esp_err_t wifi_portal_start_config_ap(const char *ssid_prefix)
}
esp_err_t wifi_portal_connect_sta(int timeout_ms)
{
{
ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal");
PortalUnlockGuard guard;
@@ -276,17 +277,18 @@ esp_err_t wifi_portal_connect_sta(int timeout_ms)
manager.StartStation();
(void)apply_sta_stability_policy();
}
int effective_timeout_ms = normalize_connect_timeout_ms(timeout_ms);
int64_t deadline_us = esp_timer_get_time() + (int64_t)effective_timeout_ms * 1000;
while (esp_timer_get_time() < deadline_us) {
if (manager.IsConnected()) {
if (WifiManager::GetInstance().IsConnected()) {
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS));
}
manager.StopStation();
WifiManager::GetInstance().StopStation();
ESP_LOGW(TAG, "Wi-Fi connect timed out after %d ms", effective_timeout_ms);
return ESP_ERR_TIMEOUT;
}
+367
View File
@@ -0,0 +1,367 @@
sp-idf-v5.5.4\tools\idf_monitor.py' -p COM5 -b 921600 --toolchain-prefix xtensa-esp32s3-elf- --make '''C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe'' ''C:\Espressif\frameworks\esp-idf-v5.5.4\tools\idf.py''' --target esp32s3 'd:\Code\chaowan\TQ_catN\build\TQ_Cat_Code.elf'
--- Warning: GDB cannot open serial ports accessed as COMx
--- Using \\.\COM5 instead...
--- esp-idf-monitor 1.9.0 on \\.\COM5 921600
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
could not open port '\\\\.\\COM5': FileNotFoundError(2, '绯荤粺鎵句笉鍒版寚瀹氱殑鏂囦欢銆?, None, 2)
--- Connection to \\.\COM5 failed. Available ports:
PS D:\Code\chaowan\TQ_catN>
PS D:\Code\chaowan\TQ_catN> $env:IDF_PATH = 'C:/Espressif/frameworks/esp-idf-v5.5.4/';
PS D:\Code\chaowan\TQ_catN> & 'C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe' 'C:\Espressif\frameworks\esp-idf-v5.5.4\tools\idf_monitor.py' -p COM5 -b 921600 --toolchain-prefix xtensa-esp32s3-elf- --make '''C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe'' ''C:\Espressif\frameworks\esp-idf-v5.5.4\tools\idf.py''' --target esp32s3 'd:\Code\chaowan\TQ_catN\build\TQ_Cat_Code.elf'
--- Warning: GDB cannot open serial ports accessed as COMx
--- Using \\.\COM5 instead...
--- esp-idf-monitor 1.9.0 on \\.\COM5 921600
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
I (324) esp_image: segment 1: paddr=0093496c vaddr=3fc9e100 sizeESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0x1b (SPI_FAST_FLASH_BOOT)
Saved PC:0x4037bca6
--- 0x4037bca6: esp_cpu_wait_for_intr at C:/Espressif/frameworks/esp-idf-v5.5.4/components/esp_hw_support/cpu.c:64
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce2820,len:0x14f0
load:0x403c8700,len:0xd24
load:0x403cb700,len:0x2ef0
entry 0x403c8924
I (24) boot: ESP-IDF v5.5.4 2nd stage bootloader
I (24) boot: compile time Jun 26 2026 10:07:28
I (25) boot: Multicore bootloader
I (25) boot: chip revision: v0.2
I (28) boot: efuse block revision: v1.3
I (31) boot.esp32s3: Boot SPI Speed : 80MHz
I (35) boot.esp32s3: SPI Mode : DIO
I (39) boot.esp32s3: SPI Flash Size : 16MB
I (43) boot: Enabling RNG early entropy source...
I (47) boot: Partition Table:
I (50) boot: ## Label Usage Type ST Offset Length
I (56) boot: 0 nvs WiFi data 01 02 00009000 00004000
I (63) boot: 1 otadata OTA data 01 00 0000d000 00002000
I (69) boot: 2 phy_init RF data 01 01 0000f000 00001000
I (76) boot: 3 gifpack Unknown data 01 83 00010000 00800000
I (82) boot: 4 ota_0 OTA app 00 10 00810000 00330000
I (89) boot: 5 ota_1 OTA app 00 11 00b40000 00330000
I (95) boot: 6 model Unknown data 01 82 00e70000 00180000
I (102) boot: End of partition table
I (105) esp_image: segment 0: paddr=00810020 vaddr=3c110020 size=124944h (1198404) map
I (324) esp_image: segment 1: paddr=0093496c vaddr=3fc9e100 size=0578ch ( 22412) load
I (329) esp_image: segment 2: paddr=0093a100 vaddr=40374000 size=05f18h ( 24344) load
I (334) esp_image: segment 3: paddr=00940020 vaddr=42000020 size=10daf8h (1104632) map
I (530) esp_image: segment 4: paddr=00a4db20 vaddr=40379f18 size=14164h ( 82276) load
I (548) esp_image: segment 5: paddr=00a61c8c vaddr=50000000 size=00020h ( 32) load
I (559) boot: Loaded app from partition at offset 0x810000
I (559) boot: Disabling RNG early entropy source...
I (569) octal_psram: vendor id : 0x0d (AP)
I (569) octal_psram: dev id : 0x02 (generation 3)
I (570) octal_psram: density : 0x03 (64 Mbit)
I (571) octal_psram: good-die : 0x01 (Pass)
I (576) octal_psram: Latency : 0x01 (Fixed)
I (580) octal_psram: VCC : 0x01 (3V)
I (584) octal_psram: SRF : 0x01 (Fast Refresh)
I (589) octal_psram: BurstType : 0x01 (Hybrid Wrap)
I (594) octal_psram: BurstLen : 0x01 (32 Byte)
I (598) octal_psram: Readlatency : 0x02 (10 cycles@Fixed)
I (604) octal_psram: DriveStrength: 0x00 (1/1)
I (608) MSPI Timing: Enter psram timing tuning
I (613) esp_psram: Found 8MB PSRAM device
I (616) esp_psram: Speed: 80MHz
I (619) cpu_start: Multicore app
I (912) esp_psram: SPI SRAM memory test OK
I (920) cpu_start: GPIO 44 and 43 are used as console UART I/O pins
I (921) cpu_start: Pro cpu start user code
I (921) cpu_start: cpu freq: 240000000 Hz
I (923) app_init: Application information:
I (927) app_init: Project name: TQ_Cat_Code
I (931) app_init: App version: 6aaf13d-dirty
I (935) app_init: Compile time: Jun 27 2026 11:00:31
I (940) app_init: ELF file SHA256: 8cd15c4b1...
--- Warning: Checksum mismatch between flashed and built applications. Checksum of built application is 6bbc2d153c535122f3e0520a7e0b375ceed0070d0ec1f234d950f57c0548b115
I (945) app_init: ESP-IDF: v5.5.4
I (948) efuse_init: Min chip rev: v0.0
I (952) efuse_init: Max chip rev: v0.99
I (956) efuse_init: Chip rev: v0.2
I (960) heap_init: Initializing. RAM available for dynamic allocation:
I (966) heap_init: At 3FCA7AE0 len 00041C30 (263 KiB): RAM
I (972) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM
I (977) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM
I (982) esp_psram: Adding pool of 8178K of PSRAM memory to heap allocator
I (989) spi_flash: detected chip: generic
I (992) spi_flash: flash io: dio
I (995) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (1002) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (1008) main_task: Started on CPU0
I (1018) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1018) main_task: Calling app_main()
I (1018) app_comp_boot: ========================================
I (1028) app_comp_boot: App Composition Boot Plan A
I (1028) app_comp_boot: dry_run=0
I (1038) app_comp_boot: ========================================
I (1038) app_comp_boot: boot stage 01 power_hold event=already_asserted_by_platform_core
I (1048) app_comp_boot: boot stage 02 audio_init event=boot_sound_policy_begin
I (1058) app_comp_boot: boot stage 03 boot_sound event=play_boot_sound_with_timeout
I (1068) control_plane_rt: Boot sound policy start timeout=3000ms
I (1078) multimodal_audio: Heap audio_init_begin: free8=8647743 largest8=8257536 free_internal=275643 largest_internal=212992 largest_dma=212992 free_spiram=8372100 largest_spiram=8257536
I (1088) multimodal_audio: Heap before_opus_init: free8=8647743 largest8=8257536 free_internal=275643 largest_internal=212992 largest_dma=212992 free_spiram=8372100 largest_spiram=8257536
I (1108) multimodal_audio: Heap before_opus_enc_open: free8=8647743 largest8=8257536 free_internal=275643 largest_internal=212992 largest_dma=212992 free_spiram=8372100 largest_spiram=8257536
I (1118) multimodal_audio: Try opus profile=configured sr=16000 ch=1 frame=5 bitrate=24000 app=0 complexity=0 vbr=0 dtx=0 fec=0
I (1138) multimodal_audio: Opus ready: 16000Hz frame=60ms bitrate=24kbps enc(in=1920 out=280)
I (1138) multimodal_audio: Heap after_opus_init: free8=8596159 largest8=8257536 free_internal=275567 largest_internal=212992 largest_dma=212992 free_spiram=8320592 largest_spiram=8257536
I (1158) multimodal_audio: Heap after_opus_ready: free8=8596159 largest8=8257536 free_internal=275567 largest_internal=212992 largest_dma=212992 free_spiram=8320592 largest_spiram=8257536
I (1178) multimodal_audio: I2C ready: port=0 sda=1 scl=2 ctrl(8-bit)=0x30 probe(7-bit)=0x18
I (1188) multimodal_audio: I2S ready: port=0 mclk=38 bclk=39 ws=41 dout=40 din=42 use_mclk=1 dma(desc=4 frame=160)
I (1198) ES8311: Work in Slave mode
I (1198) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1
I (1198) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1
I (1208) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1
I (1218) I2S_IF: STD Mode 0 bits:16/16 channel:2 sample_rate:16000 mask:1
I (1238) Adev_Codec: Open codec device OK
I (1238) multimodal_audio: Codec controls: out_volume=60 in_gain=30 pa_gpio=12 output_gate=volume hw_mute=0
I (1238) multimodal_audio: Using fixed capture map MIC=ch0
I (1248) multimodal_audio: Heap after_codec_ready: free8=8590095 largest8=8257536 free_internal=271723 largest_internal=212992 largest_dma=212992 free_spiram=8318372 largest_spiram=8257536
I (1258) multimodal_audio: Mic uplink capture task started (stack=32768B opus in=1920B out=280B read=1920B channels=1 mic_ch=0)
I (1258) multimodal_audio: Mic uplink send task started (stack=12288B queue=24 packet_max=1024B)
I (1258) multimodal_audio: Downstream playback queue allocated in PSRAM: len=1024 item=8
I (1288) multimodal_audio: Codec initialized: 16000Hz 1ch 16bit opus(frame=60ms bitrate=24kbps)
I (1288) multimodal_audio: Downstream playback task started (stack=12288B queue=1024 packet_max=2048B)
I (1338) voice_prompt: LittleFS mounted at /littlefs, used=458752 total=8388608
I (1338) multimodal_audio: Downstream response begin: sample_rate=16000 frame=60ms codec_channels=1 pa_gpio=12 out_volume=60
I (1338) multimodal_audio: Downstream TX bounce buffer allocated in internal RAM: 3200B
I (1348) multimodal_audio: Downstream idle mute timer ready (timeout=250ms)
I (1358) multimodal_audio: Downstream output primed: volume=60 warmup=320B channels=1
I (2138) multimodal_audio: Downstream playback completed after response end (queue_empty)
I (2268) voice_prompt: Prompt queued: /littlefs/audio_prompts/boot.opus packets=13 bytes=2244
I (2268) control_plane_rt: Boot sound audio resources kept for runtime
I (2268) app_comp_boot: boot stage 04 audio_ready event=boot_sound_policy_end_keep_runtime
I (2278) app_comp_boot: boot stage 05 lcd_device event=control_plane_init_lcd_device
I (2368) multimodal_audio: Downstream idle mute after 250ms (response_active=0)
I (2408) lcd_direct: Direct LCD initialized (MOSI=17 SCLK=18 CS=6 DC=7 RST=3 BL=9 clock=20000000)
I (2408) app_comp_boot: boot stage 05 lcd_device result=ESP_OK
I (2408) app_comp_boot: boot stage 06 lcd_render_loop event=control_plane_start_lcd_render_worker
I (2418) cp_rt_workers: LCD render worker started
I (2418) lcd_direct: Direct LCD render loop started
I (2418) app_comp_boot: boot stage 06 lcd_render_loop result=ESP_OK
I (2438) app_comp_boot: boot stage 07 platform_domain event=control_plane_init
I (2438) platform_core: Platform initialized
I (2448) domain: Direction axis initialized (angle=0.0, speed=1500us)
I (2448) domain_rt_adapters: Expression request id=0 trigger=0
I (2458) lcd_direct: Direct LCD presentation queued kind=1 asset=jichubiaoqing
I (2468) app_comp_boot: boot stage 08 turntable_calibration skipped=disabled_by_config
I (2468) app_comp_boot: boot stage 09 storage_prepare event=control_plane_prepare_runtime_storage
I (2488) platform_core: NVS stats: used_entries=152 free_entries=352 total_entries=504 namespace_count=6
I (2488) platform_core: NVS namespace "wifi": used_entries=8 free_entries_shared=352
I (2498) platform_core: NVS namespace "ota": used_entries=5 free_entries_shared=352
I (2508) platform_core: NVS namespace "auth": used_entries=51 free_entries_shared=352
I (2518) platform_core: NVS namespace "auth_key": used_entries=7 free_entries_shared=352
I (2518) platform_core: NVS namespace "fortune": used_entries=0 free_entries_shared=352
I (2528) app_comp_boot: boot stage 10 wifi_bootstrap event=control_plane_bootstrap_connectivity
I (2538) control_plane_rt: Boot Wi-Fi config SSID=csy2 timeout_ms=30000
I (2548) platform_core: NVS stats: used_entries=152 free_entries=352 total_entries=504 namespace_count=6
I (2558) platform_core: NVS namespace "wifi": used_entries=8 free_entries_shared=352
I (2558) platform_core: NVS namespace "ota": used_entries=5 free_entries_shared=352
I (2568) platform_core: NVS namespace "auth": used_entries=51 free_entries_shared=352
I (2578) platform_core: NVS namespace "auth_key": used_entries=7 free_entries_shared=352
I (2588) platform_core: NVS namespace "fortune": used_entries=0 free_entries_shared=352
I (2588) SsidManager: compare [csy2:4] [csy2:4]
W (2598) SsidManager: SSID csy2 already exists, overwrite it
I (2598) wifi_portal: Ensured configured Wi-Fi credentials for SSID "csy2" with 2 previous candidate(s)
I (2608) wifi_portal: Starting STA connection with 2 saved credential candidate(s)
I (2618) wifi_portal: STA connect: 2 saved Wi-Fi credential candidate(s)
I (2628) wifi_portal: STA connect: candidate[0] SSID="csy2"
I (2628) wifi_portal: STA connect: candidate[1] SSID="TalkingQ"
I (2638) WifiManager: Initializing...
I (2638) pp: pp rom version: e7ae62f
I (2638) net80211: net80211 rom version: e7ae62f
I (2658) wifi:wifi driver task: 3fcbb87c, prio:23, stack:6656, core=0
I (2658) wifi:wifi firmware version: 0947e8b
I (2658) wifi:wifi certification version: v7.0
I (2658) wifi:config NVS flash: disabled
I (2668) wifi:config nano formatting: disabled
I (2668) wifi:Init data frame dynamic rx buffer num: 16
I (2668) wifi:Init static rx mgmt buffer num: 5
I (2678) wifi:Init management short buffer num: 32
I (2678) wifi:Init static tx buffer num: 8
I (2688) wifi:Init tx cache buffer num: 16
I (2688) wifi:Init static tx FG buffer num: 2
I (2698) wifi:Init static rx buffer size: 1600
I (2698) wifi:Init static rx buffer num: 8
I (2698) wifi:Init dynamic rx buffer num: 16
I (2708) wifi_init: rx ba win: 6
I (2708) wifi_init: accept mbox: 6
I (2708) wifi_init: tcpip mbox: 32
I (2718) wifi_init: udp mbox: 6
I (2718) wifi_init: tcp mbox: 6
I (2718) wifi_init: tcp tx win: 5760
I (2728) wifi_init: tcp rx win: 5760
I (2728) wifi_init: tcp mss: 1440
I (2728) wifi_init: WiFi/LWIP prefer SPIRAM
I (2738) wifi_init: WiFi IRAM OP enabled
I (2738) wifi_init: WiFi RX IRAM OP enabled
I (2738) WifiManager: Initialized
I (2748) WifiManager: Starting station
I (2748) phy_init: phy_version 711,97bcf0a2,Aug 25 2025,19:04:10
I (2798) wifi:mode : sta (30:ed:a0:aa:b2:78)
I (2798) wifi:enable tsf
I (2798) wifi:Set ps type: 0, coexist: 0
I (2798) wifi_portal: Wi-Fi STA stability policy: ps=none inactive=15s
I (3078) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (5208) WifiStation: Found AP: csy2, BSSID: 64:23:15:28:70:ac, RSSI: -74, Channel: 1, Authmode: 4
I (5218) wifi_portal: Connecting to "csy2"
W (5218) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2
I (5298) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0
I (5298) wifi:state: init -> auth (0xb0)
I (5308) wifi:state: auth -> assoc (0x0)
I (5318) wifi:state: assoc -> run (0x10)
I (5328) wifi:connected with csy2, aid = 13, channel 1, BW20, bssid = 64:23:15:28:70:ac
I (5328) wifi:security: WPA2-PSK, phy: bgn, rssi: -72, cipher(pairwise:0x3, group:0x1), pmf:0
I (5338) wifi:pm start, type: 0
I (5338) wifi:dp: 1, bi: 102400, li: 10, scale listen interval from 1024000 us to 1024000 us
I (5348) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000
I (5418) wifi:AP's beacon interval = 102400 us, DTIM period = 1
I (6428) esp_netif_handlers: sta ip: 192.168.3.44, mask: 255.255.255.0, gw: 192.168.3.1
I (6428) WifiStation: Got IP: 192.168.3.44
I (6428) wifi_portal: Connected to "csy2"
I (6428) wifi:Set ps type: 0, coexist: 0
I (6428) wifi_portal: Wi-Fi STA stability policy: ps=none inactive=15s
I (6498) multimodal_audio: Downstream response begin: sample_rate=16000 frame=60ms codec_channels=1 pa_gpio=12 out_volume=60
I (6498) multimodal_audio: Downstream output primed: volume=60 warmup=320B channels=1
I (7998) multimodal_audio: Downstream playback completed after response end (queue_empty)
I (8128) voice_prompt: Prompt queued: /littlefs/audio_prompts/network_connect_success.opus packets=25 bytes=4678
I (8128) app_comp_boot: boot stage 11 runtime_workers event=control_plane_start_runtime_workers
I (8128) cp_rt_workers: Direction console disabled by configuration
I (8138) cp_rt_workers: LCD render worker already running
I (8138) cp_rt_workers: Wi-Fi state monitor task started
I (8148) platform_lan_http: Heap after_io_buffers: free_internal=182739 largest_internal=139264 largest_dma=139264 free_spiram=8157304 largest_spiram=8126464
I (8168) mdns_mem: mDNS task will be created from SPIRAM
I (8168) platform_lan_http: LAN user configuration available at http://192.168.3.44:80/ and http://tq-cat.local:80/
I (8178) cp_system_event: System event task started queue_len=32
I (8178) cp_key_input: Key input task started irq_queue_len=16 debounce=20ms long=800ms double=450ms
I (8178) cp_rt_workers: Daily fortune worker started
I (8178) app_comp_boot: boot stage 12 network_services event=started_by_runtime_workers_after_readiness
I (8208) main_task: Returned from app_main()
I (8228) multimodal_audio: Downstream idle mute after 250ms (response_active=0)
I (8398) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
W (8688) cp_key_input: PTT initial level is active; wait for next stable press before emitting press event
I (8688) cp_key_input: key config key=PTT gpio=15 active_level=0 pullup=1 pulldown=0 initial_level=0
I (8698) cp_key_input: key config key=SW2 gpio=45 active_level=0 pullup=1 pulldown=0 initial_level=1
I (9178) cp_mm_auth: Request device token: https://gz.api.talkingq.com/token/api/device/token
I (9178) cp_dev_auth: Auth state restored (state=READY jwt_valid=1 exp=1782549499 scope=0x0000000f kv=1 wait_factory=0 fail_count=0)
I (9278) cp_dev_auth: Loaded auth key material (key_version=1)
I (9278) cp_mm_auth: Heap before token POST: free_internal=162231 largest_internal=116736 largest_dma=116736 free_spiram=8150284
I (9368) wifi:<ba-add>idx:0 (ifx:0, 64:23:15:28:70:ac), tid:0, ssn:5, winSize:64
I (9488) esp-x509-crt-bundle: Certificate validated
I (9878) cp_mm_auth: Token fetched (workspace=llm-igyrxzdvbwja5gyt, app=6a0dabccd1514145baef5fe202793b09)
I (9878) multimodal_ws: WS ordered worker queue: ingress=512 stack=12288
I (9888) multimodal_ws: Heap before_ws_client_start: free_internal=161727 largest_internal=108544 largest_dma=108544 free_spiram=8128620 largest_spiram=7995392
I (9898) multimodal_ws: Smallest WebSocket stack candidate=8192
I (9898) multimodal_ws: Try WebSocket stack=8192 buf=2048 ping=20s pong_to=120s keepalive=1
I (9918) websocket_client: Started
I (9918) multimodal_ws: Heap after_ws_client_start: free_internal=144219 largest_internal=100352 largest_dma=100352 free_spiram=8128644 largest_spiram=7995392
I (9928) multimodal_ws: WebSocket starting: wss://dashscope.aliyuncs.com/api-ws/v1/inference
I (9938) cp_rt_workers: Multimodal client started
I (10098) esp-x509-crt-bundle: Certificate validated
I (10398) multimodal_ws: WebSocket connected
I (10398) multimodal_ws: Heap ws_connected: free_internal=142331 largest_internal=98304 largest_dma=98304 free_spiram=8128436 largest_spiram=7995392
I (10408) multimodal_audio: Downstream response begin: sample_rate=16000 frame=60ms codec_channels=1 pa_gpio=12 out_volume=60
I (10418) multimodal_audio: Downstream output primed: volume=60 warmup=320B channels=1
I (12338) multimodal_audio: Downstream playback completed after response end (queue_empty)
I (12468) voice_prompt: Prompt queued: /littlefs/audio_prompts/server_connect_success.opus packets=32 bytes=6174
I (12568) multimodal_audio: Downstream idle mute after 250ms (response_active=0)
I (13748) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (19028) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (20588) cp_key_input: gpio change key=PTT gpio=15 level=0
I (24108) cp_key_input: gpio change key=PTT gpio=15 level=0
I (24108) cp_key_input: gpio change key=PTT gpio=15 level=1
I (24308) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (29588) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (33618) cp_key_input: gpio change key=PTT gpio=15 level=0
I (33618) cp_key_input: emit key=PTT type=PRESSED gpio=15 level=0 duration=0ms clicks=0
I (33618) cp_system_event: dispatch family=KEY
I (33618) cp_system_event: key event key=PTT type=PRESSED gpio=15 level=0 duration=0ms clicks=0
I (33938) cp_mm_session: Start sent (upstream_mode=push2talk voice=longjielidou_v3 downstream_text=transcript,dialog downstream_rate_limit=3000B/s incremental=0 sample_rate=16000 frame=60ms bitrate=24kbps channels=1 pa_gpio=12)
I (34268) multimodal_audio: Heap before_uplink_start: free8=8269359 largest8=7995392 free_internal=142387 largest_internal=98304 largest_dma=98304 free_spiram=8126972 largest_spiram=7995392
I (34268) multimodal_audio: Heap after_uplink_start: free8=8269359 largest8=7995392 free_internal=142387 largest_internal=98304 largest_dma=98304 free_spiram=8126972 largest_spiram=7995392
I (34318) cp_mm_session: ASR started
I (34978) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (37208) cp_key_input: gpio change key=PTT gpio=15 level=1
I (37208) cp_key_input: emit key=PTT type=RELEASED gpio=15 level=1 duration=3593ms clicks=0
I (37208) cp_system_event: dispatch family=KEY
I (37218) cp_system_event: key event key=PTT type=RELEASED gpio=15 level=1 duration=3593ms clicks=0
I (37228) multimodal_audio: Heap after_uplink_stop: free8=8269211 largest8=7995392 free_internal=142259 largest_internal=98304 largest_dma=98304 free_spiram=8126952 largest_spiram=7995392
I (37398) cp_mm_session: ASR (final): 你好。
I (37398) multimodal_audio: Heap after_uplink_stop: free8=8267499 largest8=7995392 free_internal=141367 largest_internal=98304 largest_dma=98304 free_spiram=8126132 largest_spiram=7995392
I (37408) cp_mm_session: ASR ended
I (37408) multimodal_audio: Heap after_uplink_stop: free8=8268747 largest8=7995392 free_internal=141207 largest_internal=98304 largest_dma=98304 free_spiram=8127540 largest_spiram=7995392
I (37668) cp_key_input: emit key=PTT type=SHORT_PRESS gpio=15 level=1 duration=0ms clicks=1
I (37668) cp_system_event: dispatch family=KEY
I (37668) cp_system_event: key event key=PTT type=SHORT_PRESS gpio=15 level=1 duration=0ms clicks=1
I (38518) cp_mm_session: TTS (final): 喵~今天周六下午好呀,要不要查查今日财位,或者抽支签放松一下?本...
I (38628) multimodal_audio: Heap after_uplink_stop: free8=8268595 largest8=7995392 free_internal=141287 largest_internal=98304 largest_dma=98304 free_spiram=8127308 largest_spiram=7995392
I (38628) cp_mm_session: TTS responding started
I (38638) multimodal_audio: Downstream response begin: sample_rate=16000 frame=60ms codec_channels=1 pa_gpio=12 out_volume=60
I (38648) multimodal_audio: Downstream output primed: volume=60 warmup=320B channels=1
I (38698) multimodal_ws: WebSocket send text done (LocalRespondingStarted, realtime=1, bytes=217, elapsed_ms=10)
I (40198) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (45268) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (45308) cp_mm_session: TTS responding ended
I (47228) multimodal_audio: Downstream playback completed after response end (queue_empty)
I (47238) multimodal_ws: WebSocket send text done (LocalRespondingEnded, realtime=1, bytes=215, elapsed_ms=10)
I (47458) multimodal_audio: Downstream idle mute after 250ms (response_active=0)
I (50598) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (53828) cp_key_input: gpio change key=PTT gpio=15 level=0
I (53828) cp_key_input: emit key=PTT type=PRESSED gpio=15 level=0 duration=0ms clicks=0
I (53828) cp_system_event: dispatch family=KEY
I (53838) cp_system_event: key event key=PTT type=PRESSED gpio=15 level=0 duration=0ms clicks=0
I (54328) multimodal_audio: Heap before_uplink_start: free8=8258079 largest8=7995392 free_internal=141419 largest_internal=98304 largest_dma=98304 free_spiram=8119980 largest_spiram=7995392
I (54328) multimodal_audio: Heap after_uplink_start: free8=8261399 largest8=7995392 free_internal=141419 largest_internal=98304 I (54338) cp_mm_session: ASR started
largest_dma=98304 free_spiram=8119980 largest_spiram=7995392
I (55818) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (59368) multimodal_audio: Mic uplink sent: packets=84 bytes=15120 failures=0
I (59368) multimodal_audio: Mic uplink levels: raw_ch0(avg=12411 peak=32768) raw_ch1(avg=0 peak=0) uplink(avg=12411 peak=32768) codec_channels=1 map(MIC=ch0) queued=84/15120B drops=0
I (61288) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (64408) multimodal_audio: Mic uplink sent: packets=84 bytes=15120 failures=0
I (64408) multimodal_audio: Mic uplink levels: raw_ch0(avg=3591 peak=16140) raw_ch1(avg=0 peak=0) uplink(avg=3591 peak=16140) codec_channels=1 map(MIC=ch0) queued=84/15120B drops=0
E (65608) transport_ws: Error transport_poll_write(0)
E (65608) websocket_client: esp_transport_write() returned 0, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, errno=0
W (65618) multimodal_ws: WebSocket error
I (65628) multimodal_audio: Heap before_uplink_start: free8=8256651 largest8=7995392 free_internal=141259 largest_internal=98304 largest_dma=98304 free_spiram=8115392 largest_spiram=7995392
I (65638) multimodal_audio: Heap after_uplink_start: free8=8256651 largest8=7995392 free_internal=141259 largest_internal=98304 largest_dma=98304 free_spiram=8115392 largest_spiram=7995392
I (66568) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
W (67098) multimodal_audio: Mic uplink queue overflow: 1 old packets replaced
I (68238) cp_key_input: gpio change key=PTT gpio=15 level=1
I (68238) cp_key_input: emit key=PTT type=RELEASED gpio=15 level=1 duration=14409ms clicks=0
I (68238) cp_system_event: dispatch family=KEY
I (68248) cp_system_event: key event key=PTT type=RELEASED gpio=15 level=1 duration=14409ms clicks=0
I (68698) cp_key_input: emit key=PTT type=SHORT_PRESS gpio=15 level=1 duration=0ms clicks=1
I (71578) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
W (71618) multimodal_audio: Mic uplink stop timed out with 24 queued packet(s), in_flight=1
I (71618) multimodal_audio: Heap after_uplink_stop: free8=8257903 largest8=7995392 free_internal=141259 largest_internal=98304 largest_dma=98304 free_spiram=8116644 largest_spiram=7995392
I (71628) websocket_client: Reconnect after 2000 ms
I (71628) multimodal_ws: WebSocket disconnected
W (74258) multimodal_audio: Mic uplink stop timed out with 0 queued packet(s), in_flight=1
I (74258) multimodal_audio: Heap after_uplink_stop: free8=8265403 largest8=7995392 free_internal=144935 largest_internal=100352 largest_dma=100352 free_spiram=8120468 largest_spiram=7995392
I (74268) cp_system_event: dispatch family=KEY
I (74278) cp_system_event: key event key=PTT type=SHORT_PRESS gpio=15 level=1 duration=0ms clicks=1
I (76888) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
W (77638) multimodal_audio: Mic uplink stop timed out with 0 queued packet(s), in_flight=1
I (77638) multimodal_audio: Heap after_uplink_stop: free8=8265403 largest8=7995392 free_internal=144935 largest_internal=100352 largest_dma=100352 free_spiram=8120468 largest_spiram=7995392
W (77648) multimodal_ws: WebSocket send bin failed (0/180, errno=0)
I (77658) multimodal_audio: Mic uplink sent: packets=0 bytes=0 failures=1
W (77668) multimodal_audio: Mic uplink send failed: ESP_FAIL
I (77958) esp-x509-crt-bundle: Certificate validated
I (78438) multimodal_ws: WebSocket connected
I (78448) multimodal_ws: Heap ws_connected: free_internal=141363 largest_internal=98304 largest_dma=98304 free_spiram=8120184 largest_spiram=7995392
I (82168) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (87448) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (92728) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (98008) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (103288) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (108568) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (109268) control_plane_mm_rt: Idle multimodal transport timeout, stopping client after 30000 ms
I (109268) multimodal_audio: Heap after_uplink_stop: free8=8261403 largest8=7995392 free_internal=141419 largest_internal=98304 largest_dma=98304 free_spiram=8119984 largest_spiram=7995392
I (109278) multimodal_audio: Heap after_uplink_stop: free8=8261403 largest8=7995392 free_internal=141419 largest_internal=98304 largest_dma=98304 free_spiram=8119984 largest_spiram=7995392
I (110048) multimodal_ws: Heap after_ws_stop_cleanup: free_internal=161211 largest_internal=108544 largest_dma=108544 free_spiram=8142780 largest_spiram=7995392
I (113848) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
+126
View File
@@ -0,0 +1,126 @@
I (1933979) esp-x509-crt-bundle: Certificate validated
E (1934309) transport_ws: Sec-WebSocket-Accept not found
E (1934319) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1934319) multimodal_ws: WebSocket error
W (1934329) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1934339) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1934349) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1934369) websocket_client: Reconnect after 2000 ms
I (1934369) multimodal_ws: WebSocket disconnected
W (1934379) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1934389) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1934399) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1936609) esp-x509-crt-bundle: Certificate validated
I (1936779) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
E (1936949) transport_ws: Sec-WebSocket-Accept not found
E (1936949) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1936959) multimodal_ws: WebSocket error
W (1936959) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1936969) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1936989) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1936999) websocket_client: Reconnect after 2000 ms
I (1937009) multimodal_ws: WebSocket disconnected
W (1937009) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1937019) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1937039) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1939219) esp-x509-crt-bundle: Certificate validated
E (1939509) transport_ws: Sec-WebSocket-Accept not found
E (1939509) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1939509) multimodal_ws: WebSocket error
W (1939519) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1939529) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1939539) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1939559) websocket_client: Reconnect after 2000 ms
I (1939559) multimodal_ws: WebSocket disconnected
W (1939569) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1939579) multimodal_audio: Heap after_uplink_stop: free8=8273975 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128868 largest_spiram=7995392
I (1939599) multimodal_audio: Heap after_uplink_stop: free8=8272195 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8129208 largest_spiram=7995392
I (1941799) esp-x509-crt-bundle: Certificate validated
I (1942059) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
E (1942129) transport_ws: Sec-WebSocket-Accept not found
E (1942129) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1942139) multimodal_ws: WebSocket error
W (1942139) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1942149) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1942169) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1942179) websocket_client: Reconnect after 2000 ms
I (1942189) multimodal_ws: WebSocket disconnected
W (1942189) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1942199) multimodal_audio: Heap after_uplink_stop: free8=8273975 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128868 largest_spiram=7995392
I (1942219) multimodal_audio: Heap after_uplink_stop: free8=8273975 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128868 largest_spiram=7995392
I (1944429) esp-x509-crt-bundle: Certificate validated
E (1944729) transport_ws: Sec-WebSocket-Accept not found
E (1944729) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1944739) multimodal_ws: WebSocket error
W (1944739) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1944749) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1944759) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1944779) websocket_client: Reconnect after 2000 ms
I (1944789) multimodal_ws: WebSocket disconnected
W (1944789) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1944799) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1944819) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1947129) esp-x509-crt-bundle: Certificate validated
I (1947339) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
E (1947589) transport_ws: Sec-WebSocket-Accept not found
E (1947589) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1947599) multimodal_ws: WebSocket error
W (1947599) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1947609) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1947629) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1947649) websocket_client: Reconnect after 2000 ms
I (1947649) multimodal_ws: WebSocket disconnected
W (1947649) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1947659) multimodal_audio: Heap after_uplink_stop: free8=8273975 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128868 largest_spiram=7995392
I (1947679) multimodal_audio: Heap after_uplink_stop: free8=8273975 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128868 largest_spiram=7995392
I (1950029) esp-x509-crt-bundle: Certificate validated
E (1950549) transport_ws: Sec-WebSocket-Accept not found
E (1950549) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1950559) multimodal_ws: WebSocket error
W (1950559) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1950569) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1950589) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1950609) websocket_client: Reconnect after 2000 ms
I (1950609) multimodal_ws: WebSocket disconnected
W (1950619) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1950619) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1950639) multimodal_audio: Heap after_uplink_stop: free8=8273975 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128868 largest_spiram=7995392
I (1952619) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (1952979) esp-x509-crt-bundle: Certificate validated
E (1953469) transport_ws: Sec-WebSocket-Accept not found
E (1953469) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1953469) multimodal_ws: WebSocket error
W (1953479) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1953489) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1953499) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1953519) websocket_client: Reconnect after 2000 ms
I (1953519) multimodal_ws: WebSocket disconnected
W (1953529) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1953539) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1953549) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1955929) esp-x509-crt-bundle: Certificate validated
E (1956379) transport_ws: Sec-WebSocket-Accept not found
E (1956379) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1956389) multimodal_ws: WebSocket error
W (1956389) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1956399) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1956419) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1956439) websocket_client: Reconnect after 2000 ms
I (1956439) multimodal_ws: WebSocket disconnected
W (1956439) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1956449) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1956469) multimodal_audio: Heap after_uplink_stop: free8=8273971 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128864 largest_spiram=7995392
I (1957899) lcd_direct: LCD asset sequence drawn asset=jichubiaoqing frames=3
I (1958769) esp-x509-crt-bundle: Certificate validated
E (1959239) transport_ws: Sec-WebSocket-Accept not found
E (1959239) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_OK, tls_error_code=0, tls_flags=0, esp_ws_handshake_status_code=401, errno=119
W (1959249) multimodal_ws: WebSocket error
W (1959249) cp_mm_session: Transport unavailable: reason=ws_error caller=transport_event_bridge
I (1959259) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1959279) multimodal_audio: Heap after_uplink_stop: free8=8270347 largest8=7995392 free_internal=141431 largest_internal=98304 largest_dma=98304 free_spiram=8128916 largest_spiram=7995392
I (1959289) websocket_client: Reconnect after 2000 ms
I (1959299) multimodal_ws: WebSocket disconnected
W (1959299) cp_mm_session: Transport unavailable: reason=ws_disconnected caller=transport_event_bridge
I (1959309) multimodal_audio: Heap after_uplink_stop: free8=8273979 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128872 largest_spiram=7995392
I (1959329) multimodal_audio: Heap after_uplink_stop: free8=8273979 largest8=7995392 free_internal=145107 largest_internal=98304 largest_dma=98304 free_spiram=8128872 largest_spiram=7995392
I (1961529) esp-x509-crt-bundle: Certificate validated
-7
View File
@@ -88,13 +88,6 @@ menu "Example Configuration"
Core used for local device workers such as LCD rendering,
key input, system events, direction console, and diagnostics.
config TQ_DIRECTION_CONSOLE_ENABLE
bool "Enable UART direction debug console"
default n
help
Start the serial direction console for development or factory
diagnostics. Normal product runtime should leave this disabled.
config TQ_TASK_MONITOR_ENABLE
bool "Enable periodic FreeRTOS task monitor log"
default y
+539
View File
@@ -0,0 +1,539 @@
# TQ_catN 远程日志系统设计方案
**版本**:v1.0(待审核)
**日期**:2026-06-27
**范围**:全设备日志收集、本地缓冲、网络上传
---
## 一、设计目标
| 目标 | 说明 |
|------|------|
| **可靠性** | 网络抖动或断开时,日志不丢失,具备本地持久化和重传能力 |
| **低开销** | 对实时语音交互(PTT/WS/audio)零干扰,CPU 占用 < 5%,内存占用 < 256KB |
| **分层合规** | 严格遵循现有四层单向依赖架构(platform → domain → control_plane → app_composition) |
| **可配置** | 支持按模块/级别动态开关,支持生产环境静默/调试环境全量 |
| **安全性** | 日志上传使用 HTTPS + device JWT 认证,敏感字段自动脱敏 |
---
## 二、架构设计
### 2.1 与现有分层架构的集成
```
app_composition
└── 初始化 remote_log_runtime(在 boot sequence 中启动)
control_plane
└── remote_log_policy.c # 策略:上传触发条件、级别控制、模块过滤
└── remote_log_config.h # 模块/级别开关配置
domain
└── remote_log_format.c # 日志格式化:JSON 序列化、时间戳、device_id 注入
└── remote_log_compress.c # 可选:压缩(miniz/lz4)
platform
└── remote_log_buffer.c # 双缓冲:内存 ring buffer + 文件队列(LittleFS)
└── remote_log_transport.c # 网络传输:复用 platform_https_post_json
└── remote_log_hook.c # ESP-IDF log hook:esp_log_set_vprintf 重定向
```
**依赖方向**:
- platform 层提供底层能力(buffer、transport、hook)
- domain 层提供格式化/压缩(无状态转换)
- control_plane 层提供策略(何时上传、上传什么)
- app_composition 层负责初始化编排
**禁止**:control_plane / domain 直接调用 esp_log_set_vprintf 或 esp_http_client API,必须通过 platform facade。
---
### 2.2 数据流
```
[App Code: ESP_LOGI/ESP_LOGE]
│
▼
[platform/remote_log_hook.c] ──→ 拦截 ESP-IDF 日志输出
│ (原始串口输出保留,不影响调试)
▼
[platform/remote_log_buffer.c]
├── 内存环形缓冲区(热路径,ISR/任务安全)
│ └── 容量:64KB(PSRAM 优先,内部 RAM 回退)
│
└── 文件队列(冷路径,网络断开时持久化)
└── 容量:512KB(LittleFS gifpack 分区空闲空间)
└── 最大文件数:32 个,每个 16KB
└── 命名:log_batch_001.jsonl ~ log_batch_032.jsonl
│
▼(满足上传条件时)
[domain/remote_log_format.c]
└── 封装为 JSON batch:
{
"device_id": "66c...",
"batch_id": "uuid",
"timestamp_ms": 1782552243000,
"logs": [
{"ts": 1782552243001, "level": "E", "tag": "cp_mm_session", "msg": "..."},
...
]
}
│
▼
[domain/remote_log_compress.c] ──→ 可选:miniz 压缩(压缩率 ~60%)
│
▼
[platform/remote_log_transport.c]
└── 复用 platform_https_post_json() → 服务端
POST https://gz.api.talkingq.com/log/batch
Content-Type: application/json
Authorization: Bearer <device_token>
```
---
## 三、核心模块设计
### 3.1 日志拦截层(platform/remote_log_hook.c)
**方案**:使用 ESP-IDF 的 `esp_log_set_vprintf()` 钩子,在保留原有串口输出的同时,将日志复制到远程日志缓冲区。
```c
// 接口设计
void platform_remote_log_hook_install(void);
void platform_remote_log_hook_uninstall(void);
// 内部实现
static int remote_log_vprintf(const char *fmt, va_list args)
{
// 1. 调用原始 vprintf,保持串口输出
int ret = s_original_vprintf(fmt, args);
// 2. 格式化为结构化记录,写入 ring buffer
// 使用非阻塞写入,buffer 满时丢弃最旧记录
platform_remote_log_buffer_write(fmt, args);
return ret;
}
```
**关键约束**:
- `remote_log_vprintf` 可能在 ISR 上下文被调用(如 `ESP_EARLY_LOG`),因此 `platform_remote_log_buffer_write` 内部必须:
- 使用 `xRingbufferSendFromISR` 或自旋锁保护的环形缓冲区
- 禁止任何可能阻塞的操作(malloc、semaphore take 等)
- 如果检测到 ISR 上下文,只做**原子计数 + 标记**,由后台任务在退出 ISR 后批量拷贝
**日志级别过滤**:在 hook 层不做级别过滤(避免重复判断),过滤由 control_plane 策略层通过 Kconfig 宏控制编译期开关。
---
### 3.2 双缓冲层(platform/remote_log_buffer.c)
#### 3.2.1 内存环形缓冲区(热路径)
```c
#define REMOTE_LOG_RING_BUFFER_SIZE (64 * 1024) // 64KB
#define REMOTE_LOG_RING_BLOCK_SIZE 256 // 每条日志最大 256 字节(超长截断)
static uint8_t s_ring_buffer[REMOTE_LOG_RING_BUFFER_SIZE]; // PSRAM 优先分配
static volatile size_t s_ring_write_idx;
static volatile size_t s_ring_read_idx;
```
**设计**:
- 使用简单的无锁环形缓冲区(单生产者:hook ISR/任务;单消费者:后台 flush 任务)
- 写入:原子递增 write_idx,memcpy 数据
- 读取:后台任务定期(100ms)将数据从 ring buffer 搬到文件队列
- 满策略:丢弃最旧记录(覆盖 read_idx 指向的数据),避免阻塞生产者
#### 3.2.2 文件队列(冷路径 / 持久化)
```c
#define REMOTE_LOG_FILE_MAX_BATCHES 32
#define REMOTE_LOG_FILE_BATCH_SIZE (16 * 1024) // 16KB / 文件
```
**设计**:
- 使用 LittleFS 的 `gifpack` 分区空闲空间(当前分区 8MB,OTA 后仍有富余)
- 文件名:`/littlefs/log/batch_001.jsonl` ~ `/littlefs/log/batch_032.jsonl`
- 写入格式:JSON Lines(每行一条 JSON,便于追加和断点续传)
- 循环覆盖:当 32 个文件都写满后,覆盖最早的文件
**掉电保护**:
- 每个 batch 文件写完后调用 `fsync`,确保数据落盘
- 启动时扫描 `/littlefs/log/` 目录,恢复未上传的 batch
---
### 3.3 格式化层(domain/remote_log_format.c)
**输入**:原始 ESP-IDF 日志字符串(如 `I (1934379) cp_mm_session: Transport unavailable`)
**输出**:结构化 JSON batch
```c
typedef struct {
int64_t timestamp_ms; // SNTP 同步后的毫秒时间戳
char level; // 'V'/'D'/'I'/'W'/'E'
char tag[32]; // 模块 TAG
char message[192]; // 日志内容(超长截断)
} remote_log_entry_t;
```
**批处理策略**:
- 每批最多 50 条日志 或 每批最多 8KB 数据(先到为准)
- 超时机制:即使不满 50 条,最多等待 5 秒后强制上传
- 压缩:使用 miniz(zlib 兼容,纯 C,~8KB 代码)压缩 JSON batch,减少流量 50-70%
**脱敏规则**:
- 自动匹配并替换:`password=xxx`、`Bearer xxx`、`token=xxx`
- 使用正则或简单字符串扫描:`s/([Pp]assword|[Tt]oken|[Bb]earer)[=:]\S+/\1=***REDACTED***/g`
---
### 3.4 传输层(platform/remote_log_transport.c)
**复用现有能力**:
- HTTP 客户端:复用 `platform_https_post_json()`(已有 TLS、DNS、超时处理)
- 认证:复用 device JWT token(已有 `control_plane_multimodal_auth_service`)
**上传策略**:
```c
typedef struct {
const char *endpoint_url; // 如 "https://gz.api.talkingq.com/log/batch"
uint32_t batch_max_count; // 50
uint32_t batch_max_size_bytes; // 8192
uint32_t flush_interval_ms; // 5000
uint32_t upload_timeout_ms; // 10000
uint32_t max_retry_count; // 3
uint32_t retry_backoff_ms; // 2000
} remote_log_transport_config_t;
```
**上传流程**:
1. 检查 Wi-Fi 是否连接(`control_plane_is_sta_connected()`)
2. 检查 device token 是否有效(复用 auth service)
3. 读取一批日志(优先内存 buffer,其次文件队列)
4. 封装 JSON batch + device_id + batch_id
5. 调用 `platform_https_post_json()` 上传
6. 上传成功:删除已上传的文件 batch / 释放内存 buffer
7. 上传失败(非 401):按指数退避重试(2s / 4s / 8s),失败后回写文件队列
8. 上传失败(401):触发 device token 刷新,刷新成功后重试
**网络降级**:
- WS 连接活跃时(用户正在对话):暂停日志上传,避免干扰实时音频/WS 流量
- Wi-Fi 断开时:完全停止上传,仅写入文件队列
- 内存 buffer 满且文件队列满时:丢弃最旧记录(采样策略:优先保留 Error/Warning)
---
### 3.5 策略层(control_plane/remote_log_policy.c)
**职责**:控制"上传什么、何时上传、上传多少"
```c
typedef enum {
REMOTE_LOG_LEVEL_NONE = 0,
REMOTE_LOG_LEVEL_ERROR = 1, // 仅 Error
REMOTE_LOG_LEVEL_WARN = 2, // Error + Warning
REMOTE_LOG_LEVEL_INFO = 3, // Error + Warning + Info
REMOTE_LOG_LEVEL_DEBUG = 4, // 全部
} remote_log_level_t;
// 模块级别开关(与现有 TAG 对齐)
typedef struct {
bool cp_mm_session; // control_plane multimodal session
bool cp_mm_runtime; // control_plane multimodal runtime
bool cp_workers; // control_plane runtime workers
bool cp_connectivity; // control_plane connectivity
bool transport_ws; // platform transport ws
bool audio_pipeline; // platform audio pipeline
bool direction; // domain direction
bool fortune; // domain fortune
// ... 其他模块
} remote_log_module_mask_t;
```
**策略规则**:
- 生产环境默认:`LEVEL_ERROR`,仅关键模块开启
- 调试环境默认:`LEVEL_DEBUG`,全部模块开启
- 动态调整:通过 LAN HTTP 配置页面(复用 `domain_user_config_server`)下发策略变更
---
## 四、Kconfig 配置项
建议在 `main/Kconfig.projbuild` 中新增菜单:
```kconfig
menu "Remote Logging"
config TQ_REMOTE_LOG_ENABLED
bool "Enable remote logging"
default n
help
Enable log collection and remote upload. Increases flash
usage by ~24KB and RAM usage by ~80KB.
config TQ_REMOTE_LOG_LEVEL
int "Default log level for remote upload"
default 1
range 0 4
help
0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
config TQ_REMOTE_LOG_RING_BUFFER_SIZE
int "Memory ring buffer size (KB)"
default 64
range 8 256
config TQ_REMOTE_LOG_FILE_BATCHES
int "Max file batches on LittleFS"
default 32
range 8 64
config TQ_REMOTE_LOG_BATCH_MAX_COUNT
int "Max log entries per batch"
default 50
range 10 200
config TQ_REMOTE_LOG_FLUSH_INTERVAL_MS
int "Flush interval (ms)"
default 5000
range 1000 30000
config TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS
int "Upload HTTP timeout (ms)"
default 10000
range 3000 30000
config TQ_REMOTE_LOG_COMPRESS
bool "Enable miniz compression"
default y
help
Compress log batches before upload. Reduces data usage
by ~60% at the cost of ~8KB flash and ~2KB RAM.
config TQ_REMOTE_LOG_ENDPOINT
string "Log upload endpoint URL"
default "https://gz.api.talkingq.com/log/batch"
help
HTTPS endpoint for log batch upload.
endmenu
```
---
## 五、内存和性能预算
### 5.1 Flash 占用
| 组件 | 代码+数据 | 说明 |
|------|----------|------|
| remote_log_hook.c | ~4KB | 钩子 + ring buffer 管理 |
| remote_log_buffer.c | ~6KB | 双缓冲 + LittleFS 文件操作 |
| remote_log_transport.c | ~4KB | HTTP 上传 + 重试逻辑 |
| remote_log_format.c | ~6KB | JSON 序列化 + 脱敏 |
| remote_log_compress.c | ~8KB | miniz 压缩库 |
| remote_log_policy.c | ~2KB | 策略判断 + 模块过滤 |
| **合计** | **~30KB** | 可配置关闭压缩节省 8KB |
### 5.2 RAM 占用(运行时)
| 组件 | 内存 | 位置 |
|------|------|------|
| Ring buffer | 64KB | PSRAM(配置为 PSRAM 优先) |
| Upload working buffer | 8KB | PSRAM |
| HTTP client(复用现有) | ~4KB | 内部 RAM(复用已有实例) |
| 格式化/压缩临时区 | ~4KB | PSRAM |
| **合计** | **~80KB** | 其中内部 RAM < 10KB |
### 5.3 CPU 占用
| 场景 | CPU 占用 |
|------|----------|
| 日志写入(hook) | < 1%(纯内存拷贝) |
| 定时 flush(100ms) | < 2%(文件写入) |
| 上传(每 5s 一次) | < 5%(JSON 格式化 + HTTP POST) |
| 用户对话期间(WS 活跃) | 0%(暂停上传) |
---
## 六、启动时序
在 `app_composition_default_runtime.c` 的 boot sequence 中,在 `control_plane_init()` 之后、`wifi_bootstrap` 之前插入:
```c
// 步骤 7.5:初始化远程日志系统
esp_err_t log_err = control_plane_init_remote_logging();
if (log_err != ESP_OK) {
ESP_LOGW(TAG, "Remote logging init failed: %s", esp_err_to_name(log_err));
// 非致命错误,继续启动
}
```
**理由**:
- 在 `control_plane_init()` 之后:确保 device auth 已加载(用于上传认证)
- 在 `wifi_bootstrap` 之前:确保 Wi-Fi 连接建立后日志系统已就绪,可以立即开始收集
---
## 七、服务端接口契约
### 7.1 上传接口
```http
POST /log/batch
Content-Type: application/json
Authorization: Bearer <device_jwt_token>
{
"device_id": "66c7f8a2e4b0d1e2f3a4b5c6",
"batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp_ms": 1782552243000,
"firmware_version": "1.2.3",
"logs": [
{
"ts": 1782552243001,
"level": "E",
"tag": "transport_ws",
"msg": "Sec-WebSocket-Accept not found"
},
{
"ts": 1782552243010,
"level": "W",
"tag": "cp_mm_session",
"msg": "Transport unavailable: reason=ws_error"
}
]
}
```
### 7.2 服务端响应
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"message": "OK",
"data": {
"accepted": 2,
"dropped": 0
}
}
```
### 7.3 错误码
| HTTP 状态码 | 含义 | 客户端行为 |
|------------|------|-----------|
| 200 | 成功 | 删除已上传的 batch |
| 400 | 格式错误 | 丢弃当前 batch(避免无限重试) |
| 401 | 认证失败 | 刷新 device JWT 后重试 |
| 413 | Payload 过大 | 减小 batch size 后重试 |
| 429 | 频率限制 | 指数退避后重试 |
| 500+ | 服务端错误 | 指数退避后重试 |
---
## 八、安全与隐私
### 8.1 认证
- 复用现有 device JWT 认证体系,不引入新密钥
- token 过期时自动刷新(复用 `control_plane_multimodal_auth_service`)
### 8.2 传输
- 强制 HTTPS,复用现有 TLS 证书校验(`esp-x509-crt-bundle`)
- 禁止 HTTP 明文传输
### 8.3 脱敏
- 自动脱敏:password、token、Bearer、secret、api_key 等前缀后的值
- 用户配置:支持通过 LAN HTTP 配置页面自定义脱敏关键词
### 8.4 数据保留
- 本地:最多保留 32 个 batch 文件(~512KB),循环覆盖
- 服务端:建议保留 30 天,过期自动清理
---
## 九、故障排查与调试
### 9.1 本地诊断命令
通过现有的 factory CLI 或 LAN HTTP 配置页面,增加以下诊断接口:
```
GET /api/diag/log/status
→ {
"enabled": true,
"level": 2,
"ring_buffer_used": 32768,
"ring_buffer_total": 65536,
"file_batches_pending": 5,
"last_upload_status": "success",
"last_upload_time": "2026-06-27T10:30:00Z"
}
```
### 9.2 本地日志导出
```
GET /api/diag/log/download?batch=latest
→ 返回最近一个 batch 文件的内容(纯文本)
```
---
## 十、实施建议
### Phase 1(最小可用)
1. `platform/remote_log_hook.c` — 拦截 ESP_LOG 到 ring buffer
2. `platform/remote_log_buffer.c` — 内存缓冲 + 文件持久化
3. `platform/remote_log_transport.c` — 复用 HTTPS 上传
4. `control_plane/remote_log_policy.c` — 级别过滤 + 模块开关
5. Kconfig 配置项 + boot sequence 初始化
### Phase 2(增强)
6. `domain/remote_log_format.c` — JSON 格式化 + 脱敏
7. `domain/remote_log_compress.c` — miniz 压缩
8. LAN HTTP 配置页面集成(动态调整级别)
9. 诊断接口(`/api/diag/log/status`)
### Phase 3(优化)
10. 服务端实时告警(Error 级别日志触发服务端告警)
11. 日志采样(高频 Debug 日志按概率采样)
12. 日志关联(与 device session_id 关联,便于追踪单设备全链路)
---
## 十一、风险评估
| 风险 | 缓解措施 |
|------|----------|
| 日志写入拖慢 ISR | hook 层使用纯内存拷贝 + 无锁 ring buffer,禁止 malloc |
| 文件写入磨损 Flash | LittleFS 磨损均衡,日志 batch 文件大小控制在 16KB |
| 上传流量过大 | 压缩(-60%)+ 级别过滤(仅 ERROR/WARN)+ 批量上传(5s 间隔) |
| 上传干扰 WS 音频 | WS 活跃时暂停上传,仅写入本地 buffer |
| 敏感信息泄露 | 自动脱敏 + HTTPS + 服务端 ACL |
| 内存不足 | PSRAM 优先分配,内部 RAM 占用 < 10KB |
---
## 十二、总结
本方案基于现有四层架构,以 **"零侵入、低开销、高可靠"** 为核心原则:
- **platform 层**:提供日志拦截、双缓冲、网络传输的底层能力
- **domain 层**:提供 JSON 格式化、压缩、脱敏的业务处理
- **control_plane 层**:提供级别控制、模块过滤、上传策略的策略决策
- **app_composition 层**:负责启动编排
关键设计决策:
1. 复用 `platform_https_post_json()`,不引入新 HTTP 客户端
2. 复用 device JWT,不引入新认证体系
3. 使用 `esp_log_set_vprintf()` 拦截,不修改现有 ESP_LOGx 调用
4. 使用 LittleFS 持久化,不引入新文件系统
5. 用户对话期间暂停上传,保证实时语音不受干扰
-1
View File
@@ -14,7 +14,6 @@ CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY=y
CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
# CONFIG_TQ_TASK_MONITOR_ENABLE is not set
# CONFIG_TQ_DIRECTION_CONSOLE_ENABLE is not set
# CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN is not set
CONFIG_TQ_BOOT_SOUND_TIMEOUT_MS=3000
CONFIG_TQ_BOOT_SOUND_FILE="boot.opus"
+1 -1
View File
@@ -75,7 +75,7 @@ fi
if rg -n "FACTORY_CLI_PARK_DELAY_MS|Cooperative exit: park" components/control_plane/src/runtime/control_plane_factory_cli.c >/dev/null 2>&1; then
fail "factory CLI worker must cooperatively exit and clear its handle instead of parking for force-delete"
fi
if rg -n "if \\(s_(model_cmd|ws_wait|direction_console|lcd_render_loop|wifi_state|ptt|slot_switch)_task == NULL\\)" components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1; then
if rg -n "if \\(s_(model_cmd|ws_wait|lcd_render_loop|wifi_state|ptt|slot_switch)_task == NULL\\)" components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1; then
fail "control-plane runtime worker task handles must be read through the runtime lock helpers"
fi
if ! rg -n "ws_start_async_stop" components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c >/dev/null 2>&1; then
+756
View File
@@ -0,0 +1,756 @@
# TQ_catN 工程深度代码审查报告(第二版)
**审查范围**:control_plane / domain / platform / app_composition 全部源文件
**审查维度**:内存安全、线程安全、状态机、资源管理、FreeRTOS 集成、架构合规
---
## 一、严重(Critical)— 可能导致崩溃或永久功能丧失
### C1. Watchdog 任务自删除导致多模态客户端永久失效
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_runtime.c`
- **函数**:`multimodal_watchdog_task()` / `stop_watchdog_task()`
- **行号**:356-373, 375-435
```c
// watchdog 任务检测到空闲超时后:
control_plane_multimodal_stop(); // 内部调用 stop_watchdog_task()
// stop_watchdog_task:
if (!wait_task_exit(..., 1200)) {
TaskHandle_t task = get_multimodal_task_handle(&s_multimodal_watchdog_task);
if (task != NULL) {
vTaskDelete(task); // 删除自己,控制流不再返回
}
set_multimodal_task_handle(&s_multimodal_watchdog_task, NULL); // 永远执行不到
}
```
**问题**:watchdog 任务自己调用 `vTaskDelete(task)`(task 即自身),后续清理代码(将句柄置 NULL、重置 stop flag)永远执行不到。`s_multimodal_watchdog_task` 保持为悬空句柄,`s_multimodal_watchdog_stop_requested` 永久为 true。后续 `control_plane_start_multimodal_client()` 因句柄非 NULL 而拒绝重新创建 watchdog,**多模态客户端再也无法恢复**。
**修复**:watchdog 任务不应自删除。应通过 `xTaskNotifyGive` 通知外部任务执行删除,或使用 `vTaskDelete(NULL)` 后将清理工作移交给调用者。
---
### C2. Key Input ISR 对已释放队列的 Use-After-Free
- **文件**:`components/control_plane/src/runtime/control_plane_key_input.c`
- **函数**:`control_plane_key_gpio_isr_handler()` / `control_plane_stop_key_input_task()`
- **行号**:160-178, 493-515
```c
// ISR 中:
QueueHandle_t queue = s_gpio_irq_queue;
if (queue == NULL) { return; }
(void)xQueueSendFromISR(queue, &event, &higher_priority_task_woken);
// 停止流程中:
vQueueDelete(s_gpio_irq_queue); // queue 被释放
s_gpio_irq_queue = NULL;
```
**问题**:`gpio_isr_handler_remove()` 与 `vQueueDelete()` 之间无法保证中断不再触发。ISR 先将 `s_gpio_irq_queue` 读到局部变量 `queue`,随后主任务释放并删除队列,ISR 仍可能向已释放的 Queue 发送事件,导致内存损坏或系统崩溃。
**修复**:删除 queue 前应先 `gpio_intr_disable()` 所有引脚并确认 ISR 不再执行,或使用引用计数保护 queue 生命周期。
---
### C3. 音频下游队列释放后使用(UAF)
- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c`
- **函数**:`multimodal_audio_handle_downstream_opus()`
- **行号**:2056-2088
```c
if (!lock_downstream_playback_queue(pdMS_TO_TICKS(20))) { ... }
QueueHandle_t queue = ctx->downstream_playback_queue;
unlock_downstream_playback_queue();
// 解锁后在另一个任务可能销毁 queue 的情况下继续使用
if (xQueueSend(queue, &packet, pdMS_TO_TICKS(20)) == pdTRUE) { ... }
```
**问题**:解锁后、`xQueueSend` 前,若 `stop_downstream_playback_task()` 删除了该 queue,`xQueueSend` 操作已释放内存,导致崩溃。
**修复**:`xQueueSend` 必须在持有 `s_downstream_playback_queue_mutex` 的临界区内完成。
---
### C4. 音频任务强制删除时 TaskHandle 失效(UAF)
- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c`
- **函数**:`multimodal_audio_deinit()`
- **行号**:929-942
```c
audio_snapshot_uplink_tasks(&s_audio, &capture_task, &send_task);
if (capture_task != NULL) {
delete_audio_task_with_caps(capture_task); // 可能已自删
audio_set_uplink_capture_task(&s_audio, NULL);
}
```
**问题**:`uplink_capture_task` / `uplink_send_task` 在退出前会自删。获取句柄后任务可能已完成并自删;随后 `delete_audio_task_with_caps(capture_task)` 对已释放/不存在的 TCB 操作,在 FreeRTOS 中极易触发 `abort()` 或链表损坏。
**修复**:使用任务通知 + 超时等待任务自行退出,禁止在外部持有句柄时强制删除。
---
### C5. 校准服务未持有旋转互斥锁 — 步进电机资源冲突
- **文件**:`components/domain/src/calibration/domain_calibration_service.c`
- **函数**:`domain_calibrate_with_timeout`
- **行号**:113-231
**问题**:校准全程持有 `s_state_mutex`,但**从未**获取 `s_rotation_mutex`。`domain_direction_service.c` 中的 `domain_rotate_to_angle_with_timeout` 则先获取 `s_rotation_mutex` 再获取 `s_state_mutex`。当校准时,其他任务可成功获取 `s_rotation_mutex`,然后在 `s_state_mutex` 上阻塞;一旦校准释放 `s_state_mutex`,该任务立即进入步进电机调用,与校准的下一步电机操作**并发冲突**,导致机械位置失步或硬件损坏。
**修复**:校准函数也应按相同顺序获取 `s_rotation_mutex` 和 `s_state_mutex`。
---
### C6. 角度归一化函数对无穷值死循环
- **文件**:`components/domain/src/direction/domain_angle_math.c`
- **函数**:`domain_angle_normalize`
- **行号**:9-18
```c
while (angle > 180.0f) {
angle -= 360.0f; // 对 +inf 永远为 true,死循环
}
while (angle <= -180.0f) {
angle += 360.0f; // 对 -inf 永远为 true,死循环
}
```
**问题**:若传入 `INFINITY`、`-INFINITY` 或 `NAN`,`while` 循环永不退出,导致看门狗复位或任务挂死。
**修复**:增加前置检查 `if (!isfinite(angle)) return 0.0f;` 或使用 `fmodf` 替代循环。
---
### C7. WS 发送锁释放后立即删除
- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c`
- **函数**:`multimodal_ws_stop_sync_until()`
- **行号**:417-423
```c
if (send_lock_held && s_ws_send_lock != NULL) {
xSemaphoreGive(s_ws_send_lock);
}
if (send_lock_held && s_ws_send_lock != NULL) {
vSemaphoreDelete(s_ws_send_lock); // 另一任务可能刚被唤醒并持有该锁
s_ws_send_lock = NULL;
}
```
**问题**:`xSemaphoreGive` 后、`vSemaphoreDelete` 前,其他正在等待 `s_ws_send_lock` 的任务可能获得该信号量;随后信号量被删除,导致等待任务使用已释放对象。
**修复**:在仍持有锁的状态下直接删除(或改用静态信号量/互斥量避免动态删除)。
---
### C8. 下游空闲静音 Timer 回调竞态删除
- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c`
- **函数**:`stop_downstream_idle_mute_task()` / `downstream_idle_mute_timer_cb()`
- **行号**:1479-1506, 1301-1316
```c
esp_timer_stop(timer);
esp_timer_delete(timer); // 若 timer 回调正在另一核执行,删除不安全
```
**问题**:`esp_timer_delete()` **不等待**正在执行的回调结束。若回调正在运行,`timer` 及其 `arg` 被释放后,回调中继续访问 `ctx` 导致 UAF。
**修复**:删除前设置原子标志让回调自行短路返回,并确保无活跃回调。
---
### C9. `multimodal_ws_is_connected()` 对已销毁 client 句柄操作
- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws.c`
- **函数**:`multimodal_ws_is_connected()`
- **行号**:458-470
```c
ws_client_state_snapshot(&client, &connected, &stop_requested);
...
return esp_websocket_client_is_connected(client); // client 可能已被销毁
```
**问题**:退出临界区后调用 `esp_websocket_client_is_connected(client)`,在此期间 `multimodal_ws_stop_sync_until()` 可能已 `destroy(client)` 并将句柄置 NULL。对无效/已释放句柄调用 esp_websocket API 属于 UAF。
**修复**:将 `is_connected` 判断完全放在 `s_client_state_lock` 临界区内,或增加引用计数。
---
### C10. LAN HTTP Server 强制删除任务时 IO Buffer UAF
- **文件**:`components/platform/src/runtime/platform_lan_http_server.c`
- **函数**:`platform_lan_http_server_stop()` / `server_task()`
- **行号**:822-839, 594-727
**问题**:强制删除 `server_task` 后,立即调用 `server_release_io_buffers()` 释放 buffer。若被删任务恰好在 `handle_client()` 执行路径中(已读取到局部指针但尚未返回),将继续访问已释放内存。
**修复**:在停止任务前通过 `shutdown(s_client_fd, SHUT_RDWR)` 中断阻塞的 IO,等待任务自然退出;仅在超时后才强制删除,且删除后延迟释放 buffer。
---
## 二、高(High)— 可能导致竞态条件、数据损坏或功能异常
### H1. `s_started` 与 `s_transport_available` 不一致导致永久连接失败
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c`
- **行号**:1533-1576
- **问题**:`control_plane_multimodal_start()` 先设置 `s_started = true`,再启动 transport。如果 transport 启动后立即断连,`s_started` 仍为 true,下次调用 `start()` 时幂等检查会跳过重连。
- **修复**:`s_started` 应在 `TRANSPORT_CONNECTED` 事件回调时才设置。
---
### H2. Session Service 互斥锁创建非线程安全
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c`
- **函数**:`ensure_state_mutex()`
- **行号**:283-298
```c
if (s_state_mutex != NULL) { return true; }
s_state_mutex = xSemaphoreCreateMutex(); // 非原子
```
**问题**:若两个线程首次并发进入,可能创建两个 mutex,第二个覆盖第一个导致泄漏;若已有线程获取了第一个 mutex,后续代码使用第二个 mutex,导致对共享状态无保护。
**修复**:使用 `portENTER_CRITICAL` 保护创建路径,或采用静态互斥量。
---
### H3. Device Auth 状态加载失败后被忽略继续初始化
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_device_auth.c`
- **函数**:`control_plane_multimodal_device_auth_init()`
- **行号**:1856-1888
```c
esp_err_t err = load_state_from_store_locked();
if (err != ESP_OK) {
ESP_LOGW(TAG, "Auth store state unavailable, continue with defaults");
}
// ... 继续初始化,标记 s_auth.initialized = true
```
**问题**:当 `load_state_from_store_locked()` 返回 `ESP_ERR_INVALID_SIZE`(持久化数据损坏)时,仅打印 warning 即继续。此时 `s_auth.jwt` 等状态可能包含损坏的残留值,后续 `jwt_still_valid_locked()` 可能基于损坏的过期时间做出错误判断。
**修复**:数据损坏时应清除状态为默认值,而非保留残留值。
---
### H4. PTT 状态机基于过时 Snapshot 的 TOCTOU 竞态
- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c`
- **函数**:`control_plane_handle_ptt_teardown()`
- **行号**:1193-1243
```c
ptt_runtime_t ptt = control_plane_ptt_snapshot();
if (ptt.state == PTT_STATE_TEARDOWN_LOCAL) {
control_plane_multimodal_stop_uplink();
control_plane_ptt_set_state(PTT_STATE_TEARDOWN_REMOTE, "uplink_stopped");
ptt = control_plane_ptt_snapshot();
}
```
**问题**:第一次 snapshot 后判断为 `TEARDOWN_LOCAL`,随即停止 uplink 并推进到 `TEARDOWN_REMOTE`。但若在此期间用户已释放 PTT,`control_plane_set_ptt_active(false)` 已将状态重置为 `IDLE`。随后本函数仍强行将其推进到 `TEARDOWN_REMOTE` 并发送 `StopSpeech`,导致客户端与服务端状态不一致。
**修复**:在状态推进前检查 `generation` 是否仍有效,或改用临界区保护整个 teardown 流程。
---
### H5. PTT foreground_attempts 计数与 Snapshot 不一致
- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c`
- **函数**:`control_plane_drive_ptt_state_machine()`
- **行号**:1288-1303
```c
bool started = control_plane_try_start_multimodal_from_state_machine(...);
portENTER_CRITICAL(&s_runtime_lock);
s_ptt_runtime.foreground_attempts++;
portEXIT_CRITICAL(&s_runtime_lock);
if (!started &&
ptt.foreground_attempts + 1U >= CONTROL_PLANE_PTT_FOREGROUND_RECONNECT_MAX_ATTEMPTS) {
control_plane_ptt_set_state(PTT_STATE_DENIED, ...);
}
```
**问题**:判断条件使用 snapshot 中的旧 `ptt.foreground_attempts`,而实际值已在临界区内递增。若在此期间有其他代码重置了 `foreground_attempts`,此处仍依据旧值做出 `DENIED` 决策。
**修复**:在临界区内同时读取和判断 `foreground_attempts`。
---
### H6. WS 分片重组全局缓冲区无互斥保护
- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c`
- **函数**:`process_text_fragment()` / `process_binary_fragment()`
**问题**:`s_text_reassembly_buf` / `s_binary_reassembly_buf` 在 queue 创建失败的 fallback 路径下,`handle_ws_msg_direct()` 会在 WebSocket client 事件任务上下文中直接调用这些函数。同时 `multimodal_ws_stop()` 会在调用者任务中执行 `reset_*_reassembly()`。两者无互斥,构成数据竞态。
**修复**:对重组缓冲区操作加锁,或确保 fallback 路径也串行化到同一任务。
---
### H7. `wait_worker_task_exit()` 对全局句柄无原子/屏障读取
- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c`
- **函数**:`wait_worker_task_exit()`
- **行号**:472-489
```c
while (*task_handle != NULL && (xTaskGetTickCount() - start_tick) < timeout_ticks) {
vTaskDelay(pdMS_TO_TICKS(20));
}
```
**问题**:`s_ws_dispatch_task` 由被等待任务在退出时置 NULL,但本函数直接解引用读取,无原子操作或内存屏障。在 ESP32 多核缓存下,可能永远读到旧值导致超时后误判为未退出,进而执行对已自删任务的二次删除。
**修复**:使用 `volatile TaskHandle_t` + 适当内存屏障,或通过通知机制同步。
---
### H8. LCD 渲染任务强制删除导致 Mutex 永久占用
- **文件**:`components/platform/src/lcd_direct.c`
- **函数**:`lcd_direct_stop_render_loop()`
- **行号**:691-738
```c
if (task != xTaskGetCurrentTaskHandle()) {
vTaskDelete(task); // 若 task 正持有 s_lock,Mutex 将永久锁定
...
}
```
**问题**:渲染循环中会 `xSemaphoreTake(s_lock, ...)`。若任务在持锁期间被强制删除,FreeRTOS 不会自动释放 Mutex,导致后续所有尝试获取 `s_lock` 的操作永远超时。
**修复**:避免 `vTaskDelete` 强制终止;使用通知 + 轮询标志让任务自行退出。
---
### H9. 步进电机旋转长期占用 CPU 不释放
- **文件**:`components/platform/src/platform_stepper_driver.c`
- **函数**:`rotate_with_accel()`
- **行号**:275-303
```c
for (int i = 0; i < steps; i++) {
stepper_advance_phase(clockwise);
precise_delay_us(current_delay_us); // busy-wait
}
```
**问题**:以最快 600 us/步、最大 330° ≈ 3784 步计算,最长可连续自旋约 **2.3 秒** 不执行 `taskYIELD`。在此期间其他任务(包括音频采集、WS 保活)被饿死,可能导致音频欠采样、WS 断开。
**修复**:在步进循环中每隔一定步数调用 `taskYIELD()`,或将步进控制移到硬件定时器/LEDC。
---
### H10. Factory CLI 强制删除已自行退出的任务
- **文件**:`components/control_plane/src/runtime/control_plane_factory_cli.c`
- **函数**:`control_plane_factory_cli_stop()`
- **行号**:526-550
**问题**:若 worker 在循环等待期间已自行调用 `vTaskDelete(NULL)`,主线程第二次检查时理论上应返回。但存在竞态窗口:worker 尚未执行 `vTaskDelete(NULL)` 时,主线程仍得到非 NULL 句柄,随后执行 `vTaskDelete(worker_task)` 操作一个已结束任务的 TCB,属于未定义行为。
**修复**:使用 `xTaskNotifyWait` 或事件组等待 worker 明确报告退出,而非轮询 + 强制删除。
---
### H11. System Event / Key Input 任务直接强制删除
- **文件**:`components/control_plane/src/runtime/control_plane_system_events.c`
- **函数**:`control_plane_stop_system_event_task()`
- **行号**:257-276
```c
if (s_system_event_task != NULL) {
vTaskDelete(s_system_event_task); // 任务可能正在持有锁或访问共享资源
s_system_event_task = NULL;
}
```
**问题**:两处均在超时后直接 `vTaskDelete`。FreeRTOS 不保证被删任务持有的互斥锁会被释放。若任务在 `xQueueReceive` 临界区或持有某把锁时被删,将导致后续死锁或资源泄漏。
**修复**:使用任务通知 + 超时等待任务自行退出。
---
### H12. 浮点精度导致方向查询失败
- **文件**:`components/domain/src/direction/domain_direction_catalog.c`
- **函数**:`domain_direction_catalog_find_by_angle`
- **行号**:149
```c
if (entry->angle == angle) { // 精确浮点比较
```
**问题**:`domain_get_current_direction_index()` 将经多次浮点运算后的 `s_current_angle` 传入此函数。浮点运算会引入微小误差(如 `44.999996f` 与 `45.0f`),导致精确相等比较失败。
**修复**:引入 epsilon 容差比较,如 `fabsf(entry->angle - angle) < 0.5f`。
---
### H13. HTTP 响应 JSON 打印失败时缓冲区残留脏数据
- **文件**:`components/domain/src/fortune/domain_user_config_server.c`
- **函数**:`response_json_object`
- **行号**:130-136
```c
if (!cJSON_PrintPreallocated(root, (char *)response->body,
(int)response->body_capacity, false)) {
response->body_len = 0U;
return ESP_ERR_INVALID_SIZE;
}
```
**问题**:`cJSON_PrintPreallocated` 在缓冲区不足时可能**写入部分 JSON** 后返回 `false`。此时函数仅置 `body_len = 0` 并返回错误,但 `response->body` 中残留不完整 JSON。若上层调用者忽略返回值直接发送,客户端会收到截断/损坏的 JSON。
**修复**:失败前显式将 `response->body[0] = '\0'`。
---
### H14. 旋转期间状态锁释放导致脏读及角度不一致
- **文件**:`components/domain/src/direction/domain_direction_service.c`
- **函数**:`domain_rotate_to_angle_with_timeout`
- **行号**:302-364
```c
domain_direction_unlock_state(); // 释放 state mutex
// ... 电机运动期间 ...
esp_err_t motor_err = domain_runtime_stepper_rotate_angle_with_accel_timeout(...);
```
**问题**:释放 `s_state_mutex` 后、电机运动完成前,其他任务可获取 `s_state_mutex` 并读取 `s_current_angle`(此时仍是旧值),产生不一致快照。此外,若电机实际运动与指令存在偏差,`s_current_angle` 在运动后仅按理论值更新。
**修复**:旋转完成后重新获取锁更新角度,或记录旋转目标值供读取者使用。
---
### H15. HTTP 请求路径/方法 NULL/长度未校验
- **文件**:`components/domain/src/fortune/domain_user_config_server.c`
- **函数**:`user_config_http_handler`
- **行号**:338-364
```c
if (strcmp(request->path, "/") == 0) { // request->path 可能为 NULL
```
**问题**:对 `request->path` 直接使用 `strcmp`,未校验 `NULL` 或确保 NUL 终止;`request->method` 也未校验是否为合法枚举值。若平台层传递畸形请求,将导致空指针解引用或越界读取。
**修复**:增加 `request->path != NULL` 和 `request->method` 范围校验。
---
### H16. 启动顺序错误 — 平台总初始化晚于音频/LCD
- **文件**:`components/app_composition/src/runtime/app_composition_default_runtime.c`
- **函数**:`app_composition_run_plan_a_boot_sequence`
- **行号**:67-194
**问题**:启动流程中 `control_plane_play_boot_sound()` 和 `control_plane_init_lcd_device()` 在 `control_plane_init()`(内部调用 `domain_runtime_platform_init()` → `platform_init()`)之前执行。若音频或 LCD 的 platform 实现依赖 `platform_init()` 预先配置的 GPIO/DMA/时钟等资源,前 3 步将在平台未就绪时操作硬件。
**修复**:将 `platform_init()` 或等价的底层初始化移到 boot sequence 的最前面。
---
## 三、中(Medium)— 设计缺陷、资源泄漏或边界条件
### M1. `local_responding_event_worker` 队列永久泄漏
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c`
- **函数**:`stop_local_responding_event_worker()`
- **行号**:612-654
```c
if (s_local_responding_event_queue != NULL) {
xQueueReset(s_local_responding_event_queue); // 仅 reset,未删除
}
```
**问题**:队列在 stop 时仅被 `xQueueReset`,从未 `vQueueDelete`。反复启停 multimodal client 将导致队列句柄永久累积。
**修复**:`vQueueDelete(s_local_responding_event_queue)` 并将指针置 NULL。
---
### M2. `send_start` 发送成功后状态被 mark_transport_unavailable 干扰
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c`
- **函数**:`control_plane_multimodal_send_start()`
- **行号**:1843-1866
```c
err = domain_runtime_multimodal_transport_send_text("start", json);
// ...
if (!s_start_in_progress || strcmp(s_pending_start_task_id, task_id) != 0) {
return ESP_ERR_INVALID_STATE; // Start 已发到服务端,但本地拒绝记录
}
```
**问题**:`send_text` 成功返回后、再次加锁前,若 transport callback 线程调用 `mark_transport_unavailable()` 清空了 `s_start_in_progress`,则函数返回 `ESP_ERR_INVALID_STATE`。此时服务端已创建 task,客户端却无记录,形成 orphaned server task。
**修复**:在发送前加锁获取 task_id,发送完成后在锁内更新状态。
---
### M3. `process_text_json` 中 `is_finished` 跨事件污染
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_session_service.c`
- **函数**:`process_text_json()`
- **行号**:1218, 1412-1423
```c
bool is_finished = cJSON_IsBool(finished) ? cJSON_IsTrue(finished) : false;
// ...
if (text_len != s_last_asr_text_len || is_finished) { ... }
```
**问题**:`is_finished` 在函数入口处基于 `output.finished` 计算一次。`SpeechContent` 事件(ASR 中间结果)通常不带 `finished` 字段,此时 `is_finished` 保留上一个事件(可能是 TTS `RespondingContent`)的值。若上一个事件 `finished=true`,当前 ASR 文本会被错误标记为 final。
**修复**:每个事件分支独立计算 `is_finished`,或使用事件类型判断而非共享变量。
---
### M4. 生日档案日期校验宽松 — 接受 2 月 31 日等非法日期
- **文件**:`components/domain/src/fortune/domain_fortune_store.c`
- **函数**:`domain_birth_profile_is_valid`
- **行号**:36-46
```c
profile->day >= 1 && profile->day <= 31 // 2月也可为31
```
**问题**:存储层接受所有月份的 day<=31。若数据绕过 Web 校验直接写入 NVS,非法日期将通过校验。
**修复**:增加按月份的日期上限校验,或统一使用 `is_valid_date` 函数。
---
### M5. HTTP 请求体无上限 — DoS/内存耗尽风险
- **文件**:`components/domain/src/fortune/domain_user_config_server.c`
- **函数**:`parse_profile`
- **行号**:198-212
```c
cJSON *root = cJSON_ParseWithLength((const char *)body, body_len);
```
**问题**:`body_len` 未设上限直接传入 `cJSON_ParseWithLength`。若攻击者发送数 MB 的畸形 JSON,将耗尽堆内存。
**修复**:在 platform HTTP server 层或 domain 层增加 `body_len <= MAX_JSON_BODY_SIZE` 校验。
---
### M6. 启动失败时资源未清理
- **文件**:`components/app_composition/src/runtime/app_composition_default_runtime.c`
- **函数**:`app_composition_run_plan_a_boot_sequence`
- **行号**:128-131, 164-167, 183-187
**问题**:若 `control_plane_init()`、`control_plane_prepare_runtime_storage()` 或 `control_plane_start_runtime_workers()` 失败,函数直接返回错误。但此前已初始化的音频子系统、LCD 设备、LCD 渲染任务等**没有任何 stop/deinit** 路径。
**修复**:在 boot sequence 中增加失败时的逆向清理逻辑(reverse cleanup)。
---
### M7. 旧版认证迁移写失败导致反复重试迁移
- **文件**:`components/domain/src/runtime/domain_runtime_adapters.c`
- **函数**:`domain_runtime_auth_store_load_record`
- **行号**:1005-1018
```c
esp_err_t save_err = platform_storage_set_blob(ns, key, out, actual);
if (save_err != ESP_OK) {
ESP_LOGW(TAG, "Persist migrated auth record failed");
} else {
ESP_LOGI(TAG, "Migrated legacy auth record");
}
return ESP_OK; // 无论 save_err 如何都返回 OK
```
**问题**:`save_err != ESP_OK` 时仅打印警告,仍返回 `ESP_OK`。下次加载同一条记录时,将再次执行迁移流程,造成反复读写 NVS。
**修复**:迁移写失败时应返回错误,或设置标志避免重复迁移。
---
### M8. 控制层直接调用 ESP-IDF 堆 API — 架构层旁路
- **文件**:`components/control_plane/src/runtime/control_plane_fortune_service.c`
- **函数**:`fortune_psram_first_calloc`
- **行号**:46-60
```c
void *ptr = heap_caps_calloc(count, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
```
**问题**:`control_plane` 直接调用 `heap_caps_calloc` 和包含 `soc/soc_caps.h`。根据 `AGENTS.md` 的四层依赖约束,control_plane 不应直接调用 ESP-IDF 内部 API,而应通过 `platform` facade。
**修复**:将 PSRAM 分配封装为 `platform_psram_first_calloc()`,由 platform 层暴露给上层。
---
### M9. `wifi_portal_connect_sta` 持锁忙等 10 秒
- **文件**:`components/platform/src/wifi_portal_bridge.cpp`
- **函数**:`wifi_portal_connect_sta`
- **行号**:280-291
```c
while (esp_timer_get_time() < deadline_us) {
if (manager.IsConnected()) { return ESP_OK; }
vTaskDelay(pdMS_TO_TICKS(100));
}
```
**问题**:在持有 `s_portal_mutex` 递归锁的情况下进行最多 10 秒的轮询。期间所有需要该锁的操作(保存凭证、清除凭证)全部被阻塞。
**修复**:释放锁后轮询,或使用事件组/信号量等待连接完成。
---
### M10. 旋转互斥锁为非递归锁
- **文件**:`components/domain/src/direction/domain_direction_service.c`
- **函数**:`ensure_rotation_mutex`
- **行号**:99-125
```c
SemaphoreHandle_t created_mutex = xSemaphoreCreateMutex(); // 非递归
```
**问题**:`s_rotation_mutex` 是普通互斥锁(非递归)。若同一任务因回调或嵌套调用再次进入 `domain_rotate_to_angle_with_timeout`,将在 `domain_direction_lock_rotation` 死锁。
**修复**:改为 `xSemaphoreCreateRecursiveMutex()`。
---
### M11. `platform_storage_get_blob` / `get_string` 栈上 1.6KB 结构体
- **文件**:`components/domain/src/runtime/domain_runtime_adapters.c`
- **行号**:778, 870
```c
typedef struct { char jwt[1536]; ... } domain_auth_state_legacy_blob_t;
domain_auth_state_legacy_blob_t state = {0}; // 约1.6KB 栈分配
```
**问题**:该结构体分配在栈上,在栈空间受限的任务中可能导致栈溢出。
**修复**:改为 PSRAM 动态分配。
---
### M12. `control_plane_set_ptt_active()` 临界区内逻辑偏多
- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c`
- **函数**:`control_plane_set_ptt_active()`
- **行号**:293-335
**问题**:`portENTER_CRITICAL(&s_runtime_lock)` 内包含大量字段赋值与条件分支。在高频按键场景下会延长另一核心获取自旋锁的等待时间,影响中断延迟。
**修复**:将非必要字段移到临界区外,或使用轻量级原子操作替代。
---
## 四、低(Low)— 代码质量、可维护性和优化建议
### L1. `s_server_success_prompt_played` 永不清零
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_runtime.c`
- **行号**:58
- **问题**:断连重连后成功提示音不再播放。确认是否为有意设计。
### L2. 多处 `cJSON_free` 与 `free` / `heap_caps_free` 混用
- **文件**:`control_plane_multimodal_device_auth.c`、`control_plane_multimodal_auth_service.c`
- **问题**:`cJSON_PrintUnformatted` 的结果使用 `cJSON_free()` 释放,而 PSRAM 分配的内存有时使用 `free()`。ESP-IDF 默认下 `cJSON_free` 等同于 `free`,但一旦 cJSON 库配置为自定义分配器,将导致分配/释放不匹配。
### L3. `control_plane_multimodal_generate_uuid()` 使用非加密安全随机数
- **文件**:`components/control_plane/src/multimodal/control_plane_multimodal_protocol.c`
- **行号**:102-124
- **问题**:`esp_random()` 基于硬件 RNG,但在某些启动阶段熵可能不足。对于 task_id 等协议标识符虽无安全要求,但若未来用于安全敏感场景需替换。
### L4. `audio_debug_dump_impl.c` UDP socket 无错误检查
- **文件**:`components/platform/src/multimodal/audio_pipeline/audio_debug_dump_impl.c`
- **行号**:95-127
- **问题**:`sendto` 返回值被忽略。若网络异常导致持续失败,无法及时发现 socket 异常。
### L5. `domain_runtime_post_json` 的 auth header 应改用栈缓冲区
- **文件**:`components/domain/src/runtime/domain_runtime_adapters.c`
- **行号**:477-511
- **问题**:每次调用都 `malloc` + `snprintf` + `free` 一个 Authorization header 字符串。token 最大 256 字节 + "Bearer " 前缀,总计约 264 字节,完全可以在栈上分配。
### L6. `control_plane_delete_task(NULL, task_with_caps)` 语义不清晰
- **文件**:`components/control_plane/src/runtime/control_plane_runtime_workers.c`
- **行号**:631
- **问题**:`vTaskDelete(NULL)` 会删除当前任务,但代码意图极不清晰。应加注释说明。
### L7. `control_plane_events.h` 的 event union 只有 key 成员
- **文件**:`components/control_plane/internal/control_plane_events.h`
- **行号**:50-56
- **问题**:定义了 WIFI、MULTIMODAL、LCD 等多种事件族,但 union 中只有 `key` 成员。若不打算通过事件队列传递其他事件,建议简化并加注释。
### L8. `platform_storage.c` NVS handle 频繁 open/close
- **文件**:`components/platform/src/runtime/platform_storage.c`
- **问题**:每次读写操作都调用 `nvs_open` + `nvs_close`,有固定开销。对高频 namespace 考虑缓存 handle。
### L9. `transport_ws.c` `s_headers[384]` 缓冲区偏小
- **文件**:`components/platform/src/multimodal/transport_ws/transport_ws.c`
- **行号**:158-159
- **问题**:Bearer token 255 字符 + workspace ID + header 格式可能超过 384 字节。建议增大至 512 字节。
### L10. `main.c` 忽略启动返回值
- **文件**:`main/main.c`
- **行号**:9-11
- **问题**:`(void)app_composition_start_default_runtime();` 启动失败被静默忽略。
---
## 五、修复优先级建议
### P0 — 必须立即修复(崩溃/永久功能丧失风险)
| 编号 | 问题 | 文件 |
|------|------|------|
| C1 | Watchdog 自删除导致多模态永久失效 | multimodal_runtime.c |
| C2 | Key Input ISR UAF | control_plane_key_input.c |
| C3 | 音频下游队列 UAF | audio_pipeline_runtime_impl.c |
| C4 | 音频任务强制删除 UAF | audio_pipeline_runtime_impl.c |
| C5 | 校准缺旋转锁 | domain_calibration_service.c |
| C6 | 角度归一化死循环 | domain_angle_math.c |
| C7 | WS 发送锁删除竞态 | transport_ws_runtime_impl.c |
| C8 | Timer 回调竞态删除 | audio_pipeline_runtime_impl.c |
| C9 | `is_connected` 对已销毁 client 操作 | transport_ws.c |
| C10 | LAN HTTP Server UAF | platform_lan_http_server.c |
| H1 | `s_started` 与 transport 状态不一致 | session_service.c |
| H2 | Session mutex 创建非线程安全 | session_service.c |
### P1 — 应尽快修复(竞态/数据损坏/功能异常)
| 编号 | 问题 | 文件 |
|------|------|------|
| H3 | Device Auth 损坏数据继续初始化 | device_auth.c |
| H4 | PTT teardown TOCTOU | runtime_workers.c |
| H5 | PTT foreground_attempts 不一致 | runtime_workers.c |
| H6 | WS 重组缓冲区无互斥 | transport_ws_workers_impl.c |
| H7 | wait_worker_task_exit 无原子读取 | transport_ws_workers_impl.c |
| H8 | LCD 渲染任务强制删除锁永久占用 | lcd_direct.c |
| H9 | 步进电机旋转饿死其他任务 | platform_stepper_driver.c |
| H10-H11 | 多处强制删除任务 | factory_cli.c / system_events.c |
| H12 | 浮点精确比较失败 | domain_direction_catalog.c |
| H13 | JSON 打印脏数据 | domain_user_config_server.c |
| H16 | 启动顺序错误 | app_composition_default_runtime.c |
### P2 — 建议优化(资源泄漏、设计缺陷、边界条件)
| 编号 | 问题 | 文件 |
|------|------|------|
| M1 | local_responding_event 队列泄漏 | session_service.c |
| M2 | send_start orphaned task | session_service.c |
| M3 | is_finished 跨事件污染 | session_service.c |
| M4-M5 | 日期校验/HTTP body 上限 | fortune_store.c / user_config_server.c |
| M6 | 启动失败资源未清理 | app_composition_default_runtime.c |
| M7 | 认证迁移反复重试 | domain_runtime_adapters.c |
| M8 | 架构层旁路 | control_plane_fortune_service.c |
| M9 | Wi-Fi 连接持锁忙等 | wifi_portal_bridge.cpp |
| M10 | 旋转锁非递归 | domain_direction_service.c |
### P3 — 可后续处理(代码质量、性能、文档)
L1-L10 及其他低优先级项。
---
## 六、总结
本次深度审查在三个层面共发现 **10 个 Critical、16 个 High、12 个 Medium、10 个 Low** 级别问题。
**最危险的发现**:
1. **Watchdog 任务自删除**(C1)会导致设备空闲超时后多模态功能**永久丧失**,除非重启
2. **多处 Use-After-Free**(C2-C4, C9-C10)在特定时序下会直接导致系统崩溃
3. **校准缺旋转锁**(C5)会导致步进电机并发冲突,可能损坏机械结构
4. **角度归一化死循环**(C6)对畸形输入会挂死任务,触发看门狗复位
**建议立即处理 P0 列表中的 12 项**,它们覆盖了崩溃、永久功能丧失和硬件损坏风险。P1 中的竞态条件和数据损坏问题应在下一个迭代中跟进。