fix(control_plane): preserve VAD generation on repeated PTT activation
This commit is contained in:
@@ -720,6 +720,14 @@ static bool control_plane_deactivate_ptt(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 调用方持有 s_runtime_lock;收尾期间仍允许排队下一轮。 */
|
||||||
|
static bool control_plane_ptt_has_active_turn_locked(void)
|
||||||
|
{
|
||||||
|
return s_ptt_runtime.active &&
|
||||||
|
s_ptt_runtime.state != PTT_STATE_TEARDOWN_LOCAL &&
|
||||||
|
s_ptt_runtime.state != PTT_STATE_TEARDOWN_REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
static void control_plane_set_ptt_active_with_trigger(
|
static void control_plane_set_ptt_active_with_trigger(
|
||||||
bool active,
|
bool active,
|
||||||
domain_expression_trigger_t listening_ui_trigger)
|
domain_expression_trigger_t listening_ui_trigger)
|
||||||
@@ -729,6 +737,14 @@ static void control_plane_set_ptt_active_with_trigger(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portENTER_CRITICAL(&s_runtime_lock);
|
||||||
|
bool duplicate = control_plane_ptt_has_active_turn_locked();
|
||||||
|
portEXIT_CRITICAL(&s_runtime_lock);
|
||||||
|
if (duplicate) {
|
||||||
|
ESP_LOGI(TAG, "PTT activation ignored: turn already active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!control_plane_ota_business_interactions_allowed()) {
|
if (!control_plane_ota_business_interactions_allowed()) {
|
||||||
ESP_LOGI(TAG, "PTT activation ignored during OTA");
|
ESP_LOGI(TAG, "PTT activation ignored during OTA");
|
||||||
return;
|
return;
|
||||||
@@ -746,6 +762,12 @@ static void control_plane_set_ptt_active_with_trigger(
|
|||||||
ptt_state_t new_state = PTT_STATE_IDLE;
|
ptt_state_t new_state = PTT_STATE_IDLE;
|
||||||
uint32_t generation = 0;
|
uint32_t generation = 0;
|
||||||
portENTER_CRITICAL(&s_runtime_lock);
|
portENTER_CRITICAL(&s_runtime_lock);
|
||||||
|
/* 前置检查会让出执行权;提交意图前再次检查,防止并发入口换代。 */
|
||||||
|
if (control_plane_ptt_has_active_turn_locked()) {
|
||||||
|
portEXIT_CRITICAL(&s_runtime_lock);
|
||||||
|
ESP_LOGI(TAG, "PTT activation ignored: turn already active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
old_state = s_ptt_runtime.state;
|
old_state = s_ptt_runtime.state;
|
||||||
s_ws_start_requested = true;
|
s_ws_start_requested = true;
|
||||||
s_ptt_runtime.generation++;
|
s_ptt_runtime.generation++;
|
||||||
@@ -1915,6 +1937,21 @@ static bool control_plane_service_ptt_lifecycle(void)
|
|||||||
if (!local_wake_prompt_gate &&
|
if (!local_wake_prompt_gate &&
|
||||||
ptt.press_tick != 0 &&
|
ptt.press_tick != 0 &&
|
||||||
(now - ptt.press_tick) >= capture_timeout_ticks) {
|
(now - ptt.press_tick) >= capture_timeout_ticks) {
|
||||||
|
#if CONFIG_TQ_SPEECH_ENDPOINT_ENABLE
|
||||||
|
domain_speech_endpoint_snapshot_t endpoint = {0};
|
||||||
|
esp_err_t endpoint_err = domain_speech_endpoint_get_snapshot(&endpoint);
|
||||||
|
if (endpoint_err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "VAD capture timeout: ptt_generation=%u endpoint_generation=%u active=%d speech=%d ended=%d audio_ms=%u silence_ms=%u max_process_us=%u gaps=%u",
|
||||||
|
(unsigned)ptt.generation, (unsigned)endpoint.generation,
|
||||||
|
endpoint.active ? 1 : 0, endpoint.speech_confirmed ? 1 : 0,
|
||||||
|
endpoint.ended ? 1 : 0, (unsigned)endpoint.audio_ms,
|
||||||
|
(unsigned)endpoint.silence_ms, (unsigned)endpoint.max_process_us,
|
||||||
|
(unsigned)endpoint.discontinuities);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "VAD capture timeout snapshot unavailable: %s",
|
||||||
|
esp_err_to_name(endpoint_err));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
(void)control_plane_deactivate_ptt(true,
|
(void)control_plane_deactivate_ptt(true,
|
||||||
ptt.generation,
|
ptt.generation,
|
||||||
"capture_timeout");
|
"capture_timeout");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 本地 VAD 语音端点检测
|
# 本地 VAD 语音端点检测
|
||||||
|
|
||||||
更新:2026-10-15。适用于 lucky-cat;实现所有权见 [ADR 0006](adr/0006-local-speech-endpoint.md)。
|
更新:2026-10-23。适用于 lucky-cat;实现所有权见 [ADR 0006](adr/0006-local-speech-endpoint.md)。
|
||||||
|
|
||||||
## 当前行为
|
## 当前行为
|
||||||
|
|
||||||
@@ -18,6 +18,11 @@
|
|||||||
原窗口起点,可能包含连接等待;本次不改变这一点。持续静音或未达到人声阈值的
|
原窗口起点,可能包含连接等待;本次不改变这一点。持续静音或未达到人声阈值的
|
||||||
短促声音不触发提前结束,继续走原超时。阈值以 20 ms 检测帧向上取整。
|
短促声音不触发提前结束,继续走原超时。阈值以 20 ms 检测帧向上取整。
|
||||||
|
|
||||||
|
等待连接、等待提示音和拾音期间,重复按键/唤醒请求沿用当前轮次,不递增
|
||||||
|
generation、不重置输入窗口,也不重新启用 VAD。收尾期间保留原有下一轮排队机制。
|
||||||
|
当前单击模式关闭长按识别,长按松开也可能产生 `SHORT_PRESS`;该事件同样受上述
|
||||||
|
幂等保护,不能让仍在录音的控制层换代并遗留旧代次 VAD。
|
||||||
|
|
||||||
900 ms 指 VAD 输出连续静音,不等于从物理声音结束到 StopSpeech 的精确时长;
|
900 ms 指 VAD 输出连续静音,不等于从物理声音结束到 StopSpeech 的精确时长;
|
||||||
库内判定延迟、20 ms 分帧、现有 80 ms 控制轮询、调度及尾包发送都会影响时间。
|
库内判定延迟、20 ms 分帧、现有 80 ms 控制轮询、调度及尾包发送都会影响时间。
|
||||||
句中停顿超过阈值可能被截断;慢语速、轻声、儿童、远场及噪声需实机调参。
|
句中停顿超过阈值可能被截断;慢语速、轻声、儿童、远场及噪声需实机调参。
|
||||||
@@ -47,6 +52,11 @@
|
|||||||
- `VAD endpoint`:代次、处理音频时长、连续静音、最大回调处理墙钟时间与间断数。
|
- `VAD endpoint`:代次、处理音频时长、连续静音、最大回调处理墙钟时间与间断数。
|
||||||
- `StopSpeech sent after uplink drain`:正常输入结束指令已发出。
|
- `StopSpeech sent after uplink drain`:正常输入结束指令已发出。
|
||||||
- `VAD unavailable; retain capture timeout`:本轮回退原输入窗口。
|
- `VAD unavailable; retain capture timeout`:本轮回退原输入窗口。
|
||||||
|
- `PTT activation ignored: turn already active`:重复触发已忽略,原代次与超时窗口保留。
|
||||||
|
- `VAD capture timeout`:兜底结束前的一次领域快照,包含 PTT/端点代次、活动状态、
|
||||||
|
是否已确认人声、是否结束、处理音频时长、当前连续静音、最大回调墙钟时间和间断数。
|
||||||
|
`speech=0` 表示尚未确认连续人声;`audio_ms` 很小或 `gaps` 较多可提示输入问题。
|
||||||
|
这是瞬时状态,不能据此还原整段声学分类;查询失败只记录错误,继续原超时收尾。
|
||||||
- `Abort voice turn after incomplete drain`:失败废弃,不能记作成功回复。
|
- `Abort voice turn after incomplete drain`:失败废弃,不能记作成功回复。
|
||||||
|
|
||||||
固定 PCM 重分帧数组为 640 B,另有领域状态/互斥锁和平台第二个观察者槽、
|
固定 PCM 重分帧数组为 640 B,另有领域状态/互斥锁和平台第二个观察者槽、
|
||||||
@@ -60,13 +70,16 @@ VAD 库内部动态分配、原采集栈高水位和联网后的最小内部 RAM
|
|||||||
|
|
||||||
`python3 tools/test_speech_endpoint.py` 编译并执行生产 C,使用可控 VAD/RTOS 桩:
|
`python3 tools/test_speech_endpoint.py` 编译并执行生产 C,使用可控 VAD/RTOS 桩:
|
||||||
覆盖分帧、纯静音、短噪声、句中停顿、代次、输入间断、初始化失败、注销超时、
|
覆盖分帧、纯静音、短噪声、句中停顿、代次、输入间断、初始化失败、注销超时、
|
||||||
订阅并发替换、队列出队与 busy 竞态、原采集上下文尾帧补零、排空与失败重试。
|
订阅并发替换、队列出队与 busy 竞态、原采集上下文尾帧补零、排空与失败重试;
|
||||||
|
控制层直接执行生产激活函数,覆盖各等待阶段/拾音阶段重复触发、原端点继续生效、
|
||||||
|
超时不续期、收尾排队、前置检查期间并发触发和快照查询失败后的兜底。
|
||||||
这些测试验证控制逻辑,不验证真实声学识别率或 ESP32 时序。
|
这些测试验证控制逻辑,不验证真实声学识别率或 ESP32 时序。
|
||||||
|
|
||||||
实机至少检查:
|
实机至少检查:
|
||||||
|
|
||||||
1. 按键和唤醒各测试短句、长句、未说话、轻声、噪声和不同长度句中停顿。
|
1. 按键和唤醒各测试短句、长句、未说话、轻声、噪声和不同长度句中停顿。
|
||||||
2. 连续多轮、快速取消/重新触发、服务端提前结束、TTS 播放及唤醒提示音门禁。
|
2. 连续多轮、拾音中重复单击/长按松开、快速取消/重新触发、服务端提前结束、
|
||||||
|
TTS 播放及唤醒提示音门禁。重复触发应保持代次和原窗口,收尾后新一轮仍可正常启动。
|
||||||
3. 拾音期间断网、OTA/待机切换、发送拥塞、内存不足与重复初始化。
|
3. 拾音期间断网、OTA/待机切换、发送拥塞、内存不足与重复初始化。
|
||||||
4. 记录最后人声 → endpoint → StopSpeech → 首个下行音频 → 实际播放的分段延迟;
|
4. 记录最后人声 → endpoint → StopSpeech → 首个下行音频 → 实际播放的分段延迟;
|
||||||
同时统计误截断、未判停、队列丢包、回调耗时、栈高水位和内部 RAM 最低余量。
|
同时统计误截断、未判停、队列丢包、回调耗时、栈高水位和内部 RAM 最低余量。
|
||||||
@@ -88,3 +101,17 @@ VAD 库内部动态分配、原采集栈高水位和联网后的最小内部 RAM
|
|||||||
处失败,已对照 HEAD 确认不是本次引入。仅排除该既有检查后,其余架构检查通过;
|
处失败,已对照 HEAD 确认不是本次引入。仅排除该既有检查后,其余架构检查通过;
|
||||||
本次新增路径无无限等待或跨层内部头依赖。`git diff --check` 通过。
|
本次新增路径无无限等待或跨层内部头依赖。`git diff --check` 通过。
|
||||||
- 设备:未烧录,尚无实机误截断率、延迟收益、VAD 动态堆和栈峰值数据。
|
- 设备:未烧录,尚无实机误截断率、延迟收益、VAD 动态堆和栈峰值数据。
|
||||||
|
|
||||||
|
### 2026-10-23 重复激活修复验证
|
||||||
|
|
||||||
|
- 修复:控制层在前置检查前及提交意图的临界区内检查活动轮次,忽略重复激活;
|
||||||
|
保持领域端点的代次隔离,不替换正在使用的 VAD。新增超时诊断只在兜底时记录。
|
||||||
|
- 主机:重新运行 `test_speech_endpoint.py` 全部通过,包含 VAD 开启/关闭的领域和
|
||||||
|
控制层 C 分支;上述五个显示回归脚本也全部通过。
|
||||||
|
- 构建:激活 ESP-IDF 5.5.4 后执行 `idf.py build` 成功,使用当前 VAD 开启配置;
|
||||||
|
本次未执行关闭配置的固件构建。应用镜像 3,168,368 B,单个 OTA 槽剩余 1,550,224 B。
|
||||||
|
本次不新增任务、队列、堆分配或固定音频/DMA 缓冲。
|
||||||
|
- 架构:原守卫仍停在电量模块既有的 `portMAX_DELAY`,已对照 HEAD 确认;仅排除
|
||||||
|
该既有检查后,其余守卫通过。`git diff --check` 通过。
|
||||||
|
- 设备:未烧录。首次录音未提前断句的声学原因仍需新日志和实机复测;本次主机测试
|
||||||
|
与构建不能证明真实噪声环境下的人声/静音识别效果。
|
||||||
|
|||||||
@@ -272,11 +272,12 @@ CP_TEST = r"""
|
|||||||
static ptt_runtime_t ptt;
|
static ptt_runtime_t ptt;
|
||||||
static domain_speech_endpoint_snapshot_t endpoint;
|
static domain_speech_endpoint_snapshot_t endpoint;
|
||||||
static bool gate, change_generation;
|
static bool gate, change_generation;
|
||||||
static int deactivated;
|
static int deactivated, snapshot_error;
|
||||||
static const char *last_reason;
|
static const char *last_reason;
|
||||||
static ptt_runtime_t control_plane_ptt_snapshot(void) {return ptt;}
|
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_stop(void) {endpoint.active=false;return 0;}
|
||||||
static int domain_speech_endpoint_get_snapshot(domain_speech_endpoint_snapshot_t *out) {
|
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;
|
*out=endpoint; if(change_generation) {++ptt.generation;change_generation=false;} return 0;
|
||||||
}
|
}
|
||||||
static bool control_plane_local_wake_prompt_gate_active(void) {return gate;}
|
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"""
|
CP_MAIN = r"""
|
||||||
int main(void) {
|
int main(void) {
|
||||||
|
test_activation();
|
||||||
ticks=100; ptt=(ptt_runtime_t){.active=true,.state=PTT_STATE_STREAMING,.generation=7,.press_tick=1};
|
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};
|
endpoint=(domain_speech_endpoint_snapshot_t){.active=true,.ended=true,.generation=6};
|
||||||
assert(!control_plane_service_ptt_lifecycle() && deactivated==0);
|
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;
|
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()); ticks=8001;
|
||||||
assert(control_plane_service_ptt_lifecycle() && deactivated==2 && !strcmp(last_reason,"capture_timeout"));
|
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;
|
ptt=(ptt_runtime_t){.state=PTT_STATE_IDLE}; endpoint.active=true;
|
||||||
assert(!control_plane_service_ptt_lifecycle() && !endpoint.active);
|
assert(!control_plane_service_ptt_lifecycle() && !endpoint.active);
|
||||||
puts("PASS: actual control-plane C stale-generation rejection, prompt gate, concurrent cancel and 8s fallback");
|
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"""
|
PENDING_TEST = r"""
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
typedef struct {bool uplink_send_in_flight;void *uplink_send_queue;} multimodal_audio_ctx_t;
|
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)
|
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.
|
# 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_')
|
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)
|
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.
|
# Integration guards supplement C execution; they do not replace device timing tests.
|
||||||
|
|||||||
Reference in New Issue
Block a user