From 7a4699ea689b3d24e114530623973ae341ee3588 Mon Sep 17 00:00:00 2001 From: chenjintao Date: Wed, 12 Aug 2026 09:47:55 +0800 Subject: [PATCH] feat(voice): gate local wake prompt on network readiness --- .../internal/control_plane_internal.h | 2 + .../control_plane_multimodal_local_wake.c | 209 +++++++++++++----- .../runtime/control_plane_runtime_workers.c | 43 +++- components/domain/include/domain.h | 2 + .../src/runtime/domain_runtime_adapters.c | 16 ++ components/platform/CMakeLists.txt | 3 + components/platform/assets/lcd/listening0.png | Bin 0 -> 19615 bytes components/platform/assets/lcd/listening1.png | Bin 0 -> 15072 bytes components/platform/assets/lcd/listening2.png | Bin 0 -> 10758 bytes components/platform/src/lcd_direct_assets.c | 12 + .../platform/src/multimodal/audio_pipeline.c | 3 + .../audio_pipeline_runtime_impl.c | 65 ++++++ main/Kconfig.projbuild | 10 + sdkconfig.defaults | 1 + tools/check_architecture_guards.sh | 19 ++ 15 files changed, 323 insertions(+), 62 deletions(-) create mode 100644 components/platform/assets/lcd/listening0.png create mode 100644 components/platform/assets/lcd/listening1.png create mode 100644 components/platform/assets/lcd/listening2.png diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index 3b3f9b9..b0932ef 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -137,6 +137,8 @@ esp_err_t control_plane_local_wake_start(void); esp_err_t control_plane_local_wake_stop(int timeout_ms); void control_plane_local_wake_pause_for_foreground(void); void control_plane_local_wake_resume_after_foreground(void); +bool control_plane_local_wake_prompt_gate_active(void); +void control_plane_ptt_reset_capture_window(void); bool control_plane_voice_turn_is_idle(void); esp_err_t control_plane_start_system_event_task(void); diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c b/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c index 78cf954..bcc7d85 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c @@ -25,6 +25,10 @@ #define CONFIG_TQ_LOCAL_WAKE_RETRY_MS 5000 #endif +#ifndef CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS +#define CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS 10000 +#endif + #ifndef CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS #define CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS 1000 #endif @@ -43,8 +47,6 @@ #define CONTROL_PLANE_LOCAL_WAKE_QUEUE_LEN 4U #define CONTROL_PLANE_LOCAL_WAKE_POLL_MS 100U -#define CONTROL_PLANE_LOCAL_WAKE_BARGE_IN_WAIT_MS 2000U -#define CONTROL_PLANE_LOCAL_WAKE_BARGE_IN_POLL_MS 20U #if CONFIG_TQ_LOCAL_WAKE_TASK_CORE < 0 #define CONTROL_PLANE_LOCAL_WAKE_TASK_CORE tskNO_AFFINITY @@ -59,6 +61,11 @@ typedef struct { bool started; bool paused_for_foreground; bool capture_owned; + bool prompt_pending; + bool prompt_playing; + bool listening_ui_active; + TickType_t prompt_wait_started_tick; + uint32_t detection_generation; } local_wake_state_t; static const char *TAG = "cp_local_wake"; @@ -81,6 +88,31 @@ static void local_wake_set_capture_owned(bool owned) portEXIT_CRITICAL(&s_local_wake_lock); } +static void local_wake_request_expression(domain_expression_id_t id) +{ + const domain_expression_request_t request = { + .id = id, + .trigger = DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE, + }; + esp_err_t err = domain_runtime_request_expression(&request); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, + "Local wake expression request failed: id=%d err=%s", + (int)id, + esp_err_to_name(err)); + } +} + +bool control_plane_local_wake_prompt_gate_active(void) +{ +#if CONFIG_TQ_LOCAL_WAKE_ENABLE && CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE + local_wake_state_t state = local_wake_snapshot(); + return state.prompt_pending || state.prompt_playing; +#else + return false; +#endif +} + static void local_wake_domain_event( const domain_local_command_event_t *event, void *user_data) @@ -153,63 +185,107 @@ static esp_err_t local_wake_start_listening(void) return ESP_OK; } -#if CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE -static bool local_wake_prepare_prompt_playback(void) +static bool local_wake_runtime_ready_for_prompt( + control_plane_multimodal_runtime_state_t *out_state) { control_plane_multimodal_runtime_state_t state = {0}; esp_err_t state_err = control_plane_multimodal_get_runtime_state(&state); if (state_err != ESP_OK) { - ESP_LOGW(TAG, - "Skip local wake prompt: runtime state unavailable: %s", - esp_err_to_name(state_err)); return false; } + if (out_state != NULL) { + *out_state = state; + } + return state.connected && + state.has_active_task && + state.server_listening && + !state.remote_responding && + !state.request_to_speak_pending && + !state.client_speech_active && + !state.ptt_release_required; +} - if (!state.remote_responding && !state.request_to_speak_pending) { - return true; +static void local_wake_service_ready_prompt(void) +{ +#if CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE + local_wake_state_t state = local_wake_snapshot(); + if (!state.prompt_pending || state.prompt_playing || state.stop_requested) { + return; } - esp_err_t request_err = control_plane_multimodal_send_request_to_speak(); - if (request_err != ESP_OK && request_err != ESP_ERR_INVALID_STATE) { - ESP_LOGW(TAG, - "Skip local wake prompt: barge-in request failed: %s", - esp_err_to_name(request_err)); - return false; - } - - TickType_t started = xTaskGetTickCount(); - TickType_t timeout_ticks = pdMS_TO_TICKS( - CONTROL_PLANE_LOCAL_WAKE_BARGE_IN_WAIT_MS); + TickType_t now = xTaskGetTickCount(); + TickType_t timeout_ticks = pdMS_TO_TICKS(CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS); if (timeout_ticks == 0U) { timeout_ticks = 1U; } - - while ((xTaskGetTickCount() - started) < timeout_ticks) { - local_wake_state_t wake_state = local_wake_snapshot(); - if (wake_state.stop_requested) { - return false; - } - - state_err = control_plane_multimodal_get_runtime_state(&state); - if (state_err == ESP_OK) { - if (!state.connected || !state.has_active_task) { - return false; - } - if (!state.remote_responding && - !state.request_to_speak_pending && - state.server_listening) { - return true; - } - } - vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_LOCAL_WAKE_BARGE_IN_POLL_MS)); + control_plane_multimodal_runtime_state_t runtime = {0}; + bool runtime_ready = local_wake_runtime_ready_for_prompt(&runtime); + if (state.prompt_wait_started_tick != 0U && + (now - state.prompt_wait_started_tick) >= timeout_ticks) { + ESP_LOGW(TAG, + "Local wake network readiness timed out: generation=%u timeout_ms=%u ws=%d task=%d listening=%d responding=%d request_pending=%d speech=%d release_required=%d", + (unsigned)state.detection_generation, + (unsigned)CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS, + runtime.connected ? 1 : 0, + runtime.has_active_task ? 1 : 0, + runtime.server_listening ? 1 : 0, + runtime.remote_responding ? 1 : 0, + runtime.request_to_speak_pending ? 1 : 0, + runtime.client_speech_active ? 1 : 0, + runtime.ptt_release_required ? 1 : 0); + control_plane_set_ptt_active(false); + return; } - ESP_LOGW(TAG, - "Skip local wake prompt: barge-in confirmation timed out after %ums", - (unsigned)CONTROL_PLANE_LOCAL_WAKE_BARGE_IN_WAIT_MS); - return false; -} + if (!runtime_ready) { + return; + } + + portENTER_CRITICAL(&s_local_wake_lock); + if (!s_local_wake.prompt_pending || s_local_wake.stop_requested) { + portEXIT_CRITICAL(&s_local_wake_lock); + return; + } + s_local_wake.prompt_pending = false; + s_local_wake.prompt_playing = true; + portEXIT_CRITICAL(&s_local_wake_lock); + + const uint32_t ready_wait_ms = state.prompt_wait_started_tick != 0U ? + (uint32_t)pdTICKS_TO_MS(now - state.prompt_wait_started_tick) : 0U; + ESP_LOGI(TAG, + "Local wake network ready: generation=%u wait_ms=%u ws=1 task=1 listening=1", + (unsigned)state.detection_generation, + (unsigned)ready_wait_ms); + TickType_t prompt_started_tick = xTaskGetTickCount(); + ESP_LOGI(TAG, + "Local wake response prompt started: generation=%u file=%s", + (unsigned)state.detection_generation, + CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE); + esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file( + CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE); + const uint32_t prompt_elapsed_ms = (uint32_t)pdTICKS_TO_MS( + xTaskGetTickCount() - prompt_started_tick); + + portENTER_CRITICAL(&s_local_wake_lock); + s_local_wake.prompt_playing = false; + s_local_wake.prompt_wait_started_tick = 0U; + portEXIT_CRITICAL(&s_local_wake_lock); + + if (prompt_err != ESP_OK) { + ESP_LOGW(TAG, + "Local wake response prompt failed: generation=%u elapsed_ms=%u err=%s", + (unsigned)state.detection_generation, + (unsigned)prompt_elapsed_ms, + esp_err_to_name(prompt_err)); + } else { + ESP_LOGI(TAG, + "Local wake response prompt completed: generation=%u elapsed_ms=%u", + (unsigned)state.detection_generation, + (unsigned)prompt_elapsed_ms); + } + control_plane_ptt_reset_capture_window(); #endif +} static void local_wake_handle_detection( const domain_local_command_event_t *event) @@ -226,31 +302,31 @@ static void local_wake_handle_detection( return; } s_local_wake.paused_for_foreground = true; + s_local_wake.listening_ui_active = true; + s_local_wake.detection_generation = event->detection_generation; +#if CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE + s_local_wake.prompt_pending = CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE[0] != '\0'; + s_local_wake.prompt_playing = false; + s_local_wake.prompt_wait_started_tick = xTaskGetTickCount(); +#endif portEXIT_CRITICAL(&s_local_wake_lock); + local_wake_request_expression(DOMAIN_EXPRESSION_LISTENING); + ESP_LOGI(TAG, + "Local wake foreground entered: generation=%u listening_ui=1 prompt_gate=%d ready_timeout_ms=%u", + (unsigned)event->detection_generation, + control_plane_local_wake_prompt_gate_active() ? 1 : 0, + (unsigned)CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS); + local_wake_stop_owned_capture(); (void)domain_runtime_local_command_suspend( CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS); ESP_LOGI(TAG, - "Local wake accepted: phrase=喵掌柜 confidence_x10000=%u generation=%u", + "Local wake accepted: phrase=喵掌柜 confidence_x10000=%u generation=%u; network preparation requested", (unsigned)event->confidence_x10000, (unsigned)event->detection_generation); -#if CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE - bool prompt_playback_ready = local_wake_prepare_prompt_playback(); - if (prompt_playback_ready && - CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE[0] != '\0') { - esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file( - CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE); - if (prompt_err != ESP_OK) { - ESP_LOGW(TAG, - "Local wake response prompt failed: %s", - esp_err_to_name(prompt_err)); - } - } -#endif - control_plane_set_ptt_active(true); } @@ -264,6 +340,7 @@ static void local_wake_task(void *arg) while (!local_wake_snapshot().stop_requested) { local_wake_state_t state = local_wake_snapshot(); TickType_t now = xTaskGetTickCount(); + local_wake_service_ready_prompt(); if (!state.paused_for_foreground && control_plane_voice_turn_is_idle() && (retry_not_before == 0U || now >= retry_not_before)) { @@ -342,6 +419,11 @@ esp_err_t control_plane_local_wake_start(void) s_local_wake.stop_requested = false; s_local_wake.paused_for_foreground = false; s_local_wake.capture_owned = false; + s_local_wake.prompt_pending = false; + s_local_wake.prompt_playing = false; + s_local_wake.listening_ui_active = false; + s_local_wake.prompt_wait_started_tick = 0U; + s_local_wake.detection_generation = 0U; s_local_wake.started = true; portEXIT_CRITICAL(&s_local_wake_lock); @@ -434,10 +516,19 @@ void control_plane_local_wake_pause_for_foreground(void) void control_plane_local_wake_resume_after_foreground(void) { #if CONFIG_TQ_LOCAL_WAKE_ENABLE + bool restore_idle_ui = false; portENTER_CRITICAL(&s_local_wake_lock); s_local_wake.paused_for_foreground = false; + s_local_wake.prompt_pending = false; + s_local_wake.prompt_playing = false; + s_local_wake.prompt_wait_started_tick = 0U; + restore_idle_ui = s_local_wake.listening_ui_active; + s_local_wake.listening_ui_active = false; TaskHandle_t task = s_local_wake.task; portEXIT_CRITICAL(&s_local_wake_lock); + if (restore_idle_ui) { + local_wake_request_expression(DOMAIN_EXPRESSION_IDLE); + } if (task != NULL) { xTaskNotifyGive(task); } 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 a3765f6..1118878 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -152,6 +152,7 @@ typedef enum { PTT_STATE_WAIT_WS, PTT_STATE_WAIT_START_SENT, PTT_STATE_WAIT_LISTENING, + PTT_STATE_WAIT_WAKE_PROMPT, PTT_STATE_WAIT_SPEECH_STARTED, PTT_STATE_STREAMING, PTT_STATE_TEARDOWN_LOCAL, @@ -227,6 +228,8 @@ static const char *ptt_state_name(ptt_state_t state) return "WAIT_START_SENT"; case PTT_STATE_WAIT_LISTENING: return "WAIT_LISTENING"; + case PTT_STATE_WAIT_WAKE_PROMPT: + return "WAIT_WAKE_PROMPT"; case PTT_STATE_WAIT_SPEECH_STARTED: return "WAIT_SPEECH_STARTED"; case PTT_STATE_STREAMING: @@ -349,6 +352,7 @@ void control_plane_set_ptt_active(bool active) 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 || + s_ptt_runtime.state == PTT_STATE_WAIT_WAKE_PROMPT || s_ptt_runtime.state == PTT_STATE_WAIT_SPEECH_STARTED || s_ptt_runtime.state == PTT_STATE_DENIED) { control_plane_ptt_transition_locked(PTT_STATE_IDLE, "release_before_streaming"); @@ -368,6 +372,30 @@ void control_plane_set_ptt_active(bool active) } } +void control_plane_ptt_reset_capture_window(void) +{ + bool active = false; + uint32_t generation = 0U; + portENTER_CRITICAL(&s_runtime_lock); + active = s_ptt_runtime.active; + if (active) { + s_ptt_runtime.press_tick = xTaskGetTickCount(); + generation = s_ptt_runtime.generation; + } + portEXIT_CRITICAL(&s_runtime_lock); + + if (!active) { + ESP_LOGW(TAG, "Skip PTT capture window reset: turn is no longer active"); + return; + } + + ESP_LOGI(TAG, + "PTT capture window armed after local wake prompt: generation=%u timeout_ms=%u", + (unsigned)generation, + (unsigned)CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS); + control_plane_notify_wifi_state_task(); +} + bool control_plane_voice_turn_is_idle(void) { bool idle = false; @@ -1218,12 +1246,14 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte } TickType_t now = xTaskGetTickCount(); + bool local_wake_prompt_gate = control_plane_local_wake_prompt_gate_active(); #if CONFIG_TQ_PTT_MODE_CLICK_TO_TALK TickType_t capture_timeout_ticks = pdMS_TO_TICKS(CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS); if (capture_timeout_ticks == 0) { capture_timeout_ticks = 1; } - if (ptt.press_tick != 0 && + if (!local_wake_prompt_gate && + ptt.press_tick != 0 && (now - ptt.press_tick) >= capture_timeout_ticks) { control_plane_set_ptt_active(false); return; @@ -1234,7 +1264,8 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte if (denied_ticks == 0) { denied_ticks = 1; } - if (ptt.press_tick != 0 && + if (!local_wake_prompt_gate && + ptt.press_tick != 0 && (now - ptt.press_tick) >= denied_ticks && ptt.state != PTT_STATE_STREAMING && ptt.state != PTT_STATE_DENIED) { @@ -1287,6 +1318,8 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte control_plane_ptt_set_state(PTT_STATE_WAIT_START_SENT, "send_start"); } else if (!state.server_listening) { control_plane_ptt_set_state(PTT_STATE_WAIT_LISTENING, "wait_listening"); + } else if (local_wake_prompt_gate) { + control_plane_ptt_set_state(PTT_STATE_WAIT_WAKE_PROMPT, "wait_local_wake_prompt"); } else if (!state.client_speech_active) { control_plane_ptt_set_state(PTT_STATE_WAIT_SPEECH_STARTED, "send_speech"); } else if (control_plane_multimodal_is_uplink_running()) { @@ -1296,7 +1329,8 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte } ptt = control_plane_ptt_snapshot(); - if ((ptt.state == PTT_STATE_WAIT_START_SENT || + if (!local_wake_prompt_gate && + (ptt.state == PTT_STATE_WAIT_START_SENT || ptt.state == PTT_STATE_WAIT_LISTENING) && (now - ptt.state_enter_tick) >= pdMS_TO_TICKS(CONTROL_PLANE_PTT_LISTENING_TIMEOUT_MS)) { @@ -1327,6 +1361,9 @@ static void control_plane_drive_ptt_state_machine(TickType_t *last_ws_start_atte if (ptt.state == PTT_STATE_WAIT_SPEECH_STARTED && ptt.speech_action_sent) { should_run_press_step = false; } + if (ptt.state == PTT_STATE_WAIT_WAKE_PROMPT) { + should_run_press_step = false; + } if (!should_run_press_step) { return; } diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index a76a912..cfd0ad5 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -118,6 +118,7 @@ typedef enum { DOMAIN_EXPRESSION_MUYU, DOMAIN_EXPRESSION_CHOUQIAN, DOMAIN_EXPRESSION_WUXING, + DOMAIN_EXPRESSION_LISTENING, } domain_expression_id_t; typedef enum { @@ -132,6 +133,7 @@ typedef enum { DOMAIN_EXPRESSION_TRIGGER_PTT_DOUBLE_CLICK, DOMAIN_EXPRESSION_TRIGGER_SW2_SINGLE_CLICK, DOMAIN_EXPRESSION_TRIGGER_SW2_DOUBLE_CLICK, + DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE, } domain_expression_trigger_t; typedef struct { diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index 7acb13b..a3ea016 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -78,6 +78,7 @@ static const uint8_t s_server_error_frame_order[] = {0, 1, 2, 1, 0}; static const uint8_t s_wifi_disconnected_frame_order[] = {0, 1, 2, 3, 2, 1, 0}; static const uint8_t s_basic_face_frame_order[] = {0, 1, 2}; static const uint8_t s_muyu_frame_order[] = {0, 1, 2, 1}; +static const uint8_t s_listening_frame_order[] = {0, 1, 2}; static const char *const s_chouqian_result_asset_dirs[] = { "chouqian_buyun", "chouqian_heping", @@ -227,6 +228,21 @@ static const domain_expression_catalog_entry_t s_expression_catalog[] = { .duration_ms = 0, }, }, + { + .id = DOMAIN_EXPRESSION_LISTENING, + .platform_request = { + .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, + .priority = 130, + .interrupt_current = true, + .return_to_idle = false, + .asset_dir = "listening", + .frame_order = s_listening_frame_order, + .frame_order_count = sizeof(s_listening_frame_order) / + sizeof(s_listening_frame_order[0]), + .repeat_count = 0, + .frame_ms = 500, + }, + }, }; static void snapshot_domain_wifi_behavior_hooks(domain_wifi_behavior_hooks_t *out_hooks) diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index 8a8e525..4b1a626 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -82,6 +82,9 @@ set(platform_lcd_png_assets jichubiaoqing0 jichubiaoqing1 jichubiaoqing2 + listening0 + listening1 + listening2 muyu0 muyu1 muyu2 diff --git a/components/platform/assets/lcd/listening0.png b/components/platform/assets/lcd/listening0.png new file mode 100644 index 0000000000000000000000000000000000000000..0891c1c7d928dc4dc3c573097e8329424c8819a2 GIT binary patch literal 19615 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 qB?`Y*i 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 Hypj4J#D4i5gu+xYS$(%aLCvBT!A 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 YDkklF(vMG`|EE)x7FQ6f7BLF?Ura>&Q~&?~ literal 0 HcmV?d00001 diff --git a/components/platform/assets/lcd/listening2.png b/components/platform/assets/lcd/listening2.png new file mode 100644 index 0000000000000000000000000000000000000000..0dc38668c1b14acf793603001e1077dce9e57222 GIT binary patch literal 10758 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 F{|%;i1ZV&N literal 0 HcmV?d00001 diff --git a/components/platform/src/lcd_direct_assets.c b/components/platform/src/lcd_direct_assets.c index bfb8ae4..812ca64 100644 --- a/components/platform/src/lcd_direct_assets.c +++ b/components/platform/src/lcd_direct_assets.c @@ -20,6 +20,12 @@ static const lcd_direct_image_t s_basic_face_frames[] = { }, }; +static const lcd_direct_image_t s_listening_frames[] = { + {.path = LCD_ASSET_BASE_PATH "listening0.png"}, + {.path = LCD_ASSET_BASE_PATH "listening1.png"}, + {.path = LCD_ASSET_BASE_PATH "listening2.png"}, +}; + static const lcd_direct_image_t s_muyu_frames[] = { { .path = LCD_ASSET_BASE_PATH "muyu0.png", @@ -236,6 +242,12 @@ const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index); } + if (strcmp(asset_dir, "listening") == 0) { + return lcd_direct_frame_from_array(s_listening_frames, + sizeof(s_listening_frames) / sizeof(s_listening_frames[0]), + frame_index); + } + if (strcmp(asset_dir, "muyu") == 0) { return lcd_direct_frame_from_array(s_muyu_frames, sizeof(s_muyu_frames) / sizeof(s_muyu_frames[0]), diff --git a/components/platform/src/multimodal/audio_pipeline.c b/components/platform/src/multimodal/audio_pipeline.c index cdfed89..83caba1 100644 --- a/components/platform/src/multimodal/audio_pipeline.c +++ b/components/platform/src/multimodal/audio_pipeline.c @@ -344,6 +344,9 @@ typedef struct { volatile bool uplink_recording_active; volatile bool uplink_task_exit_requested; volatile bool uplink_send_in_flight; + bool uplink_first_network_pcm_pending; + bool uplink_first_network_packet_pending; + TickType_t uplink_network_start_tick; multimodal_audio_uplink_cb_t uplink_cb; void *uplink_cb_ctx; bool opus_ready; diff --git a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c index c359902..07a8377 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c @@ -157,6 +157,42 @@ static void audio_set_uplink_callback(multimodal_audio_ctx_t *ctx, portEXIT_CRITICAL(&s_audio_runtime_lock); } +static bool audio_consume_first_network_pcm(multimodal_audio_ctx_t *ctx, + uint32_t *out_elapsed_ms) +{ + bool pending = false; + TickType_t started = 0U; + portENTER_CRITICAL(&s_audio_runtime_lock); + if (ctx != NULL && ctx->uplink_first_network_pcm_pending) { + ctx->uplink_first_network_pcm_pending = false; + started = ctx->uplink_network_start_tick; + pending = true; + } + portEXIT_CRITICAL(&s_audio_runtime_lock); + if (pending && out_elapsed_ms != NULL) { + *out_elapsed_ms = (uint32_t)pdTICKS_TO_MS(xTaskGetTickCount() - started); + } + return pending; +} + +static bool audio_consume_first_network_packet(multimodal_audio_ctx_t *ctx, + uint32_t *out_elapsed_ms) +{ + bool pending = false; + TickType_t started = 0U; + portENTER_CRITICAL(&s_audio_runtime_lock); + if (ctx != NULL && ctx->uplink_first_network_packet_pending) { + ctx->uplink_first_network_packet_pending = false; + started = ctx->uplink_network_start_tick; + pending = true; + } + portEXIT_CRITICAL(&s_audio_runtime_lock); + if (pending && out_elapsed_ms != NULL) { + *out_elapsed_ms = (uint32_t)pdTICKS_TO_MS(xTaskGetTickCount() - started); + } + return pending; +} + static void notify_pcm_observer(const int16_t *pcm, size_t samples) { multimodal_audio_pcm_observer_t observer = NULL; @@ -988,6 +1024,14 @@ static void uplink_send_task(void *arg) ctx->uplink_send_in_flight = true; esp_err_t send_err = cb(packet.data, packet.len, cb_ctx); ctx->uplink_send_in_flight = false; + uint32_t first_packet_elapsed_ms = 0U; + if (send_err == ESP_OK && + audio_consume_first_network_packet(ctx, &first_packet_elapsed_ms)) { + ESP_LOGI(TAG, + "Mic uplink first packet sent: activation_to_send_ms=%u bytes=%u", + (unsigned)first_packet_elapsed_ms, + (unsigned)packet.len); + } track_uplink_send_result(send_err, packet.len); if (send_err != ESP_OK && send_err != ESP_ERR_INVALID_STATE) { if (send_err != ESP_ERR_TIMEOUT) { @@ -1068,6 +1112,13 @@ static void uplink_capture_task(void *arg) continue; } size_t uplink_frames = ctx->uplink_aec_frame_samples; + uint32_t first_pcm_elapsed_ms = 0U; + if (audio_consume_first_network_pcm(ctx, &first_pcm_elapsed_ms)) { + ESP_LOGI(TAG, + "Mic uplink first PCM captured: activation_to_pcm_ms=%u samples=%u", + (unsigned)first_pcm_elapsed_ms, + (unsigned)uplink_frames); + } notify_pcm_observer(ctx->uplink_aec_clean_buf, uplink_frames); audio_debug_dump_pcm(ctx->uplink_aec_clean_buf, clean_bytes); multimodal_audio_uplink_cb_t uplink_cb = NULL; @@ -1410,6 +1461,7 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c } bool already_active = audio_get_uplink_recording_active(&s_audio); + bool network_uplink = cb != NULL; audio_set_uplink_callback(&s_audio, cb, cb_ctx); if (already_active) { return ESP_OK; @@ -1426,6 +1478,11 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c } reset_uplink_send_queue(&s_audio); + portENTER_CRITICAL(&s_audio_runtime_lock); + s_audio.uplink_network_start_tick = xTaskGetTickCount(); + s_audio.uplink_first_network_pcm_pending = network_uplink; + s_audio.uplink_first_network_packet_pending = network_uplink; + portEXIT_CRITICAL(&s_audio_runtime_lock); audio_set_uplink_recording_active(&s_audio, true); TaskHandle_t capture_task = NULL; audio_snapshot_uplink_tasks(&s_audio, &capture_task, NULL); @@ -1433,6 +1490,10 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c xTaskNotifyGive(capture_task); } log_heap_snapshot("after_uplink_start"); + ESP_LOGI(TAG, + "Mic uplink activated: mode=%s sample_rate=%u channels=1", + network_uplink ? "network" : "local_observer_only", + (unsigned)CONFIG_MULTIMODAL_AUDIO_CODEC_SAMPLE_RATE); return ESP_OK; } @@ -1443,6 +1504,10 @@ esp_err_t multimodal_audio_stop_uplink(int timeout_ms) } audio_set_uplink_recording_active(&s_audio, false); + portENTER_CRITICAL(&s_audio_runtime_lock); + s_audio.uplink_first_network_pcm_pending = false; + s_audio.uplink_first_network_packet_pending = false; + portEXIT_CRITICAL(&s_audio_runtime_lock); TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS((uint32_t)timeout_ms); while (((s_audio.uplink_send_queue != NULL && diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index f208574..042a592 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -908,6 +908,16 @@ menu "Example Configuration" default 5000 range 500 60000 + config TQ_LOCAL_WAKE_READY_TIMEOUT_MS + int "Local wake network readiness timeout (ms)" + depends on TQ_LOCAL_WAKE_ENABLE + default 10000 + range 1000 30000 + help + Maximum time to prepare the multimodal transport and reach the + server Listening state before the local wake turn is aborted. + The acknowledgement prompt is played only after readiness. + config TQ_LOCAL_WAKE_STOP_WAIT_MS int "Local wake lifecycle wait (ms)" depends on TQ_LOCAL_WAKE_ENABLE diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 5780c38..db63ba1 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -63,6 +63,7 @@ CONFIG_TQ_LOCAL_WAKE_ENABLE=y CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE="local_wake_response.opus" CONFIG_TQ_LOCAL_WAKE_RETRY_MS=5000 +CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS=10000 CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS=1000 CONFIG_TQ_LOCAL_WAKE_TASK_STACK_BYTES=6144 CONFIG_TQ_LOCAL_WAKE_TASK_PRIORITY=4 diff --git a/tools/check_architecture_guards.sh b/tools/check_architecture_guards.sh index 1d36286..5e3b436 100644 --- a/tools/check_architecture_guards.sh +++ b/tools/check_architecture_guards.sh @@ -50,6 +50,10 @@ fi if ! rg -n 'TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE' \ main/Kconfig.projbuild \ components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 || + ! rg -n 'TQ_LOCAL_WAKE_READY_TIMEOUT_MS' \ + main/Kconfig.projbuild \ + sdkconfig.defaults \ + components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 || ! rg -n 'CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y' \ sdkconfig.defaults >/dev/null 2>&1 || ! rg -n 'local_wake_response\.opus' \ @@ -59,6 +63,21 @@ if ! rg -n 'TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE' \ [[ ! -f components/platform/assets/audio_prompts/local_wake_response.opus ]]; then fail "Local wake response prompt must remain enabled with its packaged asset contract" fi +if ! rg -n 'DOMAIN_EXPRESSION_LISTENING|DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE' \ + components/domain/include/domain.h \ + components/domain/src/runtime/domain_runtime_adapters.c \ + components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 || + ! rg -n 'PTT_STATE_WAIT_WAKE_PROMPT|control_plane_local_wake_prompt_gate_active' \ + components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1 || + ! rg -n '!local_wake_prompt_gate.*&&' \ + components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1 || + ! rg -n '^ listening[012]$' components/platform/CMakeLists.txt >/dev/null 2>&1 || + ! rg -n 'listening[012]\.png' components/platform/src/lcd_direct_assets.c >/dev/null 2>&1 || + [[ ! -f components/platform/assets/lcd/listening0.png ]] || + [[ ! -f components/platform/assets/lcd/listening1.png ]] || + [[ ! -f components/platform/assets/lcd/listening2.png ]]; then + fail "Local wake must show its packaged listening animation while gating speech uplink until the ready prompt finishes" +fi if ! rg -n '#include "esp_afe_aec.h"' components/platform/src/multimodal/audio_pipeline.c >/dev/null 2>&1 || ! rg -n 'afe_aec_create\(' components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c >/dev/null 2>&1 || ! rg -n 'afe_aec_process\(' components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c >/dev/null 2>&1; then