feat(voice): gate local wake prompt on network readiness

This commit is contained in:
chenjintao
2026-08-12 09:47:55 +08:00
parent 004400fee7
commit 7a4699ea68
15 changed files with 323 additions and 62 deletions
@@ -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);
}
@@ -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;
}