fix(control_plane): preserve VAD generation on repeated PTT activation

This commit is contained in:
chenjintao
2026-10-23 14:09:34 +08:00
parent f17ce857ff
commit efd7aac623
3 changed files with 169 additions and 5 deletions
+102 -2
View File
@@ -272,11 +272,12 @@ CP_TEST = r"""
static ptt_runtime_t ptt;
static domain_speech_endpoint_snapshot_t endpoint;
static bool gate, change_generation;
static int deactivated;
static int deactivated, snapshot_error;
static const char *last_reason;
static ptt_runtime_t control_plane_ptt_snapshot(void) {return ptt;}
static int domain_speech_endpoint_stop(void) {endpoint.active=false;return 0;}
static int domain_speech_endpoint_get_snapshot(domain_speech_endpoint_snapshot_t *out) {
if(snapshot_error) return snapshot_error;
*out=endpoint; if(change_generation) {++ptt.generation;change_generation=false;} return 0;
}
static bool control_plane_local_wake_prompt_gate_active(void) {return gate;}
@@ -288,6 +289,7 @@ static void control_plane_ptt_set_state(ptt_state_t state,const char *why) {(voi
"""
CP_MAIN = r"""
int main(void) {
test_activation();
ticks=100; ptt=(ptt_runtime_t){.active=true,.state=PTT_STATE_STREAMING,.generation=7,.press_tick=1};
endpoint=(domain_speech_endpoint_snapshot_t){.active=true,.ended=true,.generation=6};
assert(!control_plane_service_ptt_lifecycle() && deactivated==0);
@@ -298,11 +300,93 @@ int main(void) {
ptt.active=true; ptt.state=PTT_STATE_STREAMING; endpoint.active=false;
assert(!control_plane_service_ptt_lifecycle()); ticks=8001;
assert(control_plane_service_ptt_lifecycle() && deactivated==2 && !strcmp(last_reason,"capture_timeout"));
ptt.active=true; snapshot_error=ESP_ERR_TIMEOUT;
assert(control_plane_service_ptt_lifecycle() && deactivated==3 && !strcmp(last_reason,"capture_timeout"));
snapshot_error=0;
ptt=(ptt_runtime_t){.state=PTT_STATE_IDLE}; endpoint.active=true;
assert(!control_plane_service_ptt_lifecycle() && !endpoint.active);
puts("PASS: actual control-plane C stale-generation rejection, prompt gate, concurrent cancel and 8s fallback");
}
"""
CP_ACTIVATION_STUB = r"""
#define s_ptt_runtime ptt
#define CONTROL_PLANE_NETWORK_ACTION_CONVERSATION 1
static int s_runtime_lock, pauses, resumes, expressions, notifications, readiness_calls;
static bool s_ws_start_requested, ota_allowed=true;
static esp_err_t readiness_error;
static void (*readiness_hook)(void);
static bool control_plane_ota_business_interactions_allowed(void) {return ota_allowed;}
static void control_plane_local_wake_pause_for_foreground(void) {++pauses;}
static void control_plane_local_wake_resume_after_foreground(void) {++resumes;}
static esp_err_t control_plane_network_feedback_require_ready(int action) {
(void)action; ++readiness_calls; if(readiness_hook) readiness_hook(); return readiness_error;
}
static void control_plane_ptt_transition_locked(ptt_state_t state,const char *reason) {
(void)reason; ptt.state=state; ptt.state_enter_tick=ticks;
}
static void control_plane_ptt_log_transition(ptt_state_t a,ptt_state_t b,uint32_t gen,const char *why) {
(void)a;(void)b;(void)gen;(void)why;
}
static void control_plane_request_ptt_expression(domain_expression_id_t id,domain_expression_trigger_t trigger) {
(void)id;(void)trigger;++expressions;
}
static void control_plane_notify_wifi_state_task(void) {++notifications;}
"""
CP_ACTIVATION_TEST = r"""
static void concurrent_activation(void) {
readiness_hook=NULL;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE);
}
static void test_activation(void) {
const ptt_state_t stages[]={PTT_STATE_WAIT_WIFI,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};
ticks=100; ptt=(ptt_runtime_t){.state=PTT_STATE_IDLE};
ota_allowed=false;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
assert(!ptt.active && !pauses && !readiness_calls);
ota_allowed=true; readiness_error=ESP_ERR_INVALID_STATE;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
assert(!ptt.active && resumes==1);
readiness_error=ESP_OK;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
assert(ptt.active && ptt.generation==1 && ptt.press_tick==100 && expressions==1);
assert(ptt.state==PTT_STATE_WAIT_WIFI && s_ws_start_requested && notifications==1);
for(unsigned i=0;i<sizeof(stages)/sizeof(stages[0]);++i) {
ptt.state=stages[i]; ptt.endpoint_start_attempted=true;
ptt.start_action_sent=true; ptt.speech_action_sent=true;
ptt_runtime_t before=ptt;
int previous_pauses=pauses, previous_ready=readiness_calls;
ticks+=1000;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE);
assert(!memcmp(&before,&ptt,sizeof(ptt)));
assert(pauses==previous_pauses && readiness_calls==previous_ready && expressions==1);
}
/* 重复触发后仍接收原代次端点;窗口也不能被续期。 */
endpoint=(domain_speech_endpoint_snapshot_t){.active=true,.ended=true,.generation=ptt.generation};
assert(control_plane_service_ptt_lifecycle() && !strcmp(last_reason,"vad_endpoint"));
ptt=(ptt_runtime_t){.active=true,.state=PTT_STATE_STREAMING,.generation=9,.press_tick=100};
endpoint=(domain_speech_endpoint_snapshot_t){0}; ticks=8099;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
assert(!control_plane_service_ptt_lifecycle()); ++ticks;
assert(control_plane_service_ptt_lifecycle() && !strcmp(last_reason,"capture_timeout"));
const ptt_state_t teardown[]={PTT_STATE_TEARDOWN_LOCAL,PTT_STATE_TEARDOWN_REMOTE};
for(unsigned i=0;i<2;++i) {
ptt=(ptt_runtime_t){.state=teardown[i],.generation=10};
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
assert(ptt.active && ptt.generation==11 && ptt.pending_press_after_teardown);
assert(ptt.state==teardown[i] && !ptt.endpoint_start_attempted);
}
ptt=(ptt_runtime_t){.state=PTT_STATE_IDLE,.generation=20};
readiness_hook=concurrent_activation;
int previous_expressions=expressions;
control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
assert(ptt.generation==21 && expressions==previous_expressions+1);
assert(ptt.listening_ui_trigger==DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE);
deactivated=0;
puts("PASS: actual PTT activation C ignores repeats, preserves endpoint/window, queues teardown and rejects concurrent duplicate");
}
"""
PENDING_TEST = r"""
#include "common.h"
typedef struct {bool uplink_send_in_flight;void *uplink_send_queue;} multimodal_audio_ctx_t;
@@ -362,7 +446,23 @@ puts("PASS: disabled domain C fallback"); }
cp_types=re.search(r'typedef enum \{\s+PTT_STATE_IDLE.*?} ptt_runtime_t;',cp_source,re.S).group(0)
# Hide facade declarations only for test doubles; production function is unchanged.
cp_test=CP_TEST.replace('static int domain_speech_endpoint_', 'int domain_speech_endpoint_')
compile_run(tmp, 'control_plane', CP_STUB + cp_types + cp_test + extract(cp_source, 'control_plane_service_ptt_lifecycle') + CP_MAIN)
cp_lifecycle=extract(cp_source, 'control_plane_service_ptt_lifecycle')
cp_activation='\n'.join(extract(cp_source, n) for n in [
'control_plane_ptt_has_active_turn_locked', 'control_plane_set_ptt_active_with_trigger'])
compile_run(tmp, 'control_plane', CP_STUB + cp_types + cp_test + CP_ACTIVATION_STUB +
cp_activation + cp_lifecycle + CP_ACTIVATION_TEST + CP_MAIN)
compile_run(tmp, 'control_plane_disabled', CP_STUB.replace(
'#define CONFIG_TQ_SPEECH_ENDPOINT_ENABLE 1', '#define CONFIG_TQ_SPEECH_ENDPOINT_ENABLE 0') +
cp_types + cp_test + CP_ACTIVATION_STUB + cp_activation + cp_lifecycle + r'''
int main(void) {
ticks=100; control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON);
ptt.state=PTT_STATE_STREAMING;
ticks=8100; control_plane_set_ptt_active_with_trigger(true,DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE);
assert(ptt.generation==1 && ptt.press_tick==100);
assert(control_plane_service_ptt_lifecycle() && !strcmp(last_reason,"capture_timeout"));
puts("PASS: VAD disabled control-plane C preserves generation and 8s fallback on repeated activation");
}
''')
compile_run(tmp, 'stop', STOP_STUB + '\n'.join(extract(AUDIO, n) for n in ['audio_uplink_tail_pending', 'audio_flush_uplink_tail', 'multimodal_audio_stop_uplink']) + STOP_START_STUB + extract(AUDIO, 'multimodal_audio_start_uplink') + STOP_TEST)
# Integration guards supplement C execution; they do not replace device timing tests.