diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index 1758cd3..9d0c67c 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -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); diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c b/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c index 3f536c6..ac04616 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_session_service.c @@ -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) { - s_local_responding_event_task = NULL; + 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(); - reset_session_state_if_task_matches(task_id); + 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)); diff --git a/components/control_plane/src/runtime/control_plane_key_input.c b/components/control_plane/src/runtime/control_plane_key_input.c index e924c22..887c0f1 100644 --- a/components/control_plane/src/runtime/control_plane_key_input.c +++ b/components/control_plane/src/runtime/control_plane_key_input.c @@ -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); diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index 1a36db6..499fd67 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -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); - portENTER_CRITICAL(&s_runtime_lock); - s_ptt_runtime.foreground_attempts++; - portEXIT_CRITICAL(&s_runtime_lock); + 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"); } diff --git a/components/domain/src/direction/domain_direction_service.c b/components/domain/src/direction/domain_direction_service.c index 82bf86b..d476741 100644 --- a/components/domain/src/direction/domain_direction_service.c +++ b/components/domain/src/direction/domain_direction_service.c @@ -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; } diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index baf948c..43afcaf 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -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; } diff --git a/components/platform/src/multimodal/transport_ws.c b/components/platform/src/multimodal/transport_ws.c index cefd667..0cec85a 100644 --- a/components/platform/src/multimodal/transport_ws.c +++ b/components/platform/src/multimodal/transport_ws.c @@ -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; diff --git a/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c b/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c index 502a05a..51bdfe4 100644 --- a/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c +++ b/components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c @@ -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); - emit_event(MULTIMODAL_WS_EVENT_DISCONNECTED, NULL, 0U); + if (!stop_requested) { + emit_event(MULTIMODAL_WS_EVENT_DISCONNECTED, NULL, 0U); + } break; } diff --git a/components/platform/src/wifi_portal_bridge.cpp b/components/platform/src/wifi_portal_bridge.cpp index 8c0cc89..7a66f6c 100644 --- a/components/platform/src/wifi_portal_bridge.cpp +++ b/components/platform/src/wifi_portal_bridge.cpp @@ -254,39 +254,41 @@ 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; + { + ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); + PortalUnlockGuard guard; - if (!wifi_portal_has_saved_credentials()) { - return ESP_ERR_NOT_FOUND; - } + if (!wifi_portal_has_saved_credentials()) { + return ESP_ERR_NOT_FOUND; + } - ESP_LOGI(TAG, - "Starting STA connection with %u saved credential candidate(s)", - (unsigned)SsidManager::GetInstance().GetSsidList().size()); - log_saved_wifi_candidates("STA connect"); + ESP_LOGI(TAG, + "Starting STA connection with %u saved credential candidate(s)", + (unsigned)SsidManager::GetInstance().GetSsidList().size()); + log_saved_wifi_candidates("STA connect"); - ESP_RETURN_ON_ERROR(ensure_wifi_manager_initialized(), TAG, "Failed to initialize Wi-Fi manager"); + ESP_RETURN_ON_ERROR(ensure_wifi_manager_initialized(), TAG, "Failed to initialize Wi-Fi manager"); - auto &manager = WifiManager::GetInstance(); - if (manager.IsConnected()) { + auto &manager = WifiManager::GetInstance(); + if (manager.IsConnected()) { + (void)apply_sta_stability_policy(); + return ESP_OK; + } + + manager.StartStation(); (void)apply_sta_stability_policy(); - return ESP_OK; } - 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; } diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..960a8d4 --- /dev/null +++ b/log.txt @@ -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: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 diff --git a/log1.txt b/log1.txt new file mode 100644 index 0000000..6cc5be6 --- /dev/null +++ b/log1.txt @@ -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 \ No newline at end of file diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 3de658a..a06391c 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -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 diff --git a/remote-logging-system-design.md b/remote-logging-system-design.md new file mode 100644 index 0000000..c777a7e --- /dev/null +++ b/remote-logging-system-design.md @@ -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 +``` + +--- + +## 三、核心模块设计 + +### 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_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. 用户对话期间暂停上传,保证实时语音不受干扰 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 67e257f..028d170 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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" diff --git a/tools/check_architecture_guards.sh b/tools/check_architecture_guards.sh index 305c254..847029b 100644 --- a/tools/check_architecture_guards.sh +++ b/tools/check_architecture_guards.sh @@ -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 diff --git a/tq-catn-deep-code-review.md b/tq-catn-deep-code-review.md new file mode 100644 index 0000000..ea89445 --- /dev/null +++ b/tq-catn-deep-code-review.md @@ -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 中的竞态条件和数据损坏问题应在下一个迭代中跟进。