提交修改又的交互规范需求,设计等相关文档
authorSnickerSun <snickersun@163.com>
Sun, 17 May 2026 15:21:14 +0000 (23:21 +0800)
committerSnickerSun <snickersun@163.com>
Sun, 17 May 2026 15:21:14 +0000 (23:21 +0800)
长对话ok,单对话显示还是有异常。需要优化实现逻辑。

1.doc/AI吊坠(双按键+三色指示灯)交互规范.md
1.doc/AI吊坠(双按键+三色指示灯)交互规范_new.md
1.doc/AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md [new file with mode: 0644]
1.doc/AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md [new file with mode: 0644]
1.doc/AI吊坠(双按键+三色指示灯)详设-对照与决策.md [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/ai_VC.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/versions.py

index 883afa7..0435ba5 100644 (file)
     
 *   避免复杂操作,核心功能(唤醒、开关机)仅需1种按键动作,老人、儿童可快速上手
     
-*   误触防护:短按Power键无操作、双击AI键仅在待机/播报后有效,减少误操作带来的困扰
\ No newline at end of file
+*   误触防护:待机短按 Power 无多余操作;单/长对话模式切换仅 Power 键双击,AI 键无双击业务
\ No newline at end of file
index 9a1b7af..a646477 100644 (file)
@@ -7,7 +7,7 @@
 - 按键:Power键(开关键)、AI键(语音交互键)
 - 指示灯:红(R)、蓝(B)、绿(G)三色(遵循定义:绿=正常/电量/充满电;蓝=AI交互;红=低电/异常)
 - 提示音:采用短促、清晰的单音/双音,区分不同操作,音量适中(避免刺耳,适配贴身佩戴),支持后续可调节(可选)
-- 操作逻辑:长按=特殊功能(开关机、强制重启、连续对话);短按/双击=常用功能(唤醒、打断、复读),杜绝误触
+- 操作逻辑:长按=特殊功能(开关机、强制重启、连续对话);短按=常用功能(唤醒、打断);**模式切换仅 Power 键双击**,杜绝误触
 
 ### LED 灯效与时间术语(与工程 `ai_VC.AI_led` 对齐)
 
 | **慢交替/交替慢闪** | 约 **1 s 换一色**   | `1000`               |
 
 
-**工程映射(默认)**:聆听蓝 **慢闪**、低电红 **慢闪**、待机绿 **慢闪** → `1000`;思考蓝绿 **交替** → `500`;弱网蓝绿 **慢交替** → `1000`。正文若写「快闪 / 2 次/秒」等,表示周期短于「闪」,工程可按场景单独设 `period_ms`(例如 `**250`** 约 0.5 s 一亮灭,便于对齐「2 次/秒」计数),与上表不冲突。
+**工程映射(默认)**:聆听蓝 **慢闪**、低电红 **慢闪**、正常待机 **灭灯**(`LED_IDLE_MODE=off`)→ 聆听/低电 `1000`;思考蓝绿 **交替** → `500`;弱网蓝绿 **慢交替** → `1000`;异常红蓝快闪 → `250`(约 2 次/秒)。正文若写「快闪 / 2 次/秒」等,表示周期短于「闪」,工程可按场景单独设 `period_ms`,与上表不冲突。
 
 ## 备注
 
-休眠模式:无灯光,无提示音(最低电量消耗)
-
-聆听模式:蓝灯闪(1 分钟内无交互,自动进入待机模式)
-
-思考模式:蓝绿交替闪
-
-播放模式:绿灯闪
-
-待机模式:绿色慢闪(2 分钟内无交互,自动进入休眠模式)
+| REQ-ID | 说明 |
+| ------ | --- |
+| REQ-NOTE-03 | 休眠模式:无灯光,无提示音(最低电量消耗) |
+| REQ-NOTE-02 | 聆听模式:蓝灯闪(1 分钟内无交互,自动进入待机模式) |
+| REQ-NOTE-02 | 思考模式:蓝绿交替闪 |
+| REQ-NOTE-02 | 播放模式:绿灯闪 |
+| REQ-NOTE-01 | 正常待机(part3):指示灯灭;2 分钟无交互进入休眠(REQ-NOTE-03)。旧工程绿灯慢闪可通过 `LED_IDLE_MODE=green_blink` 恢复 |
 
 进入聆听模式:单声“滴”(短促)
 
 ## 第一部分:基础操作(Power键)+ 灯光+提示音联动
 
 
-| 操作动作          | 设备当前状态           | 指示灯状态                   | 提示音                 | 执行结果                                          |
-| ------------- | ---------------- | ----------------------- | ------------------- | --------------------------------------------- |
-| Power键 长按3秒   | 关机状态             | 绿色灯常亮(持续3秒,同步长按时间),然后熄灭 | 现有设备开机提示音(后续视需求可修改) | 设备开机,进入正常待机状态                                 |
-| Power键 长按8秒   | 任何状态(死机/卡死/异常)   | 无变化                     | 无提示音                | 设备关机,切断所有电源                                   |
-| Power键 短按(单击) | 待机状态             | 无指示灯                    | 无提示音                | 进入休眠模式                                        |
-| Power键 短按(单击) | AI交互中(聆听/处理/TTS) | 绿色慢闪                    | 无提示音                | 停止对话,进入待机模式                                   |
-| Power键 短按(双击) | 待机状态(任意状态)       | 无变化                     | 播报:单对话模式/长对话模式      | 如果对话时停止对话,设置对话模式,进入待机模式, 非对话模式,设置对话模式直接进入待机模式 |
+| REQ-ID | 操作动作          | 设备当前状态           | 指示灯状态                   | 提示音                 | 执行结果                                          |
+| ------ | ------------- | ---------------- | ----------------------- | ------------------- | --------------------------------------------- |
+| REQ-PWR-01 | Power键 长按3秒   | 关机状态             | 绿色灯常亮(持续3秒,同步长按时间),然后熄灭 | 现有设备开机提示音(后续视需求可修改) | 设备开机,进入正常待机状态                                 |
+| REQ-PWR-02 | Power键 长按8秒   | 任何状态(死机/卡死/异常)   | 无变化                     | 无提示音                | 设备关机,切断所有电源                                   |
+| REQ-PWR-03 | Power键 短按(单击) | 待机状态             | 无指示灯                    | 无提示音                | 进入休眠模式                                        |
+| REQ-PWR-04 | Power键 短按(单击) | AI交互中(聆听/处理/TTS) | 绿色慢闪                    | 无提示音                | 停止对话,进入待机模式                                   |
+| REQ-PWR-05 | Power键 短按(双击) | 待机状态(任意状态)       | 无变化                     | 播报:单对话模式/长对话模式      | 如果对话时停止对话,设置对话模式,进入待机模式, 非对话模式,设置对话模式直接进入待机模式 |
 
 
 ## 第二部分:AI交互操作(AI键)+ 灯光+提示音联动
 
 
-| 操作动作                              | 对话模式  | 设备当前状态              | 指示灯状态               | 提示音                | 执行结果                                    |
-| --------------------------------- | ----- | ------------------- | ------------------- | ------------------ | --------------------------------------- |
-| AI键 短按1次                          | 长对话模式 | 待机状态                | 立即切换为蓝色慢闪(1次/秒)     | 单声“滴”(短促,清脆)       | 唤醒AI,进入语音聆听状态,开始收音(用户可说话)               |
-| AI键 短按1次                          | 长对话模式 | 语音聆听中(蓝色慢闪)         | 切换为蓝色慢闪             | 单声“滴”(短促)          | 重新进入语音聆听状态,开始收音(用户可说话)                  |
-| AI键 短按1次                          | 长对话模式 | TTS播报中(绿色常亮灯)       | 切换为蓝色慢闪             | 单声“滴”(短促)          | 进入语音聆听状态,开始收音(用户可说话)                    |
-| AI键 短按1次                          | 长对话模式 | 思考中(蓝绿交替闪)          | 切换为蓝色慢闪             | 单声“滴”(短促)          | 进入语音聆听状态,开始收音(用户可说话)                    |
-| AI键 短按1次                          | 单对话模式 | TTS播报中(绿色常亮)        | 灯灭→idle             | 无(或极短停播)           | **仅停播**,不开录;续聊须再 **长按**(见附录 E5.3)       |
-| AI键 短按1次                          | 单对话模式 | 待机/思考/聆听(非TTS播放)    | 无变化(短按吞掉)           | 无                  | 无业务;开录须 **长按**(见附录 E5)                  |
-| AI键 双击(两次短按,间隔≤1秒)-> 可选(根据可实现性而定) |       | 待机状态 & TTS播报结束后1分钟内 | 蓝色灯常亮(与正常TTS播报灯光一致) | “滴、滴”(2声短促,间隔0.5秒) | 复读上一次AI的TTS回复内容,复读结束后回到待机状态(蓝色灯灭)       |
-| AI键 双击>>暂时无功能                     |       |                     |                     |                    |                                         |
-| AI键 长按(按下)                        | 单对话模式 | 待机状态/思考状态/播报状态      | 蓝色快闪(频率加快,2次/秒)     | 开录提示音(start.mp3)   | 进入收音状态;TTS中重开见附录 E5.2                   |
-| AI键 长按(松开)                        | 单对话模式 | 聆听中(蓝快闪)            | 蓝绿交替闪               | “层”(接入拾音/思考提示)     | 结束上行,进入思考;协议 `audio.completed`(附录 E5.6) |
+| REQ-ID | 操作动作                      | 对话模式         | 设备当前状态           | 指示灯状态           | 提示音              | 执行结果                                    |
+| ------ | ------------------------- | ------------ | ---------------- | --------------- | ---------------- | --------------------------------------- |
+| REQ-AI-01 | AI键 短按1次                  | 长对话模式        | 待机状态             | 立即切换为蓝色慢闪(1次/秒) | 单声“滴”(短促,清脆)     | 唤醒AI,进入语音聆听状态,开始收音(用户可说话)               |
+| REQ-AI-02 | AI键 短按1次                  | 长对话模式        | 语音聆听中(蓝色慢闪)      | 切换为蓝色慢闪         | 单声“滴”(短促)        | 重新进入语音聆听状态,开始收音(用户可说话)                  |
+| REQ-AI-03 | AI键 短按1次                  | 长对话模式        | TTS播报中(绿色常亮灯)    | 切换为蓝色慢闪         | 单声“滴”(短促)        | 进入语音聆听状态,开始收音(用户可说话)                    |
+| REQ-AI-04 | AI键 短按1次                  | 长对话模式        | 思考中(蓝绿交替闪)       | 切换为蓝色慢闪         | 单声“滴”(短促)        | 进入语音聆听状态,开始收音(用户可说话)                    |
+| REQ-AI-05 | AI键 短按1次                  | 单对话模式        | TTS播报中(绿色常亮)     | 灯灭→idle         | 无(或极短停播)         | **仅停播**,不开录;续聊须再 **长按**(见 [DES-SCN-020.3](AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md))       |
+| REQ-AI-06 | AI键 短按1次                  | 单对话模式        | 待机/思考/聆听(非TTS播放) | 无变化(短按吞掉)       | 无                | 无业务;开录须 **长按**(见 [DES-SCN-020](AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md))                  |
+| REQ-AI-07 | AI键 长按(按下)                | 单对话模式        | 待机状态/思考状态/播报状态   | 蓝色快闪(频率加快,2次/秒) | 开录提示音(start.mp3) | 进入收音状态;TTS中重开见 [DES-SCN-020.2](AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)                   |
+| REQ-AI-08 | AI键 长按(松开)                | 单对话模式        | 聆听中(蓝快闪)         | 蓝绿交替闪           | “层”(接入拾音/思考提示)   | 结束上行,进入思考;协议 `audio.completed`(见 [DES-SCN-020.6](AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)) |
 
 
 ## 第三部分:电量/充电状态 + 灯光+提示音联动
 
 
-| 设备状态              | 触发条件            | 指示灯状态                   | 提示音                 | 补充说明                     |
-| ----------------- | --------------- | ----------------------- | ------------------- | ------------------------ |
-| 正常待机(电量≥30%)      | 开机后无操作、交互结束后    | 指示灯灭(AI交互过程遵循如上第二部分的定义) | 无提示音                |                          |
-| 低电量提醒(10%≤电量<30%) | 电量降至阈值、每5分钟触发1次 | 红色慢闪(1次/1秒),每次闪3次       | “滴、滴”(2声短促,与灯光同步)   | 提醒用户尽快充电,不频繁打扰           |
-| 电量耗尽(电量<10%)      | 电量降至关机阈值        | 红色快闪3次(2次/秒)→ 熄灭        | “滴、滴、滴”(3声短促,与灯光同步) | 快闪结束后设备自动关机              |
-| 充电中(电量<100%)      | 连接充电器,接触正常      | 红色常亮                    | 单声“滴”(短促)           | 充电期间,按键操作仍有效(开关机、AI交互)   |
-| 充电满(电量=100%)      | 充电至满电阈值         | 红色常亮→切换为绿色常亮(延迟1秒)      | “滴——”(长音,持续1秒)      | 绿色常亮保持,提醒用户可拔下充电器(晚上不打扰) |
+| REQ-ID | 设备状态              | 触发条件            | 指示灯状态                   | 提示音                 | 补充说明                     |
+| ------ | ----------------- | --------------- | ----------------------- | ------------------- | ------------------------ |
+| REQ-BAT-01 | 正常待机(电量≥30%)      | 开机后无操作、交互结束后    | 指示灯灭(AI交互过程遵循如上第二部分的定义) | 无提示音                |                          |
+| REQ-BAT-02 | 低电量提醒(10%≤电量<30%) | 电量降至阈值、每5分钟触发1次 | 红色慢闪(1次/1秒),每次闪3次       | “滴、滴”(2声短促,与灯光同步)   | 提醒用户尽快充电,不频繁打扰           |
+| REQ-BAT-03 | 电量耗尽(电量<10%)      | 电量降至关机阈值        | 红色快闪3次(2次/秒)→ 熄灭        | “滴、滴、滴”(3声短促,与灯光同步) | 快闪结束后设备自动关机              |
+| REQ-BAT-04 | 充电中(电量<100%)      | 连接充电器,接触正常      | 红色常亮                    | 单声“滴”(短促)           | 充电期间,按键操作仍有效(开关机、AI交互)   |
+| REQ-BAT-05 | 充电满(电量=100%)      | 充电至满电阈值         | 红色常亮→切换为绿色常亮(延迟1秒)      | “滴——”(长音,持续1秒)      | 绿色常亮保持,提醒用户可拔下充电器(晚上不打扰) |
 
 
 ## 第四部分:AI交互异常状态 + 灯光+提示音联动
 
 
-| 异常场景             | 指示灯状态                    | 提示音                 | 处理方式                 |
-| ---------------- | ------------------------ | ------------------- | -------------------- |
-| 语音识别失败(未听清/无法识别) | 红蓝快闪2次(2次/秒)→ 回到待机状态(灯灭) | “滴、滴”(2声短促,与灯光同步)   | 用户可再次短按AI键,重新唤醒聆听    |
-| 网络异常(无法请求AI)     | 蓝绿交替慢闪(1次/2秒),持续闪烁       | “网络异常信号弱”(每5秒1次,循环) | 检查网络连接,恢复网络后自动回到待机状态 |
-| AI处理超时           | 蓝色常亮→红蓝快闪2次→回到待机状态(灯灭)   | “滴、滴”(2声短促)         | 处理失败,用户可重新发起语音请求     |
+| REQ-ID | 异常场景             | 指示灯状态                    | 提示音                 | 处理方式                 |
+| ------ | ---------------- | ------------------------ | ------------------- | -------------------- |
+| REQ-ERR-01 | 语音识别失败(未听清/无法识别) | 红蓝快闪2次(2次/秒)→ 回到待机状态(灯灭) | “滴、滴”(2声短促,与灯光同步)   | 用户可再次短按AI键,重新唤醒聆听    |
+| REQ-ERR-02 | 网络异常(无法请求AI)     | 蓝绿交替慢闪(1次/2秒),持续闪烁       | “网络异常信号弱”(每5秒1次,循环) | 检查网络连接,恢复网络后自动回到待机状态 |
+| REQ-ERR-03 | AI处理超时           | 蓝色常亮→红蓝快闪2次→回到待机状态(灯灭)   | “滴、滴”(2声短促)         | 处理失败,用户可重新发起语音请求     |
 
 
 ## 第五部分:补充规范(工程&用户体验)
 
 - 所有操作均有“灯光+提示音”双重反馈,无屏状态下也能清晰判断设备状态
 - 避免复杂操作,核心功能(唤醒、开关机)仅需1种按键动作,老人、儿童可快速上手
-- 误触防护:短按Power键无操作、双击AI键仅在待机/播报后有效,减少误操作带来的困扰
-
----
-
-## 附录 A:灯效编程对照表(`ai_VC.AI_led`)
-
-灯由 `EventMesh.publish("set_led_state", { ... })` 聚合驱动;**优先级自上而下**,先命中先显示,后面的输入仍写入内部状态但不生效,直到高优先级条件解除。
-
-
-| 优先级 | 输入字段       | 取值(示例)                | 内部 `_mode`    | 灯效(R/G/B) | `period_ms`(节拍)                      | 对应术语    | 说明                   |
-| --- | ---------- | --------------------- | ------------- | --------- | ------------------------------------ | ------- | -------------------- |
-| P1  | `ai_state` | `playing`             | `green_on`    | 绿常亮       | —                                    | —       | TTS/本地播报中            |
-| P2  | `ai_state` | `thinking`            | `bg_alt`      | 蓝↔绿交替     | `500`                                | **交替**  | 约 0.5 s 换一色          |
-| P3  | `ai_state` | `listening`           | `blue_blink`  | 蓝亮灭       | `1000`                               | **慢闪**  | 约 2 s 一亮灭;整颗 GPIO 亮灭 |
-| B1  | `charging` | `full`                | `green_on`    | 绿常亮       | —                                    | —       | USB 在位且电压达充满阈值       |
-| B2  | `charging` | `charging`            | `red_on`      | 红常亮       | —                                    | —       | USB 在位且未充满           |
-| B3  | `battery`  | `low` / `empty`       | `red_blink`   | 红亮灭       | `1000`                               | **慢闪**  | 非充电时按 SoC/OCV 判定     |
-| B4  | `signal`   | `weak`                | `bg_alt`      | 蓝↔绿交替     | `1000`                               | **慢交替** | CSQ 低于阈值             |
-| 默认  | (上述均未命中)   | `ai_state` 一般为 `idle` | `green_blink` | 绿亮灭       | `LED_IDLE_GREEN_BLINK_MS`(默认 `1000`) | **慢闪**  | 待机/空闲                |
-
-
-`**set_led_state` 消息字典键**
-
-
-| 键          | 类型  | 含义                                            |
-| ---------- | --- | --------------------------------------------- |
-| `ai_state` | str | `idle` / `listening` / `thinking` / `playing` |
-| `charging` | str | `none` / `charging` / `full`                  |
-| `battery`  | str | `normal` / `low` / `empty`                    |
-| `signal`   | str | `normal` / `weak`                             |
-
-
-**可配置项(`default_config.py` / `vc_config`)**
-
-
-| 配置名                                     | 作用                                      |
-| --------------------------------------- | --------------------------------------- |
-| `LED_GPIO`                              | (R, G, B) 引脚名                           |
-| `LED_ACTIVE_LOW` / `LED_ACTIVE_LOW_RGB` | 点亮电平反相                                  |
-| `LED_IDLE_GREEN_BLINK_MS`               | 待机绿灯**慢闪**半周期(ms),默认 `1000`,代码侧最小 `400` |
-
-
-**与正文规范的差异(便于联调对齐)**
-
-
-| 正文描述                    | 当前工程实现要点                                                                                                                                       |
-| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| 待机灯灭 / Power 短按待机无灯     | 代码待机为 **绿灯慢闪**(见上表默认行);与正文第三 part「正常待机灯灭」不一致时以代码为准或后续再改表                                                                                       |
-| 休眠无灯光                   | 休眠路径在 `ui.py`(`pm.autosleep` 等);LED 是否全灭取决于休眠前是否再发 `set_led_state`                                                                             |
-| Power 长按 8s 关机无灯 / 无提示音 | 正文第一 part 为「无变化、无提示音」;若需与关机动画/提示音一致,由电源键模块与产品再定稿                                                                                               |
-| 备注「蓝灯闪 / 绿灯闪」与附录节拍      | 工程侧聆听为 **慢闪**(`1000`)、播放为 **绿常亮**;与口语「闪」字面不完全相同时,以文首 **术语表** 与附录 A 为准                                                                          |
-| 闪 / 慢闪 / 交替 / 慢交替       | 与文首 **「LED 灯效与时间术语」** 及上表一致;`listening` / 低电 / 待机为 **慢闪**(`1000`),`thinking` 为 **交替**(`500`),弱网为 **慢交替**(`1000`);若正文写「1 次/秒」亮灭见术语 **闪**(`500`) |
-
-
----
-
-## 附录 B:对话模式与状态机(`ai_VC.py`)
-
-> **源码对齐**:下列 FSM、`VC_TtsRxPhase`、`[FSM]`/`[KEY]` 日志字段等与 `2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py` 一致。仓库内若另有 `project/*/sdk/.../ai_VC.py` 等副本,可能尚未合并同一套 `VCStateMachine`,**以当前固件实际编译进镜像的文件为准**。
-
-### B1 对话模式 `VC_mode`(存储键 `VC_mode`)
-
-
-| 值   | 常量名(代码)             | 含义       | 协议侧要点                                                   |
-| --- | ------------------- | -------- | ------------------------------------------------------- |
-| `0` | `VCMode.Realtime`   | 长对话(连续)  | VAD 开,一般 **无需** `audio.create` / `audio.completed` 包裹上行 |
-| `1` | `VCMode.PushToTalk` | 单对话(PTT) | VAD 关,**必须** `audio.create` → 帧 → `audio.completed`     |
-
-
-### B2 会话层 `VCSessionState`(`AI_VC.fsm.session`)
-
-
-| 值   | 名称             | 含义                   |
-| --- | -------------- | -------------------- |
-| `0` | `Disconnected` | 未连接 / 已断开            |
-| `1` | `Connecting`   | 连接中                  |
-| `2` | `Starting`     | 已发 Start,等 `started` |
-| `3` | `Started`      | 会话已建立,可上行/收事件        |
-
-
-### B3 轮次层 `VCTurnState`(`AI_VC.fsm.turn`)
-
-
-| 值   | 名称                | 含义(简述)                   | 当前 `VCStateMachine` 赋值情况                                                  |
-| --- | ----------------- | ------------------------ | ------------------------------------------------------------------------- |
-| `0` | `Idle`            | 本轮空闲                     | 会写入(会话开始/断开等)                                                             |
-| `1` | `Listening`       | 聆听 / 等用户说话(长对话侧常见)       | 会写入(`started` / `canceled` / `audio.played` 后等)                           |
-| `2` | `SpeechStreaming` | 语音上行中(流式)                | **枚举保留**;上行是否打开见 `fsm.speech_uplink_open`(`asr.started` 置 1)              |
-| `3` | `SpeechEnded`     | 本轮说话结束(等 ASR/LLM)        | 会写入(`vad.silence` 等)                                                      |
-| `4` | `Thinking`        | LLM 思考中                  | 会写入(`llm.created`;`llm.completed` 兜底补位)                                   |
-| `5` | `TtsReceiving`    | 正在收 TTS 音频流              | 会写入(`tts.started` / `local.synthesis_sent`)                               |
-| `6` | `Playing`         | 本地正在播 TTS                | 会写入(`local.play_start`)                                                   |
-| `7` | `AwaitPlayedAck`  | 已播完,等 `audio.played` 等门控 | **枚举保留**;当前以 `fsm.await_played` 布尔门控为主,`turn` 多在 `Playing`/`TtsReceiving` |
-
-
-### B3.1 TTS 接收相 `VC_TtsRxPhase`(`AI_VC.fsm.tts_rx_phase`)
-
-与 `tts.started` / `tts.completed` 对齐,用于区分服务端下发阶段(本地可能仍在播)。
-
-
-| 值   | 名称                | 含义                          |
-| --- | ----------------- | --------------------------- |
-| `0` | `Idle`            | 无 TTS 接收相活动                 |
-| `1` | `Started`         | 已收到 `tts.started`           |
-| `2` | `ServerCompleted` | 已收到 `tts.completed`(本地未必播完) |
-
-
-### B4 旧版 `VC_State_`* 整型(已移除,仅作历史对照)
-
-源码注释:**「原 `VC_State_`* 整型已移除;会话/轮次见 `VCTurnState` / `VCStateMachine`(含 `tts_rx_phase`、`speech_uplink_open`)。」** 下列宏名**不再出现在运行逻辑中**,仅便于对照旧日志或旧文档。
-
-
-| 宏名(历史)                                  | 值    | 含义      |
-| --------------------------------------- | ---- | ------- |
-| `VC_State_None`                         | 0    | 无       |
-| `VC_State_Listening`                    | 1    | 听       |
-| `VC_State_SpeechStart`                  | 2    | 说开始     |
-| `VC_State_SpeechIng`                    | 3    | 正在说     |
-| `VC_State_SpeechEnd`                    | 4    | 说结束     |
-| `VC_State_RequestToRespond`             | 5    | 请求回复    |
-| `VC_State_Thinking`                     | 6    | 思考      |
-| `VC_State_Responding`                   | 7    | 响应/播放相关 |
-| `VC_State_TTSStart` … `VC_State_TTSEnd` | 8–11 | TTS 子阶段 |
-| `VC_State_TTSError`                     | 12   | TTS 异常  |
-| `VC_State_DialogError`                  | 13   | 对话异常    |
-| `VC_State_End`                          | 14   | 结束      |
-
-
-### B5 灯与 FSM 的对应关系(编程时)
-
-
-| `set_led_state` 的 `ai_state` | 典型触发场景(与 FSM/业务相关)                           |
-| ---------------------------- | -------------------------------------------- |
-| `listening`                  | 开麦聆听、长对话续聊等                                  |
-| `thinking`                   | `llm.created` / 思考阶段灯                        |
-| `playing`                    | `tts.started` 后本地 PCM 播放、`audio_cb` 播放起止等    |
-| `idle`                       | 交互结束、休眠前、或硬件态仅刷新 `charging/battery/signal` 时 |
-
-
-> **调试**:`EventMesh.publish("get_led_debug_state")` 返回当前聚合 `{ ai, charging, battery, signal, mode, period_ms }`,与串口 `[KEY]` / `[FSM]` 日志对照。
-
-### B6 状态机层级与典型跳转(实现速查)
-
-- **两层模型**:`session`(WebSocket 会话生命周期)与 `turn`(单轮「听→说→想→播」)。`**[FSM]` 单行日志**(`VCStateMachine.log`)形如:`[FSM] <reason> session=Started turn=Listening await_played=0 tts_rx=0 speech_up=0`(其中 `tts_rx` 为 `VC_TtsRxPhase` 数值,`speech_up` 为 `speech_uplink_open`)。
-- `**[KEY]` 行内片段**(`_vc_fsm_log_snip`):`sess=<int> turn=<int> tts_rx=<int> await=<0|1> sp_up=<0|1>`,与上式含义一致。
-- **协议约束**(代码注释摘要):对齐《金三服务-语音对话 V2.5》——收到 `started` 后才允许发音频帧;VAD 开时长对话无需 `audio.create`/`completed`;VAD 关时 PTT 必须 `audio.create`→帧→`audio.completed`;`audio.played` 为进入下一轮的门控之一。
-- **可选严格协议行为**:模块级开关 `VC_PROTO_GUARDS`(默认 `0`)控制 started 前 PCM 缓冲/丢弃、PTT 下 `audio.create` 前禁发 PCM、`llm.completed` 后久无 `tts.started` 的会话兜底等;与产品灯效无直接耦合,联调协议时可关注。
-- **详细边条件**:以 `ai_VC.py` 中 `VCStateMachine` 各 `on_server_`* / `on_local_*` 及事件适配 `on_server_event` 为准;下图为**阅读级**总览(与当前赋值路径对齐:`Listening` 可直接到 `SpeechEnded`),非穷举所有分支。
-
-```mermaid
-stateDiagram-v2
-    direction LR
-    state "会话 session" as S {
-        [*] --> Disconnected
-        Disconnected --> Connecting : websocket_connect
-        Connecting --> Starting : 发 Start
-        Starting --> Started : rx started
-        Started --> Disconnected : Close / 断链
-    }
-    state "轮次 turn(Started 内)" as T {
-        [*] --> Idle
-        Idle --> Listening : 开麦/续聊
-        Listening --> SpeechEnded : vad.silence 等
-        SpeechEnded --> Thinking : llm.created / completed 兜底
-        Thinking --> TtsReceiving : tts.started
-        TtsReceiving --> Playing : local.play_start
-        Playing --> Listening : audio.played 后 on_local_audio_played
-        Listening --> Idle : cancel / Close
-    }
-```
-
-
-
-`**VC_mode` 与 FSM 关系(简述)**
-
-
-| `VC_mode` | 对 `turn` 的影响要点                                                                                |
-| --------- | --------------------------------------------------------------------------------------------- |
-| `0` 长对话   | 易在 `Listening` 与上行、`canceled` 自动续听等路径间切换;详见 PTT 与长对话分支                                        |
-| `1` 单对话   | 依赖 `audio.create` / `audio.completed`;长按 `ptt_begin`:本地停播 →(忙态)`cancel` → `audio.create` 重开上行 |
-
-
----
-
-## 附录 C:充电 / 电量 / 信号(硬件态)与 LED 同步(`AI_hw_status` + `AI_led`)
-
-硬件侧由 `**AI_hw_status._update()`** 周期性(默认 `HW_STATUS_PERIOD_MS`,如 10000 ms)及 `**hw_usb_state` 事件** 刷新,并 **一次下发**:
-
-`EventMesh.publish("set_led_state", {"charging": chg, "battery": bat, "signal": sig})`
-
-仅更新这三个字段;`**ai_state` 仍由上一条 VC/业务逻辑保留**,最终灯效由 `**AI_led._apply_by_table` 优先级**决定:**任意非 `idle` 的 `ai_state`(listening/thinking/playing)都会盖住充电/电量/弱网灯**。
-
-### C1 采样输入 → 内部枚举(工程判定)
-
-
-| USB 在位 (`GET_USB_STATE` / 事件) | 电池电压 `vbatt_mv`(默认算法)                                   | `charging` | `battery`(仅 **USB 不在位** 时按 SoC 算)    |
-| ----------------------------- | ------------------------------------------------------- | ---------- | ------------------------------------ |
-| 否                             | SoC ≥ 5% 且非低电档                                          | `none`     | `normal`                             |
-| 否                             | SoC < 5%                                                | `none`     | `empty`                              |
-| 否                             | 5% ≤ SoC < 20%(工程档,非正文 10%/30%)                         | `none`     | `low`                                |
-| 是                             | < 充满阈值(默认 **≥4290 mV** 视为满,可 `VBATT_CHARGE_FULL_MV` 覆盖) | `charging` | 不按 SoC 刷低电(充电中不走路径 B3)               |
-| 是                             | ≥ 充满阈值                                                  | `full`     | 不参与 SoC 低电判定,`battery` 仍为默认 `normal` |
-
-
-**说明**:USB 在位时 `**battery` 不按电压刷新**(代码仅在 `usb_status != 1` 时根据 SoC 置 `low`/`empty`),灯效以 `**charging` 优先**(见附录 C3)。
-
-**SoC 来源**:默认 **OCV 电压表插值**(`_ocv_map`);若 `USE_SIMPLE_VBATT_THRESH=True`,则按 `VBATT_CHARGE_FULL_MV` / `VBATT_LOW_MV` / `VBATT_EMPTY_MV` 三档近似。
-
-### C2 信号弱网
-
-
-| 条件                                                                      | `signal` |
-| ----------------------------------------------------------------------- | -------- |
-| `net.csqQueryPoll()` 得到的 RSSI 存在且 **0 ≤ RSSI < SIGNAL_WEAK_CSQ**(默认 10) | `weak`   |
-| 否则                                                                      | `normal` |
-
-
-### C3 枚举 → `AI_led` 灯效(且 `**ai_state == idle`** 时生效)
-
-
-| `charging` | `battery`       | `signal` | 内部 `_mode`  | 灯效              | 节拍与术语                               |
-| ---------- | --------------- | -------- | ----------- | --------------- | ----------------------------------- |
-| `full`     | *               | *        | `green_on`  | 绿常亮             | —                                   |
-| `charging` | *               | *        | `red_on`    | 红常亮             | —                                   |
-| `none`     | `low` / `empty` | *        | `red_blink` | 红**慢闪**         | `period_ms=1000`(约 2 s 一亮灭)         |
-| `none`     | `normal`        | `weak`   | `bg_alt`    | 蓝绿**慢交替**       | `period_ms=1000`(约 1 s 换一色)         |
-| `none`     | `normal`        | `normal` | —           | 附录 A 默认:**绿慢闪** | `LED_IDLE_GREEN_BLINK_MS`,默认 `1000` |
-
-
-### C4 正文「第三部分」与工程灯效对照(含 AI 覆盖)
-
-
-| 正文设备状态      | 正文指示灯  | 工程基础态(`ai_state=idle`)                         | **AI 交互中**(`listening`/`thinking`/`playing`) |
-| ----------- | ------ | ---------------------------------------------- | -------------------------------------------- |
-| 正常待机(≥30%)  | 灯灭     | **绿灯慢闪**(见附录 A 差异说明)                           | 以 AI 灯为准(蓝慢闪 / 蓝绿交替 / 绿常亮)                   |
-| 低电量 10%~30% | 红慢闪等   | `battery=low` → `red_blink`(阈值实为 SoC<20%)      | AI 优先:例如 **播放中仍为绿常亮**,低电红慢闪不显示               |
-| 电量耗尽 <10%   | 红快闪→灭  | `battery=empty` → `red_blink`(SoC<5%);关机另由电源策略 | 同上,AI 可盖住                                    |
-| 充电中 <100%   | 红常亮    | `charging=charging` → `red_on`                 | AI 优先:如 **聆听蓝慢闪** 盖住充电红                      |
-| 充电满         | 红→绿常亮等 | `charging=full` → `green_on`                   | 与 **TTS `playing` 绿常亮** 同色,语义需靠场景区分          |
-
-
----
-
-## 附录 D:异常状态(正文第四部分)与 LED 同步(工程现状)
-
-当前 `**AI_led*`* 无单独「红蓝快闪」「网络异常专用慢闪」等 `_mode`;异常分支多为 **TTS / `websocket_error` / `Close` + `ai_state: idle`**,灯效回到 **硬件态表(附录 C3)** 或 **待机绿灯慢闪**。
-
-
-| 正文异常场景        | 正文指示灯          | 工程侧典型处理                                                          | 灯效(概括)                                                                  |
-| ------------- | -------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------- |
-| 语音识别失败(未听清)   | 红蓝快闪 2 次 → 待机灭 | `asr.result` 空串时置 `AI_TTS_str="我没听清…"`,走后续 TTS/状态机;**未专设红蓝闪灯**   | 播提示 TTS 时多为 `**playing`→绿常亮**;结束变 `**idle`** 后接 **附录 C3**(弱电则红慢闪,否则绿慢闪) |
-| 网络异常(无法请求 AI) | 蓝绿慢交替 + 语音循环   | `websocket_error` 等:`Close`、`set_led_state({"ai_state":"idle"})` | `**idle` + 硬件态**:弱网则 `**bg_alt`(慢交替)**;否则 **绿慢闪**;无「持续网络异常」专用灯型         |
-| AI 处理超时       | 蓝常亮 → 红蓝快闪 → 灭 | 依赖超时/兜底分支,**无独立灯型表项**                                            | 过程中可能 `**thinking`(交替)** 或 `**idle`**;未实现规范中的红蓝快闪序列                     |
-
-
-### D1 与附录 A 的编程关系(异常联调)
-
-1. 需要 **「异常专用灯」**:应在业务点增加 `**set_led_state` 新键**(如 `alert`)并在 `**AI_led._apply_by_table`** 中插入优先级;当前未做。
-2. 仅依赖 **现有键**:异常后务必 `**ai_state: idle`** 才会重新露出 **充电/电量/信号** 灯;否则用户会一直看到 **AI 阶段灯**。
-3. `**websocket_error`** 内已 `**set_led_state({"ai_state":"idle"})`**,可与 `**get_led_debug_state`** 联调确认 `charging/battery/signal` 是否已刷新。
-
----
-
-## 附录 E:AI 对话处理逻辑(单对话 vs 长对话)
-
-> **与正文关系**:第二部分表格描述用户可感知的「按键→灯→音→结果」;本附录描述 **协议与本地执行顺序**,供固件(`ai_VC.py`)联调与评审。实现以当前编译进镜像的 `ai_VC.py` 为准。  
-> **模式存储**:`VC_mode`(`0`=长对话 / `1`=单对话),Power 键双击可切换并 TTS 播报模式名(见正文第一部分)。
-
-### E0 典型场景速查(产品语言)
-
-**单对话 · 场景 1:TTS 播报中,AI 键重新开始对话(长按)**
-
-
-| 步骤  | 要求                                               |
-| --- | ------------------------------------------------ |
-| 1   | **本地立即停播**,清空 `AI_audio_queue` 与 codec 缓冲,杜绝残余尾音 |
-| 2   | 发 **停止当前轮** 消息(`cancel`,服务端停止生成;**不换会话**)        |
-| 3   | 发 **继续对话** 消息(`audio.create`)                    |
-| 4   | 打开录音 → 播 **开录提示音**(start.mp3)→ 开始上传 PCM          |
-| 5   | 松手发 `audio.completed`,进入思考 → TTS 播放              |
-
-
-**长对话 · 对照**:TTS 播报中 **短按** AI 键续聊 → 本地清队列 + `cancel` → 收 `canceled` 后 **直接开麦**(无需 `audio.create`)。详见 **E4.2**、**E6**。
-
-**场景索引**
-
-
-| 模式  | 场景          | 章节        |
-| --- | ----------- | --------- |
-| 长对话 | TTS 中短按打断续聊 | E4.2      |
-| 长对话 | 聆听中短按重开麦    | E4.3      |
-| 长对话 | 思考中短按打断     | E4.4      |
-| 长对话 | 待机唤醒        | E4.5      |
-| 长对话 | VAD 自动多轮    | E4.7      |
-| 单对话 | TTS 中长按重开   | E5.2      |
-| 单对话 | TTS 中短按仅停播  | E5.3      |
-| 单对话 | 待机/思考长按开录   | E5.4      |
-| 单对话 | 标准一轮 PTT    | E5.6      |
-| 单对话 | 播完后再问       | E5.7      |
-| 通用  | 完整状态矩阵      | E4.6、E5.8 |
-| 通用  | 服务端事件对照     | E10       |
-| 可选  | AI 键双击复读    | E9        |
-
-
-### E1 模式总览
-
-
-| 维度        | 长对话 `VC_mode=0`(Realtime)                    | 单对话 `VC_mode=1`(Push-To-Talk / PTT)                                    |
-| --------- | -------------------------------------------- | ---------------------------------------------------------------------- |
-| 用户操作      | **短按** AI 键唤醒 / 打断 / 续聊                      | **长按** 开录、**松开** 结束本轮上行                                                |
-| VAD       | 开(服务端判停)                                     | 关(`disable_vad=true`)                                                  |
-| 上行包裹      | 一般 **无需** `audio.create` / `audio.completed` | **必须** `audio.create` → PCM 帧 → `audio.completed`                      |
-| 打断 TTS 续聊 | 短按:发 `**cancel`** + 本地清队列 → 等 `canceled` 后开麦 | **长按**:本地停播清队列 →(TTS/思考态)发 `**cancel`**(不换 `sid`)→ `audio.create` 开新一轮 |
-| 典型灯效      | 聆听蓝慢闪 / 思考蓝绿交替 / 播放绿常亮                       | 长按聆听蓝快闪;松手后思考蓝绿交替                                                      |
-| 进入下一轮门控   | `audio.played`(播完上报)、`canceled` 后自动开麦等       | 松手 `audio.completed` 后等 ASR/LLM/TTS;再次长按走 `ptt_begin`                  |
-
-
-**设计目标**:用户在任何中间态(TTS 播报、思考、弱网恢复中等)按 AI 键,应 **尽快** 进入「下一轮可说话」或「继续当前会话内的新一轮上行」,且 **无残余 TTS 尾音、无播放队列叠录**。
-
-### E2 协议消息与「停止 / 继续」语义
-
-
-| 产品用语          | WebSocket `event`              | 适用模式                   | 作用                                                       |
-| ------------- | ------------------------------ | ---------------------- | -------------------------------------------------------- |
-| 停止当前轮次 / 打断播报 | `cancel`                       | **长对话**(短按打断 TTS、思考中等) | 通知服务端撤销当前响应;端侧随后收 `canceled`,再开麦                         |
-| 本地停止播放(非协议)   | —(`VC_init` + `VC_Play_End`)   | **单对话、长对话**            | 立即停止 PCM/流播放,**清空 `AI_audio_queue`**,必要时 `aud.stopAll()` |
-| 继续对话 / 开新一轮上行 | 长对话:开麦上行 PCM(无 `audio.create`) | 长对话                    | `request_uplink_start` → `VC_Speech_Start_now`           |
-| 继续对话 / 开新一轮上行 | 单对话:`audio.create`             | 单对话                    | `AI_client_VC_send_speech` → 录音 → 帧上行                    |
-| 本轮说话结束        | `audio.completed`              | 单对话                    | 松手触发 `AI_client_VC_end_speech`                           |
-| 本地 TTS 播完     | `audio.played`                 | 长对话为主                  | 播完后上报,服务端进入下一轮 ASR(门控之一)                                 |
-
-
-> **「停止对话」与 `cancel` 的分工**:`cancel` 仅撤销**当前轮**服务端响应(LLM/TTS 生成),**不断开 WebSocket、不换 `sid`**;与 Power 键 `Close`(换会话)区分。单对话在 TTS/思考态 **长按重开** 时也会发 `cancel`,避免旧轮下行与新一轮 `audio.create` 竞态;须 **先本地停播清队列**(用户侧立即静音),再发 `cancel`,再发 `audio.create`。
-
-### E3 本地停播与清缓存(凡打断 TTS 必做)
-
-无论哪种模式,只要用户打断 **TTS/本地播报**,须在同一按键处理链内完成(顺序不可颠倒):
-
-
-| 步骤  | 动作                                             | 说明                                             |
-| --- | ---------------------------------------------- | ---------------------------------------------- |
-| L1  | `VC_Play_End()` / `VC_init()`                  | 停播放流、`VC_Play_flag=0`;`AI_audio_queue.clear()` |
-| L2  | `aud.stopPlayStream()` + `aud.stopAll()`       | 避免 DMA/codec 缓冲残留                              |
-| L3  | `_drop_tts_audio = 1`(可选窗口)                    | 打断后短时 **丢弃** 服务端仍下发的 TTS 帧,防「断断续续继续播」          |
-| L4  | `fsm.await_played = false`(若曾等 `audio.played`) | 避免门控卡死,无法开麦                                    |
-| L5  | 灯 `ai_state: idle` 或 `listening`               | 打断瞬间可先 `idle`;开录后切 `listening`                 |
-
-
-TTS 自然播完:队列读空后 `VC_Play_End`;若本轮需门控,再在适当时机发 `audio.played`。
-
-### E4 长对话(`VC_mode=0`)— AI 键处理
-
-#### E4.1 状态 → 动作
-
-
-| 设备状态(用户视角)  | FSM 近似                          | AI 键 **短按**   | 协议 / 本地顺序                                                                     |
-| ----------- | ------------------------------- | ------------- | ----------------------------------------------------------------------------- |
-| 待机、未建会话     | `session≠Started`               | 唤醒            | `AI_VC_start` → `Start` → 等 `started` → 开麦 → 蓝慢闪 + 开录提示                       |
-| 聆听中         | `turn=Listening`                | 重新聆听          | `request_uplink_start("key.press/continue")` → 清僵死录音态 → `VC_Speech_Start_now` |
-| 思考中         | `turn=Thinking`                 | 打断并开麦         | 见 **E4.4**(宜 `cancel`;管线忙时同 E4.2)                                             |
-| **TTS 播报中** | `turn=Playing` / `TtsReceiving` | **打断并续聊**     | 见 **E4.2**                                                                    |
-| 弱网 / 异常     | `signal=weak` 等                 | 同待机或 Close 策略 | 以异常附录 D 为准                                                                    |
-
-
-#### E4.2 场景:TTS 播报中短按 — 打断并进入下一轮聆听
-
-**目标**:尽快停播、通知服务端停止当前响应、开麦。
-
-```text
-用户短按 AI 键
-  → (1) is_ruppt_tts = 1
-  → (2) VC_init(立即停播 + 清空 AI_audio_queue)
-  → (3) 发 cancel(RequestToSpeak)
-  → (4) 等 server.canceled
-  → (5) FSM: uplink_start(canceled) → VC_Speech_Start_now
-  → (6) 蓝慢闪 + 开录提示音(start.mp3 等)+ 上行 PCM(VAD,无 audio.create)
-```
-
-**灯与音**:发 `cancel` 前后本地已无 TTS;开录时蓝慢闪 + 「滴」类开录提示(与正文第二部分一致)。  
-**注意**:`Close_flag` 须在 `RequestToSpeak` 路径清零,否则 `canceled` 后 `_uplink_request_start` 会拒开麦。
-
-#### E4.3 场景:会话已建立、聆听中短按 — 重新开麦
-
-**用户意图**:上一轮 VAD 已判停或仍在聆听,希望 **再说一段**。
-
-```text
-用户短按 AI 键(session=Started,turn=Listening/SpeechEnded,非 TTS 忙)
-  → request_uplink_start("key.press/continue")
-  → _vc_clear_stale_record_state(若 record_flag 僵死)
-  → VC_Speech_Start_now
-  → 蓝慢闪 + 开录提示音 + PCM 上行(VAD,无 audio.create)
-```
-
-不发 `cancel`(当前轮无服务端 TTS/播放占用)。
-
-#### E4.4 场景:思考中短按 — 打断并开麦
-
-**用户意图**(正文第二部分):不等 LLM/TTS 完成,直接说话。
-
-```text
-用户短按 AI 键(turn=Thinking 或 TTS 管线忙)
-  → 若 _vc_tts_pipeline_busy()(含 _tts_pending / tts_rx / Playing 等):
-       同 E4.2:VC_init → cancel → canceled → uplink_start
-  → 否则(纯 Thinking、管线不忙):
-       产品应 cancel 打断;当前实现可能仅 request_uplink_start,
-       若 turn≠Listening/SpeechEnded 会被门控拒绝(串口 Deny START by key.press/continue)
-       → 联调时建议统一走 RequestToSpeak
-```
-
-**灯与音**:打断成功后蓝慢闪 + 开录提示。
-
-#### E4.5 场景:待机 / 未建会话短按 — 唤醒
-
-```text
-用户短按 AI 键(未连接或未 started)
-  → 落入 AI_key_press 兜底:AI_VC_start → Start → 等 started
-  → started 后:request_uplink_start("server.started/idle")
-  → 蓝慢闪 + 开录提示 + VAD 上行
-```
-
-**注意**:此路径含 `utime.sleep_ms(500)` 与历史 `Close` 兼容逻辑,与「已连接续聊」分支不同;联调以 `[KEY]`、`[VC][uplink_ctrl]` 为准。
-
-#### E4.6 长对话 — 完整状态矩阵(AI 键短按)
-
-
-| 设备状态(用户视角) | FSM 近似                   | 短按动作       | 协议要点                                 | 灯效    |
-| ---------- | ------------------------ | ---------- | ------------------------------------ | ----- |
-| 待机、未建会话    | `session≠Started`        | 唤醒开麦       | `Start` → `started` → 上行 PCM         | 蓝慢闪   |
-| 聆听中        | `Listening`              | 重开麦        | `uplink_start`,无 `cancel`            | 蓝慢闪   |
-| 用户说完等 ASR  | `SpeechEnded`            | 可重开麦       | `uplink_start`(若门控允许)                | 蓝慢闪   |
-| 思考中        | `Thinking`               | 打断开麦       | **宜** `cancel`→`canceled`→开麦(见 E4.4) | 蓝慢闪   |
-| TTS 接收/播报  | `TtsReceiving`/`Playing` | 打断续聊       | E4.2:`cancel` + 清队列                  | 蓝慢闪   |
-| TTS 自然播完   | → `audio.played`         | 无需按键       | 自动 `uplink_start`(长对话)               | 蓝慢闪   |
-| 弱网         | `signal=weak`            | 同唤醒或 Close | 附录 D                                 | 蓝绿慢交替 |
-
-
-#### E4.7 长对话 — 无按键自动轮次(VAD 闭环)
-
-```text
-开麦聆听(Listening)
-  → 用户说话,PCM 上行
-  → 服务端 vad.silence → uplink_stop,turn=SpeechEnded
-  → llm.created → Thinking(蓝绿交替)
-  → tts.started → TtsReceiving → local.play_start → Playing(绿常亮)
-  → 本地播完 → audio.played(若 _need_audio_played_ack)
-  → on_local_audio_played → Listening,可自动 uplink_start 进入下一轮
-```
-
-**门控**:`await_played` 为真时,禁止 `completed`/`canceled` 抢跑开麦;`Close_flag=1` 时拒绝 `canceled`/`audio.played` 触发的自动开麦。
-
-### E5 单对话(`VC_mode=1`)— AI 键处理
-
-#### E5.1 操作分工
-
-
-| 操作         | 作用                   | 说明                                                                                         |
-| ---------- | -------------------- | ------------------------------------------------------------------------------------------ |
-| **短按**     | 仅在 **TTS/播放管线忙** 时有效 | 仅 **本地** `VC_Play_End` + 清队列;**不** 开录、**不** 发 `audio.create`;非播放态短按无业务(由 `ptt_begin` 承担开录) |
-| **长按(按下)** | 开始 / **重新开始** 一轮对话   | 走 `ptt_begin`(`AI_key_long_press`)                                                         |
-| **长按(松开)** | 结束本轮上行               | `AI_client_VC_end_speech`(`audio.completed`)→ 思考灯 + 「层」提示音                                 |
-
-
-#### E5.2 场景 1:TTS 播报中 **长按** — 重新开始一轮对话(核心示例)
-
-**用户意图**:不等 TTS 播完,尽快进入下一轮「按住说话」或继续当前会话内的新一轮上行。
-
-**产品要求(执行顺序不可颠倒)**:
-
-1. **先本地停播并清空播放缓存**(用户侧须立即静音,防止残余 TTS 尾音)。
-2. **再发停止当前轮次消息**(WebSocket `cancel`,仅当服务端仍在 TTS/思考等中间态)。
-3. **再发继续对话协议**(`audio.create`,不换 `sid`)。
-4. **打开录音** → **播开录提示音** → **上行 PCM 帧**(`audio.create` 与开麦顺序受 `VC_PROTO_GUARDS` 约束)。
-
-**固件链路(`ptt_begin`,与 `ai_VC.py` 对齐)**:
-
-```text
-用户长按 AI 键(ptt_begin)
-  → (1) Close_flag = 0;_drop_tts_audio = 1
-  → (2) VC_init()                         // 立即停播 + 清空 AI_audio_queue(VC_Play_End)
-  → (3) 若仍在播:再次 VC_Play_End()       // 兜底 codec/DMA 缓冲
-  → (4) 若上一轮录音未结束:VC_Speech_End()
-  → (5) 若 turn∈{Thinking,TtsReceiving,Playing} 或 TTS 管线忙:
-         发 cancel(停止当前轮服务端响应,不换 sid)
-  → (6) 若 WebSocket 未连接 / 未 started:
-         AI_VC_start 或 Start;_ptt_force_record_on_started,等 started 后自动 (7)~(11)
-      否则(会话已 started):
-  → (7) 发 audio.create(AI_client_VC_send_speech)   // 继续对话 / 新一轮上行
-  → (8) VC_Speech_Start_now()                         // 开硬件录音
-  → (9) 开录提示音(RECORD_START_TONE_FILE / start.mp3,由 VC_Speech_Start 路径播放)
-  → (10) 蓝灯聆听态(长按按下:蓝快闪,见正文第二部分)
-  → (11) 上行 PCM 帧(须在 audio.create 之后)
-```
-
-**为何本地停播优先于 `cancel`**:用户按下 AI 键后 **≤100ms** 内应听不到旧 TTS;若先发 `cancel` 再等本地清队列,DMA/队列中已缓冲的 PCM 仍会播出。本地 `VC_init` 后再 `cancel`,可同时满足「马上静音」与「服务端停止生成」。
-
-**与产品描述的对应**:
-
-
-| 产品描述          | 单对话实现                                                     |
-| ------------- | --------------------------------------------------------- |
-| 「停止对话消息」      | TTS/思考态:`**cancel`**;并 **必先** 完成 E3 本地 L1~L3              |
-| TTS 立即停止或自然结束 | **以 L1~L2 立即停播为准**;无新数据后播放线程退出;`_drop_tts_audio` 丢弃迟到的下行帧 |
-| 「继续对话协议」      | `**audio.create`**(保持当前 `sid` / `dialog_id`)              |
-| 打开录音          | `VC_Speech_Start_now`                                     |
-| 开录提示音         | `RECORD_START_TONE_FILE` / start.mp3                      |
-| 上传录音          | 录音线程 → WS 发 PCM 帧 → 松手发 `audio.completed`                 |
-
-
-**松开手指**:`audio.completed` → 服务端 ASR/LLM/TTS → 思考蓝绿交替 + 「层」音 → 播放绿常亮。
-
-#### E5.3 场景 2:TTS 播报中 **短按** — 仅打断播放
-
-```text
-短按 → VC_Play_End + 清队列 + await_played=false + 灯 idle
-     → 不 audio.create、不开麦(_ptt_short_press_stop 标记,松手不触发 end_speech)
-```
-
-用于「只想闭嘴、暂不继续说」;要续聊须 **再次长按**。
-
-#### E5.4 场景 3:待机 / 思考中 **长按** — 开始或重开一轮
-
-- **待机、未连接**:`AI_VC_start` → 等 `started` → 自动 `audio.create` + 开录(`_ptt_force_record_on_started`)。
-- **思考中、无 TTS 播放**:同 E5.2 的 (1)~(11);`turn=Thinking` 时发 `cancel`,再 `audio.create`。
-
-#### E5.5 场景 4:聆听中 **长按** — 重录当前轮
-
-用户按下时尚未松手、或希望 **覆盖本轮录音**:
-
-```text
-长按(ptt_begin)
-  → VC_init / 停上一轮录音(VC_Speech_End 若 flag≠0)
-  → cancel(若仍在 Thinking/TTS 管线)
-  → audio.create + VC_Speech_Start_now + 开录提示
-  → 松手 → audio.completed
-```
-
-**说明**:单对话一轮以「按下→松开」为边界;聆听中再次长按视为 **新开一轮** `audio.create`,非长对话式续麦。
-
-#### E5.6 场景 5:标准一轮 PTT(按下 → 松开 → 应答)
-
-**用户操作**:待机或上轮结束后 **长按说话,松手结束**。
-
-```text
-长按按下(ptt_begin)
-  → audio.create → VC_Speech_Start_now → 蓝快闪 + start.mp3
-  → 上行 PCM 帧…
-长按松开(_ai_key_release_handle_ptt,record_flag≠0)
-  → AI_client_VC_end_speech(audio.completed)
-  → uplink_stop,turn 经 SpeechEnded → Thinking
-  → 灯:蓝绿交替 + 「层」提示音
-服务端 asr.result → llm → tts.started
-  → 绿常亮播放 TTS
-  → 播完:本地 VC_Play_End;单对话一般 **不等** audio.played 门控下一轮
-  → 用户可再次长按开始新一轮(E5.2 / E5.4)
-```
-
-**与长对话差异**:松手后 **不会** 自动开麦;必须再次 **长按** 才能说下一句话。
-
-#### E5.7 场景 6:TTS 自然播完 → 待机后再问
-
-```text
-TTS 队列读空 → VC_Play_End → ai_state 回 idle(或绿灯慢闪待机)
-  → 用户长按 ptt_begin
-  → 若 session 仍 started:直接 audio.create + 开录
-  → 若会话已 Close/超时:AI_VC_start → started → 再开录
-```
-
-无 `cancel`(除非上轮服务端仍在 Thinking/TTS 下行)。
-
-#### E5.8 单对话 — 完整状态矩阵(AI 键)
-
-
-| 设备状态(用户视角) | 短按            | 长按按下                    | 长按松开              | 协议要点                             |
-| ---------- | ------------- | ----------------------- | ----------------- | -------------------------------- |
-| 待机、未连接     | 无             | 建链+开录                   | —                 | `Start`→`started`→`audio.create` |
-| 聆听中(蓝快闪)   | 吞掉            | 重开一轮(E5.5)              | `audio.completed` | 松手结束上行                           |
-| 思考中        | 吞掉            | 打断+`audio.create`(E5.4) | 若已开录则 completed   | `cancel`(忙态)                     |
-| TTS 播报中    | **仅停播**(E5.3) | **重开一轮**(E5.2)          | 停播后松手无 completed  | 长按:cancel+create                 |
-| TTS 播完待机   | 吞掉            | 新一轮(E5.7)               | completed         | `audio.create`                   |
-| 弱网 / 异常    | 无             | 同待机尝试                   | —                 | 附录 D                             |
-
-
-#### E5.9 单对话时序简图(长按打断 TTS)
-
-```mermaid
-sequenceDiagram
-    participant U as 用户
-    participant D as 设备端
-    participant S as 服务端
-
-  Note over D,S: turn=Playing,TTS 下行中
-  U->>D: 长按 AI 键
-  D->>D: VC_init / VC_Play_End,清空播放队列
-  D->>D: _drop_tts_audio=1
-  D->>S: cancel(TTS/思考态)
-  D->>S: audio.create
-  D->>D: VC_Speech_Start_now,开录提示音
-  D->>S: PCM 音频帧…
-  U->>D: 松开
-  D->>S: audio.completed
-  S-->>D: asr / llm / tts 事件
-```
-
-
-
-### E6 长对话打断 TTS 时序(对照)
-
-```mermaid
-sequenceDiagram
-    participant U as 用户
-    participant D as 设备端
-    participant S as 服务端
-
-  U->>D: 短按 AI 键
-  D->>D: VC_init(立即停播,清播放队列)
-  D->>S: cancel
-  S-->>D: canceled
-  D->>D: uplink_start → VC_Speech_Start_now,开录提示音
-  D->>S: PCM 上行(VAD,无 audio.create)
-```
-
-
-
-### E7 与电源键、模式切换的边界
-
-
-| 操作            | 行为                                                          |
-| ------------- | ----------------------------------------------------------- |
-| Power 短按(交互中) | `Close`:断会话 / 清 `sid`,进待机;**不是** 续聊                         |
-| Power 双击      | 切换 `VC_mode` 并 TTS 播报「单对话/长对话」;若正在对话先 `Close`               |
-| 单对话 ↔ 长对话切换后  | 须重新 `Start` / 按键建会话;避免沿用上一种模式的 `cancel` / `audio.create` 假设 |
-
-
-### E8 联调检查清单
-
-
-| #   | 检查项                        | 长对话                                                | 单对话                                           |
-| --- | -------------------------- | -------------------------------------------------- | --------------------------------------------- |
-| 1   | TTS 中按键后 **500ms 内** 无残余播报 | 短按+cancel 路径                                       | 长按 `ptt_begin` 路径                             |
-| 2   | `AI_audio_queue` 深度为 0     | `VC_init` 后打印 `[VC] init audio_queue`              | 同左                                            |
-| 3   | 开录前已 `started`             | 是                                                  | 是;否则 `audio.create` 被 SKIP(`VC_PROTO_GUARDS`) |
-| 4   | 开录提示音不叠按键音                 | 短按提示音已关                                            | `ptt_begin` 内不再叠短嘟                            |
-| 5   | 打断后能快速第二轮                  | `canceled`→开麦                                      | 松手→播完→再长按;或 TTS 中直接长按                         |
-| 6   | 串口日志                       | `[KEY]`、`[PTT]`、`[FSM]`、`[VC][proto] audio.create` | 同左                                            |
-
-
-### E9 AI 键双击(复读,可选)
-
-正文第二部分:**待机且上轮 TTS 结束 1 分钟内**,双击 AI 键复读上一段 TTS。
-
-
-| 项目   | 说明                                                                                              |
-| ---- | ----------------------------------------------------------------------------------------------- |
-| 产品行为 | 蓝常亮播复读内容 → 结束回待机(灯灭)                                                                            |
-| 工程现状 | `ai_VC` 内 **AI 键双击业务未实现**;`pwk_key_double_press` 为 **Power 键双击切模式**(`VC_mode` 0↔1 + TTS 提示),勿混淆 |
-| 若需落地 | 缓存末轮 TTS PCM/文本;双击时本地 `play_tts` 或重放队列;期间 `ai_state=playing`;不与 `ptt_begin`/`cancel` 并发         |
-
-
-### E10 服务端事件与端侧动作对照(联调)
-
-
-| 服务端 `event`                     | 长对话典型端侧动作                | 单对话典型端侧动作                                               |
-| ------------------------------- | ------------------------ | ------------------------------------------------------- |
-| `started`                       | `uplink_start` 或等用户按键    | PTT:`audio.create`+开麦(若 `_ptt_force_record_on_started`) |
-| `vad.silence`                   | `uplink_stop`,停上行        | 一般不走(VAD 关)                                             |
-| `asr.started` / `asr.completed` | 更新 FSM,可能提示音             | 松手后 ASR                                                 |
-| `llm.created`                   | `turn=Thinking`,蓝绿交替     | 同左                                                      |
-| `tts.started`                   | 停上行、`play_start`、绿常亮     | 同左                                                      |
-| `tts.completed`                 | 标记服务端下发结束;本地可能仍播         | 同左                                                      |
-| `canceled`                      | `uplink_start` 自动开麦(长对话) | **不**自动开麦(`VC_mode≠0` 门控)                               |
-| `completed`                     | 无 TTS 时可能 `uplink_start` | 不自动开麦                                                   |
-| `config.update`                 | `uplink_stop`            | `uplink_stop`                                           |
-
-
-本地 `**audio.played`**:长对话播完上报后门控下一轮;单对话以用户 **长按** 开始下一轮为主。
-
-### E11 实现差异与待对齐项(评审用)
-
-
-| #   | 场景        | 产品/正文     | 当前 `ai_VC.py` 要点                                        |
-| --- | --------- | --------- | ------------------------------------------------------- |
-| 1   | 长对话·思考中短按 | 打断并开麦     | 仅 TTS 管线忙时走 `cancel`;纯 `Thinking` 可能 `Deny START`(E4.4) |
-| 2   | 长对话·待机短按  | 直接唤醒      | 未命中分支时走 `AI_VC_start` 兜底(含 500ms sleep)                 |
-| 3   | 单对话·短按    | 非 TTS 无操作 | `_ai_key_press_ptt_mode` 非播放态 **return True 吞掉**        |
-| 4   | AI 键双击复读  | 可选        | **未实现**;Power 双击为切模式(E9)                                |
-| 5   | 待机灯       | 正文灯灭      | 工程绿灯慢闪(附录 A)                                            |
-
-
-若产品要求单对话 **短按即开录**,须改 `_ai_key_press_ptt_mode` 并同步正文第二部分表格。
-
-### E12 联调日志关键字(补充 E8)
-
-
-| 日志                                               | 含义                            |
-| ------------------------------------------------ | ----------------------------- |
-| `[KEY] AI_key_press VC_mode=`                    | 按键模式与 FSM 快照                  |
-| `[PTT] cancel before new uplink`                 | 单对话长按前已发 cancel               |
-| `[PTT] wait server.started`                      | 等 `started` 后再 `audio.create` |
-| `[VC][proto] audio.create send` / `SKIP`         | PTT 开轮是否成功                    |
-| `[VC][uplink_ctrl] Request START` / `Deny START` | 长对话开麦门控                       |
-| `[FSM] server.canceled`                          | 打断成功,长对话可续麦                   |
-| `[VC] init audio_queue`                          | 清播放队列成功                       |
-
+- 误触防护:待机短按 Power 无多余操作;单/长对话模式切换 **仅 Power 键双击**(见 REQ-PWR-05),AI 键无双击业务
 
 ---
 
+## 工程详设(对照 / 联调 / 决策)
 
+| 文档 | 用途 |
+| --- | --- |
+| **[详设-对照与决策.md](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)** | 架构、需求-设计-实现矩阵、GAP 双轨决策(**评审入口**) |
+| **[详设-场景与协议流程.md](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)** | 按键/协议步骤、时序图(联调深读) |
 
-
-
-## **要点摘要**
-
-**长对话**:短按打断 TTS → `VC_init` → `cancel` → `canceled` → 自动开麦;VAD 判停后可多轮自动循环(E4.7)。
-
-**单对话**:长按开录 / 松手 `audio.completed`;TTS 中 **短按只停播**,续聊须 **再长按**;标准流程见 E5.6。
-
-**实现差异(E11)**:长对话在纯 `Thinking` 且 TTS 管线不忙时,短按可能被 `uplink` 门控拒绝,产品要求打断时应统一走 `cancel`;AI 键双击复读尚未实现。
-
-如需下一步把 E11 中的差异落实到 `ai_VC.py` 代码修改,可以说明优先项。
-
+上表 `REQ-*` 编号与详设主文档第 3、4 章一一对应。
\ No newline at end of file
diff --git a/1.doc/AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md b/1.doc/AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md
new file mode 100644 (file)
index 0000000..7f9aa7a
--- /dev/null
@@ -0,0 +1,11 @@
+# AI吊坠(双按键+三色指示灯)— 工程附录(已迁移)
+
+> **本文档已重构**,请使用新的详设体系进行代码审查与需求对照:
+
+| 文档 | 用途 |
+| --- | --- |
+| **[详设-对照与决策.md](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)** | 架构、三角对照矩阵、GAP 双轨决策(**评审入口**) |
+| **[详设-场景与协议流程.md](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)** | 按键/协议步骤、时序图(联调深读) |
+| **[交互规范_new.md](./AI吊坠(双按键+三色指示灯)交互规范_new.md)** | 产品需求(`REQ-*` 编号) |
+
+**实现真源**:`2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py`
diff --git a/1.doc/AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md b/1.doc/AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md
new file mode 100644 (file)
index 0000000..e48241d
--- /dev/null
@@ -0,0 +1,398 @@
+# AI吊坠(双按键+三色指示灯)详设 — 场景与协议流程
+
+> **上级文档**:[详设-对照与决策](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)(三角对照、GAP 决策入口)  
+> **需求文档**:[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)  
+> **实现真源**:`2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py`
+
+**阅读说明**:本章为步骤级场景展开(按键顺序、协议事件、时序图)。若与主详设 **第 3/4 章** 或 GAP「决策」列冲突,**以 GAP 已决项为准**。
+
+---
+## 场景详设总览
+
+> **与正文关系**:需求文档第二部分描述用户可感知的「按键→灯→音→结果」;本章描述 **协议与本地执行顺序**,供固件(`ai_VC.py`)联调与评审。实现以当前编译进镜像的 `ai_VC.py` 为准。  
+> **模式存储**:`VC_mode`(`0`=长对话 / `1`=单对话),Power 键双击可切换并 TTS 播报模式名(见正文第一部分)。
+
+### DES-SCN-000 典型场景速查(产品语言)
+
+**单对话 · 场景 1:TTS 播报中,AI 键重新开始对话(长按)**
+
+
+| 步骤  | 要求                                               |
+| --- | ------------------------------------------------ |
+| 1   | **本地立即停播**,清空 `AI_audio_queue` 与 codec 缓冲,杜绝残余尾音 |
+| 2   | 发 **停止当前轮** 消息(`cancel`,服务端停止生成;**不换会话**)        |
+| 3   | 发 **继续对话** 消息(`audio.create`)                    |
+| 4   | 打开录音 → 播 **开录提示音**(start.mp3)→ 开始上传 PCM          |
+| 5   | 松手发 `audio.completed`,进入思考 → TTS 播放              |
+
+
+**长对话 · 对照**:TTS 播报中 **短按** AI 键续聊 → 本地清队列 + `cancel` → 收 `canceled` 后 **直接开麦**(无需 `audio.create`)。详见 **DES-SCN-010.2**、**DES-SCN-011**。
+
+**场景索引**
+
+
+| 模式  | 场景          | 章节        |
+| --- | ----------- | --------- |
+| 长对话 | TTS 中短按打断续聊 | DES-SCN-010.2      |
+| 长对话 | 聆听中短按重开麦    | DES-SCN-010.3      |
+| 长对话 | 思考中短按打断     | DES-SCN-010.4      |
+| 长对话 | 待机唤醒        | DES-SCN-010.5      |
+| 长对话 | VAD 自动多轮    | DES-SCN-010.7      |
+| 单对话 | TTS 中长按重开   | DES-SCN-020.2      |
+| 单对话 | TTS 中短按仅停播  | DES-SCN-020.3      |
+| 单对话 | 待机/思考长按开录   | DES-SCN-020.4      |
+| 单对话 | 标准一轮 PTT    | DES-SCN-020.6      |
+| 单对话 | 播完后再问       | DES-SCN-020.7      |
+| 通用  | 完整状态矩阵      | DES-SCN-010.6、DES-SCN-020.8 |
+| 通用  | 服务端事件对照     | DES-SCN-050       |
+
+
+### DES-SCN-001 模式总览
+
+
+| 维度        | 长对话 `VC_mode=0`(Realtime)                    | 单对话 `VC_mode=1`(Push-To-Talk / PTT)                                    |
+| --------- | -------------------------------------------- | ---------------------------------------------------------------------- |
+| 用户操作      | **短按** AI 键唤醒 / 打断 / 续聊                      | **长按** 开录、**松开** 结束本轮上行                                                |
+| VAD       | 开(服务端判停)                                     | 关(`disable_vad=true`)                                                  |
+| 上行包裹      | 一般 **无需** `audio.create` / `audio.completed` | **必须** `audio.create` → PCM 帧 → `audio.completed`                      |
+| 打断 TTS 续聊 | 短按:发 `**cancel`** + 本地清队列 → 等 `canceled` 后开麦 | **长按**:本地停播清队列 →(TTS/思考态)发 `**cancel`**(不换 `sid`)→ `audio.create` 开新一轮 |
+| 典型灯效      | 聆听蓝慢闪 / 思考蓝绿交替 / 播放绿常亮                       | 长按聆听蓝快闪;松手后思考蓝绿交替                                                      |
+| 进入下一轮门控   | `audio.played`(播完上报)、`canceled` 后自动开麦等       | 松手 `audio.completed` 后等 ASR/LLM/TTS;再次长按走 `ptt_begin`                  |
+
+
+**设计目标**:用户在任何中间态(TTS 播报、思考、弱网恢复中等)按 AI 键,应 **尽快** 进入「下一轮可说话」或「继续当前会话内的新一轮上行」,且 **无残余 TTS 尾音、无播放队列叠录**。
+
+### DES-SCN-002 协议消息与「停止 / 继续」语义
+
+
+| 产品用语          | WebSocket `event`              | 适用模式                   | 作用                                                       |
+| ------------- | ------------------------------ | ---------------------- | -------------------------------------------------------- |
+| 停止当前轮次 / 打断播报 | `cancel`                       | **长对话**(短按打断 TTS、思考中等) | 通知服务端撤销当前响应;端侧随后收 `canceled`,再开麦                         |
+| 本地停止播放(非协议)   | —(`VC_init` + `VC_Play_End`)   | **单对话、长对话**            | 立即停止 PCM/流播放,**清空 `AI_audio_queue`**,必要时 `aud.stopAll()` |
+| 继续对话 / 开新一轮上行 | 长对话:开麦上行 PCM(无 `audio.create`) | 长对话                    | `request_uplink_start` → `VC_Speech_Start_now`           |
+| 继续对话 / 开新一轮上行 | 单对话:`audio.create`             | 单对话                    | `AI_client_VC_send_speech` → 录音 → 帧上行                    |
+| 本轮说话结束        | `audio.completed`              | 单对话                    | 松手触发 `AI_client_VC_end_speech`                           |
+| 本地 TTS 播完     | `audio.played`                 | 长对话为主                  | 播完后上报,服务端进入下一轮 ASR(门控之一)                                 |
+
+
+> **「停止对话」与 `cancel` 的分工**:`cancel` 仅撤销**当前轮**服务端响应(LLM/TTS 生成),**不断开 WebSocket、不换 `sid`**;与 Power 键 `Close`(换会话)区分。单对话在 TTS/思考态 **长按重开** 时也会发 `cancel`,避免旧轮下行与新一轮 `audio.create` 竞态;须 **先本地停播清队列**(用户侧立即静音),再发 `cancel`,再发 `audio.create`。
+
+### DES-SCN-003 本地停播与清缓存(凡打断 TTS 必做)
+
+无论哪种模式,只要用户打断 **TTS/本地播报**,须在同一按键处理链内完成(顺序不可颠倒):
+
+
+| 步骤  | 动作                                             | 说明                                             |
+| --- | ---------------------------------------------- | ---------------------------------------------- |
+| L1  | `VC_Play_End()` / `VC_init()`                  | 停播放流、`VC_Play_flag=0`;`AI_audio_queue.clear()` |
+| L2  | `aud.stopPlayStream()` + `aud.stopAll()`       | 避免 DMA/codec 缓冲残留                              |
+| L3  | `_drop_tts_audio = 1`(可选窗口)                    | 打断后短时 **丢弃** 服务端仍下发的 TTS 帧,防「断断续续继续播」          |
+| L4  | `fsm.await_played = false`(若曾等 `audio.played`) | 避免门控卡死,无法开麦                                    |
+| L5  | 灯 `ai_state: idle` 或 `listening`               | 打断瞬间可先 `idle`;开录后切 `listening`                 |
+
+
+TTS 自然播完:队列读空后 `VC_Play_End`;若本轮需门控,再在适当时机发 `audio.played`。
+
+### DES-SCN-010 长对话(`VC_mode=0`)— AI 键处理
+
+#### DES-SCN-010.1 状态 → 动作
+
+
+| 设备状态(用户视角)  | FSM 近似                          | AI 键 **短按**   | 协议 / 本地顺序                                                                     |
+| ----------- | ------------------------------- | ------------- | ----------------------------------------------------------------------------- |
+| 待机、未建会话     | `session≠Started`               | 唤醒            | `AI_VC_start` → `Start` → 等 `started` → 开麦 → 蓝慢闪 + 开录提示                       |
+| 聆听中         | `turn=Listening`                | 重新聆听          | `request_uplink_start("key.press/continue")` → 清僵死录音态 → `VC_Speech_Start_now` |
+| 思考中         | `turn=Thinking`                 | 打断并开麦         | 见 **DES-SCN-010.4**(宜 `cancel`;管线忙时同 DES-SCN-010.2)                                             |
+| **TTS 播报中** | `turn=Playing` / `TtsReceiving` | **打断并续聊**     | 见 **DES-SCN-010.2**                                                                    |
+| 弱网 / 异常     | `signal=weak` 等                 | 同待机或 Close 策略 | 以异常主详设 GAP-006 为准                                                                    |
+
+
+#### DES-SCN-010.2 场景:TTS 播报中短按 — 打断并进入下一轮聆听
+
+**目标**:尽快停播、通知服务端停止当前响应、开麦。
+
+```text
+用户短按 AI 键
+  → (1) is_ruppt_tts = 1
+  → (2) VC_init(立即停播 + 清空 AI_audio_queue)
+  → (3) 发 cancel(RequestToSpeak)
+  → (4) 等 server.canceled
+  → (5) FSM: uplink_start(canceled) → VC_Speech_Start_now
+  → (6) 蓝慢闪 + 开录提示音(start.mp3 等)+ 上行 PCM(VAD,无 audio.create)
+```
+
+**灯与音**:发 `cancel` 前后本地已无 TTS;开录时蓝慢闪 + 「滴」类开录提示(与正文第二部分一致)。  
+**注意**:`Close_flag` 须在 `RequestToSpeak` 路径清零,否则 `canceled` 后 `_uplink_request_start` 会拒开麦。
+
+#### DES-SCN-010.3 场景:会话已建立、聆听中短按 — 重新开麦
+
+**用户意图**:上一轮 VAD 已判停或仍在聆听,希望 **再说一段**。
+
+```text
+用户短按 AI 键(session=Started,turn=Listening/SpeechEnded,非 TTS 忙)
+  → request_uplink_start("key.press/continue")
+  → _vc_clear_stale_record_state(若 record_flag 僵死)
+  → VC_Speech_Start_now
+  → 蓝慢闪 + 开录提示音 + PCM 上行(VAD,无 audio.create)
+```
+
+不发 `cancel`(当前轮无服务端 TTS/播放占用)。
+
+#### DES-SCN-010.4 场景:思考中短按 — 打断并开麦
+
+**用户意图**(正文第二部分):不等 LLM/TTS 完成,直接说话。
+
+```text
+用户短按 AI 键(turn=Thinking 或 TTS 管线忙)
+  → 若 _vc_tts_pipeline_busy()(含 _tts_pending / tts_rx / Playing 等):
+       同 DES-SCN-010.2:VC_init → cancel → canceled → uplink_start
+  → 否则(纯 Thinking、管线不忙):
+       产品应 cancel 打断;当前实现可能仅 request_uplink_start,
+       若 turn≠Listening/SpeechEnded 会被门控拒绝(串口 Deny START by key.press/continue)
+       → 联调时建议统一走 RequestToSpeak
+```
+
+**灯与音**:打断成功后蓝慢闪 + 开录提示。
+
+#### DES-SCN-010.5 场景:待机 / 未建会话短按 — 唤醒
+
+```text
+用户短按 AI 键(未连接或未 started)
+  → 落入 AI_key_press 兜底:AI_VC_start → Start → 等 started
+  → started 后:request_uplink_start("server.started/idle")
+  → 蓝慢闪 + 开录提示 + VAD 上行
+```
+
+**注意**:此路径含 `utime.sleep_ms(500)` 与历史 `Close` 兼容逻辑,与「已连接续聊」分支不同;联调以 `[KEY]`、`[VC][uplink_ctrl]` 为准。
+
+#### DES-SCN-010.6 长对话 — 完整状态矩阵(AI 键短按)
+
+
+| 设备状态(用户视角) | FSM 近似                   | 短按动作       | 协议要点                                 | 灯效    |
+| ---------- | ------------------------ | ---------- | ------------------------------------ | ----- |
+| 待机、未建会话    | `session≠Started`        | 唤醒开麦       | `Start` → `started` → 上行 PCM         | 蓝慢闪   |
+| 聆听中        | `Listening`              | 重开麦        | `uplink_start`,无 `cancel`            | 蓝慢闪   |
+| 用户说完等 ASR  | `SpeechEnded`            | 可重开麦       | `uplink_start`(若门控允许)                | 蓝慢闪   |
+| 思考中        | `Thinking`               | 打断开麦       | **宜** `cancel`→`canceled`→开麦(见 DES-SCN-010.4) | 蓝慢闪   |
+| TTS 接收/播报  | `TtsReceiving`/`Playing` | 打断续聊       | DES-SCN-010.2:`cancel` + 清队列                  | 蓝慢闪   |
+| TTS 自然播完   | → `audio.played`         | 无需按键       | 自动 `uplink_start`(长对话)               | 蓝慢闪   |
+| 弱网         | `signal=weak`            | 同唤醒或 Close | 主详设 GAP-006                                 | 蓝绿慢交替 |
+
+
+#### DES-SCN-010.7 长对话 — 无按键自动轮次(VAD 闭环)
+
+```text
+开麦聆听(Listening)
+  → 用户说话,PCM 上行
+  → 服务端 vad.silence → uplink_stop,turn=SpeechEnded
+  → llm.created → Thinking(蓝绿交替)
+  → tts.started → TtsReceiving → local.play_start → Playing(绿常亮)
+  → 本地播完 → audio.played(若 _need_audio_played_ack)
+  → on_local_audio_played → Listening,可自动 uplink_start 进入下一轮
+```
+
+**门控**:`await_played` 为真时,禁止 `completed`/`canceled` 抢跑开麦;`Close_flag=1` 时拒绝 `canceled`/`audio.played` 触发的自动开麦。
+
+### DES-SCN-020 单对话(`VC_mode=1`)— AI 键处理
+
+#### DES-SCN-020.1 操作分工
+
+
+| 操作         | 作用                   | 说明                                                                                         |
+| ---------- | -------------------- | ------------------------------------------------------------------------------------------ |
+| **短按**     | 仅在 **TTS/播放管线忙** 时有效 | 仅 **本地** `VC_Play_End` + 清队列;**不** 开录、**不** 发 `audio.create`;非播放态短按无业务(由 `ptt_begin` 承担开录) |
+| **长按(按下)** | 开始 / **重新开始** 一轮对话   | 走 `ptt_begin`(`AI_key_long_press`)                                                         |
+| **长按(松开)** | 结束本轮上行               | `AI_client_VC_end_speech`(`audio.completed`)→ 思考灯 + 「层」提示音                                 |
+
+
+#### DES-SCN-020.2 场景 1:TTS 播报中 **长按** — 重新开始一轮对话(核心示例)
+
+**用户意图**:不等 TTS 播完,尽快进入下一轮「按住说话」或继续当前会话内的新一轮上行。
+
+**产品要求(执行顺序不可颠倒)**:
+
+1. **先本地停播并清空播放缓存**(用户侧须立即静音,防止残余 TTS 尾音)。
+2. **再发停止当前轮次消息**(WebSocket `cancel`,仅当服务端仍在 TTS/思考等中间态)。
+3. **再发继续对话协议**(`audio.create`,不换 `sid`)。
+4. **打开录音** → **播开录提示音** → **上行 PCM 帧**(`audio.create` 与开麦顺序受 `VC_PROTO_GUARDS` 约束)。
+
+**固件链路(`ptt_begin`,与 `ai_VC.py` 对齐)**:
+
+```text
+用户长按 AI 键(ptt_begin)
+  → (1) Close_flag = 0;_drop_tts_audio = 1
+  → (2) VC_init()                         // 立即停播 + 清空 AI_audio_queue(VC_Play_End)
+  → (3) 若仍在播:再次 VC_Play_End()       // 兜底 codec/DMA 缓冲
+  → (4) 若上一轮录音未结束:VC_Speech_End()
+  → (5) 若 turn∈{Thinking,TtsReceiving,Playing} 或 TTS 管线忙:
+         发 cancel(停止当前轮服务端响应,不换 sid)
+  → (6) 若 WebSocket 未连接 / 未 started:
+         AI_VC_start 或 Start;_ptt_force_record_on_started,等 started 后自动 (7)~(11)
+      否则(会话已 started):
+  → (7) 发 audio.create(AI_client_VC_send_speech)   // 继续对话 / 新一轮上行
+  → (8) VC_Speech_Start_now()                         // 开硬件录音
+  → (9) 开录提示音(RECORD_START_TONE_FILE / start.mp3,由 VC_Speech_Start 路径播放)
+  → (10) 蓝灯聆听态(长按按下:蓝快闪,见正文第二部分)
+  → (11) 上行 PCM 帧(须在 audio.create 之后)
+```
+
+**为何本地停播优先于 `cancel`**:用户按下 AI 键后 **≤100ms** 内应听不到旧 TTS;若先发 `cancel` 再等本地清队列,DMA/队列中已缓冲的 PCM 仍会播出。本地 `VC_init` 后再 `cancel`,可同时满足「马上静音」与「服务端停止生成」。
+
+**与产品描述的对应**:
+
+
+| 产品描述          | 单对话实现                                                     |
+| ------------- | --------------------------------------------------------- |
+| 「停止对话消息」      | TTS/思考态:`**cancel`**;并 **必先** 完成 DES-SCN-003 本地 L1~L3              |
+| TTS 立即停止或自然结束 | **以 L1~L2 立即停播为准**;无新数据后播放线程退出;`_drop_tts_audio` 丢弃迟到的下行帧 |
+| 「继续对话协议」      | `**audio.create`**(保持当前 `sid` / `dialog_id`)              |
+| 打开录音          | `VC_Speech_Start_now`                                     |
+| 开录提示音         | `RECORD_START_TONE_FILE` / start.mp3                      |
+| 上传录音          | 录音线程 → WS 发 PCM 帧 → 松手发 `audio.completed`                 |
+
+
+**松开手指**:`audio.completed` → 服务端 ASR/LLM/TTS → 思考蓝绿交替 + 「层」音 → 播放绿常亮。
+
+#### DES-SCN-020.3 场景 2:TTS 播报中 **短按** — 仅打断播放
+
+```text
+短按 → VC_Play_End + 清队列 + await_played=false + 灯 idle
+     → 不 audio.create、不开麦(_ptt_short_press_stop 标记,松手不触发 end_speech)
+```
+
+用于「只想闭嘴、暂不继续说」;要续聊须 **再次长按**。
+
+#### DES-SCN-020.4 场景 3:待机 / 思考中 **长按** — 开始或重开一轮
+
+- **待机、未连接**:`AI_VC_start` → 等 `started` → 自动 `audio.create` + 开录(`_ptt_force_record_on_started`)。
+- **思考中、无 TTS 播放**:同 DES-SCN-020.2 的 (1)~(11);`turn=Thinking` 时发 `cancel`,再 `audio.create`。
+
+#### DES-SCN-020.5 场景 4:聆听中 **长按** — 重录当前轮
+
+用户按下时尚未松手、或希望 **覆盖本轮录音**:
+
+```text
+长按(ptt_begin)
+  → VC_init / 停上一轮录音(VC_Speech_End 若 flag≠0)
+  → cancel(若仍在 Thinking/TTS 管线)
+  → audio.create + VC_Speech_Start_now + 开录提示
+  → 松手 → audio.completed
+```
+
+**说明**:单对话一轮以「按下→松开」为边界;聆听中再次长按视为 **新开一轮** `audio.create`,非长对话式续麦。
+
+#### DES-SCN-020.6 场景 5:标准一轮 PTT(按下 → 松开 → 应答)
+
+**用户操作**:待机或上轮结束后 **长按说话,松手结束**。
+
+```text
+长按按下(ptt_begin)
+  → audio.create → VC_Speech_Start_now → 蓝快闪 + start.mp3
+  → 上行 PCM 帧…
+长按松开(_ai_key_release_handle_ptt,record_flag≠0)
+  → AI_client_VC_end_speech(audio.completed)
+  → uplink_stop,turn 经 SpeechEnded → Thinking
+  → 灯:蓝绿交替 + 「层」提示音
+服务端 asr.result → llm → tts.started
+  → 绿常亮播放 TTS
+  → 播完:本地 VC_Play_End;单对话一般 **不等** audio.played 门控下一轮
+  → 用户可再次长按开始新一轮(DES-SCN-020.2 / DES-SCN-020.4)
+```
+
+**与长对话差异**:松手后 **不会** 自动开麦;必须再次 **长按** 才能说下一句话。
+
+#### DES-SCN-020.7 场景 6:TTS 自然播完 → 待机后再问
+
+```text
+TTS 队列读空 → VC_Play_End → ai_state 回 idle → 正常待机灭灯(`LED_IDLE_MODE=off`)
+  → 用户长按 ptt_begin
+  → 若 session 仍 started:直接 audio.create + 开录
+  → 若会话已 Close/超时:AI_VC_start → started → 再开录
+```
+
+无 `cancel`(除非上轮服务端仍在 Thinking/TTS 下行)。
+
+#### DES-SCN-020.8 单对话 — 完整状态矩阵(AI 键)
+
+
+| 设备状态(用户视角) | 短按            | 长按按下                    | 长按松开              | 协议要点                             |
+| ---------- | ------------- | ----------------------- | ----------------- | -------------------------------- |
+| 待机、未连接     | 无             | 建链+开录                   | —                 | `Start`→`started`→`audio.create` |
+| 聆听中(蓝快闪)   | 吞掉            | 重开一轮(DES-SCN-020.5)              | `audio.completed` | 松手结束上行                           |
+| 思考中        | 吞掉            | 打断+`audio.create`(DES-SCN-020.4) | 若已开录则 completed   | `cancel`(忙态)                     |
+| TTS 播报中    | **仅停播**(DES-SCN-020.3) | **重开一轮**(DES-SCN-020.2)          | 停播后松手无 completed  | 长按:cancel+create                 |
+| TTS 播完待机   | 吞掉            | 新一轮(DES-SCN-020.7)               | completed         | `audio.create`                   |
+| 弱网 / 异常    | 无             | 同待机尝试                   | —                 | 主详设 GAP-006                             |
+
+
+#### DES-SCN-020.9 单对话时序简图(长按打断 TTS)
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant D as 设备端
+    participant S as 服务端
+
+  Note over D,S: turn=Playing,TTS 下行中
+  U->>D: 长按 AI 键
+  D->>D: VC_init / VC_Play_End,清空播放队列
+  D->>D: _drop_tts_audio=1
+  D->>S: cancel(TTS/思考态)
+  D->>S: audio.create
+  D->>D: VC_Speech_Start_now,开录提示音
+  D->>S: PCM 音频帧…
+  U->>D: 松开
+  D->>S: audio.completed
+  S-->>D: asr / llm / tts 事件
+```
+
+
+
+### DES-SCN-011 长对话打断 TTS 时序(对照)
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant D as 设备端
+    participant S as 服务端
+
+  U->>D: 短按 AI 键
+  D->>D: VC_init(立即停播,清播放队列)
+  D->>S: cancel
+  S-->>D: canceled
+  D->>D: uplink_start → VC_Speech_Start_now,开录提示音
+  D->>S: PCM 上行(VAD,无 audio.create)
+```
+
+
+
+### DES-SCN-030 与电源键、模式切换的边界
+
+
+| 操作            | 行为                                                          |
+| ------------- | ----------------------------------------------------------- |
+| Power 短按(交互中) | `Close`:断会话 / 清 `sid`,进待机;**不是** 续聊                         |
+| Power 双击      | 切换 `VC_mode` 并 TTS 播报「单对话/长对话」;若正在对话先 `Close`               |
+| 单对话 ↔ 长对话切换后  | 须重新 `Start` / 按键建会话;避免沿用上一种模式的 `cancel` / `audio.create` 假设 |
+
+> **说明**:产品需求已取消 AI 键双击;模式切换见上表 **Power 双击**(对应 REQ-PWR-05)。
+
+### DES-SCN-050 服务端事件与端侧动作对照(联调)
+
+
+| 服务端 `event`                     | 长对话典型端侧动作                | 单对话典型端侧动作                                               |
+| ------------------------------- | ------------------------ | ------------------------------------------------------- |
+| `started`                       | `uplink_start` 或等用户按键    | PTT:`audio.create`+开麦(若 `_ptt_force_record_on_started`) |
+| `vad.silence`                   | `uplink_stop`,停上行        | 一般不走(VAD 关)                                             |
+| `asr.started` / `asr.completed` | 更新 FSM,可能提示音             | 松手后 ASR                                                 |
+| `llm.created`                   | `turn=Thinking`,蓝绿交替     | 同左                                                      |
+| `tts.started`                   | 停上行、`play_start`、绿常亮     | 同左                                                      |
+| `tts.completed`                 | 标记服务端下发结束;本地可能仍播         | 同左                                                      |
+| `canceled`                      | `uplink_start` 自动开麦(长对话) | **不**自动开麦(`VC_mode≠0` 门控)                               |
+| `completed`                     | 无 TTS 时可能 `uplink_start` | 不自动开麦                                                   |
+| `config.update`                 | `uplink_stop`            | `uplink_stop`                                           |
+
+
+本地 `**audio.played`**:长对话播完上报后门控下一轮;单对话以用户 **长按** 开始下一轮为主。
+
+
diff --git a/1.doc/AI吊坠(双按键+三色指示灯)详设-对照与决策.md b/1.doc/AI吊坠(双按键+三色指示灯)详设-对照与决策.md
new file mode 100644 (file)
index 0000000..1b08cf3
--- /dev/null
@@ -0,0 +1,307 @@
+# AI吊坠(双按键+三色指示灯)详设 — 对照与决策
+
+> **需求文档**:[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)(产品行为准绳)  
+> **场景详设**:[详设-场景与协议流程](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)(按键/协议步骤展开)  
+> **实现真源**:`2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py`(`AI_led`、`AI_hw_status`、`AI_dev`)
+
+---
+
+## 0. 如何使用本文档
+
+### 0.1 三角对照
+
+
+| 角      | 文档                             | 回答的问题    |
+| ------ | ------------------------------ | -------- |
+| **需求** | `交互规范_new.md` + `REQ-`*        | 用户要什么?   |
+| **设计** | 本文第 2 章 `DES-`*、场景 `DES-SCN-`* | 固件应怎么做?  |
+| **实现** | `ai_VC.py` 等                   | 代码现在怎么做? |
+
+
+评审流程:**第 3 章矩阵** 标 `状态` → 不一致项查 **第 4 章 GAP** → 在 **决策** 列填 `采纳甲` / `采纳乙` / `搁置` / `改需求` → 再改代码或改需求,避免三方漂移。
+
+### 0.2 准绳优先级
+
+1. 你已勾选的 GAP **决策**(最高)
+2. 需求文档(产品对外承诺)
+3. 本文设计规格(工程应然)
+4. 当前代码(现状,直至按决策修改)
+
+### 0.3 要点摘要(评审速览)
+
+- **长对话**:短按打断 TTS / **思考中** → `RequestToSpeak` → `VC_init` → `cancel` → `canceled` → `uplink_start`(见 `DES-SCN-010.2`~`010.4`、`DES-SCN-011`)。  
+- **单对话**:长按开录 / 松手 `audio.completed`;TTS 中短按仅停播,续聊须再长按(见 `DES-SCN-020.6`)。  
+- **待机灯**:正常待机 **灭灯**(`LED_IDLE_MODE=off`,GAP-005 方案甲)。  
+- **电量阈值**:低电 `soc<30%`、空电 `soc<10%`(GAP-007 方案甲)。  
+- **异常灯**:`set_led_state.alert=rb_flash_2` 红蓝快闪 2 次后回表(GAP-006 已落实 ASR 空结果、thinking 超时)。
+
+---
+
+## 1. 系统架构(一页)
+
+```mermaid
+flowchart TB
+  gpio[GPIO_btn_device]
+  dev[AI_dev_去抖串行]
+  vc[AI_VC_按键与VCStateMachine]
+  ws[WebSocket_V2_5]
+  aud[录音播放队列]
+  led[AI_led]
+  hw[AI_hw_status]
+  gpio -->|EventMesh| dev
+  dev -->|AI_key_press等| vc
+  vc --> ws
+  vc --> aud
+  vc -->|set_led_state| led
+  hw -->|set_led_state| led
+```
+
+
+
+### 1.1 模块职责
+
+
+| 模块               | 文件         | 职责                                         |
+| ---------------- | ---------- | ------------------------------------------ |
+| `AI_dev`         | `ai_VC.py` | 按键 ExtInt、去抖、`publish_AI_btnInfo` 串行执行     |
+| `AI_VC`          | `ai_VC.py` | WebSocket 会话、按键路由、音频上下行                    |
+| `VCStateMachine` | `ai_VC.py` | `session` / `turn` / `tts_rx_phase` 唯一轮次真源 |
+| `AI_hw_status`   | `ai_VC.py` | 10s 轮询 + `hw_usb_state` → 充电/电量/信号         |
+| `AI_led`         | `ai_VC.py` | `set_led_state` 聚合 → GPIO 灯效               |
+
+
+### 1.2 EventMesh 主题(常用)
+
+
+| 主题                                                      | 发布方                    | 订阅方            | 说明                  |
+| ------------------------------------------------------- | ---------------------- | -------------- | ------------------- |
+| `AI_key_press` / `AI_key_long_press` / `AI_key_release` | `AI_dev`               | `AI_VC`        | AI 键                |
+| `pwk_key_double_press`                                  | 电源键模块                  | `AI_VC`        | Power 双击切 `VC_mode` |
+| `set_led_state`                                         | `AI_VC`、`AI_hw_status` | `AI_led`       | LED 唯一入口            |
+| `hw_usb_state`                                          | 底层                     | `AI_hw_status` | USB 插拔即时刷新          |
+| `get_led_debug_state`                                   | 调试                     | `AI_led`       | 返回聚合灯态              |
+
+
+### 1.3 设计原则(文档化)
+
+- 轮次/会话判断只读 `fsm.*`,不引入并行整型状态。  
+- 打断 TTS:**先** `VC_init`(本地停播清队列)**再** `cancel` / `audio.create`(见 `DES-SCN-003`)。  
+- LED 只经 `set_led_state`;AI 阶段灯优先于充电/电量(见 `DES-L3-01`)。
+
+---
+
+## 2. 设计规格(按层)
+
+### 2.1 L1 输入与按键(`DES-L1`)
+
+
+| ID        | 规格                                        | 实现锚点                                             |
+| --------- | ----------------------------------------- | ------------------------------------------------ |
+| DES-L1-01 | 长对话:AI **短按** 唤醒/打断/续聊                    | `AI_key_press` → `_ai_key_press_long_dialogue_`* |
+| DES-L1-02 | 单对话:AI **长按** 开录、**松开** `audio.completed` | `ptt_begin`、`_ai_key_release_handle_ptt`         |
+| DES-L1-03 | 单对话:TTS 中 **短按** 仅停播,不开录                  | `_ai_key_press_ptt_mode`                         |
+| DES-L1-04 | Power **双击** 切换 `VC_mode` 并 TTS 播报        | `pwk_key_double_press`                           |
+| DES-L1-05 | Power 交互中 **短按** `Close` 断会话进待机           | `pwk_key_press` 等                                |
+
+
+### 2.2 L2 会话与协议(`DES-L2`)
+
+
+| ID        | 规格                                                      | 实现锚点                                       |
+| --------- | ------------------------------------------------------- | ------------------------------------------ |
+| DES-L2-01 | `VC_mode=0`:VAD 开,一般无 `audio.create`/`completed`        | `VCMode.Realtime`                          |
+| DES-L2-02 | `VC_mode=1`:VAD 关,必须 `audio.create`→帧→`audio.completed` | `VCMode.PushToTalk`                        |
+| DES-L2-03 | 会话层:`Disconnected`→`Connecting`→`Starting`→`Started`    | `VCSessionState`                           |
+| DES-L2-04 | 轮次层:`Idle`→`Listening`→…→`Playing`→续轮                   | `VCTurnState`、`VCStateMachine`             |
+| DES-L2-05 | 长对话打断 TTS:`cancel`→`canceled`→`uplink_start`            | `RequestToSpeak`、`on_server_canceled`      |
+| DES-L2-06 | 长对话播完门控:`audio.played` 后可自动开麦                           | `fsm.await_played`、`on_local_audio_played` |
+
+
+步骤级展开见 [场景详设](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)。
+
+### 2.3 L3 呈现:灯 / 音 / 硬件态(`DES-L3`)
+
+#### DES-L3-01 LED 优先级(`AI_led._apply_by_table`)
+
+
+| 优先级 | 输入         | 取值            | 灯效    | `period_ms`               |
+| --- | ---------- | ------------- | ----- | ------------------------- |
+| P1  | `ai_state` | `playing`     | 绿常亮   | —                         |
+| P2  | `ai_state` | `thinking`    | 蓝绿交替  | 500                       |
+| P3  | `ai_state` | `listening`   | 蓝慢闪   | 1000                      |
+| B1  | `charging` | `full`        | 绿常亮   | —                         |
+| B2  | `charging` | `charging`    | 红常亮   | —                         |
+| B3  | `battery`  | `low`/`empty` | 红慢闪   | 1000                      |
+| B4  | `signal`   | `weak`        | 蓝绿慢交替 | 1000                      |
+| 默认  | —          | `idle`        | **灭灯** | `LED_IDLE_MODE=off`(可选 `green_blink`) |
+| P0  | `alert`    | `rb_flash_2`  | 红蓝快闪 2 次 | `250`(约 2 次/秒)后回表 |
+
+
+`set_led_state` 键:`ai_state` / `charging` / `battery` / `signal` / `alert`。
+
+#### DES-L3-02 硬件态采样(`AI_hw_status`)
+
+
+| USB | SoC(非充电) | `charging`        | `battery`  |
+| --- | -------- | ----------------- | ---------- |
+| 否   | ≥30%     | `none`            | `normal`   |
+| 否   | 10%~30%  | `none`            | `low`      |
+| 否   | <10%     | `none`            | `empty`    |
+| 是   | —        | `charging`/`full` | 不按 SoC 刷低电 |
+
+
+弱网:`0 ≤ CSQ < SIGNAL_WEAK_CSQ`(默认 10)→ `signal=weak`。
+
+#### DES-L3-03 灯与 FSM 对应
+
+
+| `ai_state`  | 典型触发                |
+| ----------- | ------------------- |
+| `listening` | 开麦、续聊               |
+| `thinking`  | `llm.created`       |
+| `playing`   | `tts.started` / 本地播 |
+| `idle`      | 交互结束、异常恢复           |
+
+
+---
+
+## 3. 需求 — 设计 — 实现对照矩阵
+
+
+| REQ-ID      | 需求摘要                      | 设计规格                    | 实现锚点                         | 状态    | GAP-ID  |
+| ----------- | ------------------------- | ----------------------- | ---------------------------- | ----- | ------- |
+| REQ-PWR-01  | 关机时长按 Power 3s 开机,绿灯常亮 3s | (电源模块)                  | 电源键 / `ui`                   | 需求未定义 | —       |
+| REQ-PWR-02  | 死机长按 8s 强制关机,无灯无音         | (电源模块)                  | 电源键                          | 需求未定义 | —       |
+| REQ-PWR-03  | 待机短按 Power 进休眠,无灯         | DES-L1-05               | `pm.autosleep` 等             | 部分一致  | —       |
+| REQ-PWR-04  | AI 交互中短按 Power 停对话进待机,绿慢闪 | DES-L1-05               | `Close`                      | 部分一致  | —       |
+| REQ-PWR-05  | Power 双击切换单/长对话并播报        | DES-L1-04               | `pwk_key_double_press`       | 一致    | —       |
+| REQ-AI-01   | 长对话待机短按:蓝慢闪+滴,开麦          | DES-L1-01、DES-SCN-010.5 | `_ai_key_press_long_dialogue_wake`→`AI_VC_start` | 一致    | GAP-002 |
+| REQ-AI-02   | 长对话聆听中短按:重开麦              | DES-SCN-010.3           | `request_uplink_start`       | 一致    | —       |
+| REQ-AI-03   | 长对话 TTS 中短按:打断并聆听         | DES-SCN-010.2           | `_ai_key_press_long_dialogue_interrupt` | 一致    | —       |
+| REQ-AI-04   | 长对话思考中短按:打断并聆听            | DES-SCN-010.4           | `_ai_key_press_long_dialogue_interrupt` | 一致    | GAP-001 |
+| REQ-AI-05   | 单对话 TTS 中短按:仅停播           | DES-L1-03、DES-SCN-020.3 | `_ai_key_press_ptt_mode`     | 一致    | —       |
+| REQ-AI-06   | 单对话非 TTS 短按:无操作           | DES-L1-03               | `return True` 吞掉             | 一致    | GAP-003 |
+| REQ-AI-07   | 单对话长按:蓝快闪+开录音             | DES-L1-02、DES-SCN-020.2 | `ptt_begin`                  | 一致    | —       |
+| REQ-AI-08   | 单对话松手:思考灯+层音+completed    | DES-L1-02               | `AI_client_VC_end_speech`    | 一致    | —       |
+| REQ-BAT-01  | 正常待机灯灭                    | DES-L3-01 默认            | `_set_off`(`LED_IDLE_MODE=off`) | 一致    | GAP-005 |
+| REQ-BAT-02  | 低电 10%~30% 红慢闪+双滴         | DES-L3-02               | `battery=low`(SoC<30%)       | 一致    | GAP-007 |
+| REQ-BAT-03  | 电量<10% 红快闪 3 次后关机         | DES-L3-02               | `battery=empty`(SoC<10%)     | 一致    | GAP-007 |
+| REQ-BAT-04  | 充电中红常亮+滴                  | DES-L3-02               | `charging=charging`          | 一致    | GAP-008 |
+| REQ-BAT-05  | 充满红→绿常亮+长滴                | DES-L3-02               | `charging=full`              | 一致    | —       |
+| REQ-ERR-01  | ASR 失败:红蓝快闪 2 次→待机灭       | DES-L3 P0 `alert`         | `_vc_led_alert_rb_flash`(asr 空) | 一致    | GAP-006 |
+| REQ-ERR-02  | 网络异常:蓝绿慢交替+语音循环           | DES-L3-02 B4            | `websocket_error`、`bg_alt`   | 部分一致  | GAP-006 |
+| REQ-ERR-03  | AI 超时:蓝常亮→红蓝闪→灭           | DES-L3 P0 `alert`         | `thinking_timeout`→`alert`   | 部分一致  | GAP-006 |
+| REQ-NOTE-01 | 备注:会话空闲后进入休眠前灯态           | DES-L3 默认灭灯;可选绿闪      | `LED_IDLE_MODE`              | 已对齐 part3 | GAP-005 |
+| REQ-NOTE-02 | 备注:聆听蓝闪、思考交替、播放绿闪         | DES-L3-01 P2/P3/P1      | `set_led_state`              | 部分一致  | —       |
+| REQ-NOTE-03 | 休眠无灯光                     | (休眠策略)                  | `ui.py` / `pm`               | 部分一致  | —       |
+| REQ-SUP-01  | 充电中 AI 交互优先 AI 灯          | DES-L3-01               | `_apply_by_table`            | 一致    | GAP-008 |
+| REQ-SUP-02  | 按键后灯音延迟≤100ms             | DES-SCN-003             | `VC_init` 优先                 | 一致    | —       |
+
+
+---
+
+## 4. 差异登记簿(GAP)
+
+> **决策**列请产品/固件填写:`采纳甲` | `采纳乙` | `搁置` | `改需求`
+
+
+| GAP-ID  | 需求                    | 设计目标               | 实现现状                                           | 风险        | 方案甲:架构精简·安全·实用                                  | 方案乙:改动最小                               | 决策                               | 影响面                                    |
+| ------- | --------------------- | ------------------ | ---------------------------------------------- | --------- | ----------------------------------------------- | -------------------------------------- | -------------------------------- | -------------------------------------- |
+| GAP-001 | REQ-AI-04             | 思考中短按统一打断并开麦       | 仅 TTS 忙走 `cancel`;纯 `Thinking` 可能 `Deny START` | 用户按了没反应   | `turn==Thinking` 走 `RequestToSpeak`→`cancel`→开麦 | 详设标注限制;或单行门控热修                         | **采纳甲**(已改代码) | `AI_key_press`、`_ai_key_press_long_dialogue_interrupt` |
+| GAP-002 | REQ-AI-01             | 待机短按直达唤醒           | 兜底 `Close`+`AI_VC_start`+`sleep(500)`          | 唤醒慢/偶发断链感 | 去掉兜底 `Close`;未连接单一路径 `AI_VC_start`              | 保留现状,联调看 `[KEY]`                       | **采纳甲**(已改代码) | `AI_key_press`、`_ai_key_press_long_dialogue_wake` |
+| GAP-003 | REQ-AI-06             | 单对话非 TTS 短按无操作     | 非播放态 `return True` 吞掉                          | 低(防误触)    | 保持为设计,REQ 已对齐                                   | 若需短按开录:改 `_ai_key_press_ptt_mode`+改需求表 | **采纳甲** | `_ai_key_press_ptt_mode`               |
+| GAP-005 | REQ-BAT-01 vs NOTE-01 | part3 灯灭 vs 备注绿灯慢闪 | idle 默认 `green_blink`                          | 产品表述冲突    | `AI_led` idle 灭灯                                | **改需求** part3 为绿灯慢闪                    | **采纳甲**(`LED_IDLE_MODE=off`) | `AI_led._apply_by_table`               |
+| GAP-006 | REQ-ERR-01~03         | 红蓝快闪等专用灯序          | 无 `alert` 模式;TTS/弱网替代                          | 异常辨识度弱    | 新增 `alert` 键+优先级+业务触发                           | 需求降级为语音+弱网灯;接受现状                       | **采纳甲**(ASR 空、thinking 超时已接 alert) | `AI_led`、`ai_VC` 异常分支                  |
+| GAP-007 | REQ-BAT-02~03         | 10%/30% 阈值         | SoC 5%/20% 工程档                                 | 提醒时机偏差    | `AI_hw_status` 阈值可配置并对齐需求                       | 需求脚注「以 OCV/工程档为准」                      | **采纳甲**(30%/10%) | `default_config`、`AI_hw_status`         |
+| GAP-008 | REQ-BAT-04、SUP-01     | 充电红常亮 vs AI 覆盖     | AI 非 idle 盖住充电灯                                | 充电中不易见红常亮 | 保持 AI 优先(交互反馈优先)                                | 充电中插入充电指示(需产品确认)                       | **采纳甲** | `AI_led` 优先级                           |
+
+
+### 4.1 整体姿态对比(选默认路线)
+
+
+| 维度     | 姿态 A:架构精简·安全·实用      | 姿态 B:改动最小          |
+| ------ | -------------------- | ------------------ |
+| 目标     | 需求↔实现长期一致            | 冻结现状,文档吸收差异        |
+| LED/异常 | 倾向 GAP-005/006 **甲** | 倾向 **乙**(改需求或接受现状) |
+| 按键/FSM | 倾向 GAP-001/002 **甲** | 倾向 **乙**(标注已知限制)   |
+| 代码工作量  | 中~高                  | 低(0~2 处热修)         |
+
+
+**建议**:先填齐上表 **决策** 列,再批量改需求或 `ai_VC.py`。
+
+### 4.2 GAP-001:为何 `RequestToSpeak` 必须先 `cancel` 再开麦
+
+长对话在 `turn==Thinking` 或 TTS 播放中,若直接 `uplink_start`,`_uplink_request_start` 会因 `turn` 非 `Listening/SpeechEnded` 而 **Deny START**;若先开麦再 `cancel`,本地/服务端旧轮下行可能与新一轮上行竞态。
+
+**方案甲时序(与 DES-SCN-010.2/010.4 一致)**:
+
+1. 短按 → `AI_client_VC_RequestToSpeak`:`VC_init` 停播清队列 + WebSocket 发 `cancel`。  
+2. 服务端 `server.canceled` → `on_server_canceled` 将 `turn` 置为 `Listening`。  
+3. `on_server_event("canceled")` 追加 `uplink_start` → 开麦、蓝慢闪。
+
+因此「cancel 在后」是指 **开麦动作在 canceled 事件之后**,而非不发 cancel。
+
+---
+
+## 5. 联调索引
+
+### 5.1 检查清单(原 E8)
+
+
+| #   | 检查项                    | 长对话                                 | 单对话                    |
+| --- | ---------------------- | ----------------------------------- | ---------------------- |
+| 1   | TTS 中按键后 500ms 内无残余播报  | 短按+cancel                           | 长按 `ptt_begin`         |
+| 2   | `AI_audio_queue` 深度为 0 | `VC_init` 后 `[VC] init audio_queue` | 同左                     |
+| 3   | 开录前已 `started`         | 是                                   | 否则 `audio.create` SKIP |
+| 4   | 开录提示音不叠按键音             | 短按提示音已关                             | `ptt_begin` 内不叠短嘟      |
+| 5   | 打断后能快速第二轮              | `canceled`→开麦                       | 松手→播完→再长按              |
+| 6   | 串口日志                   | `[KEY]`、`[FSM]`、`[VC][proto]`       | 同左                     |
+
+
+### 5.2 日志关键字(原 E12)
+
+
+| 日志                                               | 含义                        |
+| ------------------------------------------------ | ------------------------- |
+| `[KEY] AI_key_press VC_mode=`                    | 按键模式与 FSM 快照              |
+| `[PTT] cancel before new uplink`                 | PTT 长按前已发 cancel          |
+| `[PTT] wait server.started`                      | 等 started 后再 audio.create |
+| `[VC][proto] audio.create send` / `SKIP`         | PTT 开轮是否成功                |
+| `[VC][uplink_ctrl] Request START` / `Deny START` | 长对话开麦门控                   |
+| `[FSM] server.canceled`                          | 打断成功,长对话可续麦               |
+| `[VC] init audio_queue`                          | 清播放队列成功                   |
+
+
+### 5.3 调试 API
+
+`EventMesh.publish("get_led_debug_state")` → `{ ai, charging, battery, signal, mode, period_ms }`
+
+---
+
+## 6. 引用与场景索引
+
+
+| 文档       | 路径                                                                  |
+| -------- | ------------------------------------------------------------------- |
+| 需求规范     | [交互规范_new.md](./AI吊坠(双按键+三色指示灯)交互规范_new.md)                         |
+| 场景详设     | [详设-场景与协议流程.md](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)                     |
+| 源码       | `2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py` |
+| 旧附录(已迁移) | [交互规范_附录-工程与代码审查.md](./AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md)           |
+
+
+### 6.1 场景详设索引(`DES-SCN-`*)
+
+
+| ID          | 内容                          |
+| ----------- | --------------------------- |
+| DES-SCN-000 | 场景速查与索引                     |
+| DES-SCN-001 | 模式总览                        |
+| DES-SCN-002 | 协议消息语义                      |
+| DES-SCN-003 | 打断 TTS 本地清缓存顺序              |
+| DES-SCN-010 | 长对话 AI 键(含 010.2~010.7 子场景) |
+| DES-SCN-011 | 长对话打断 TTS 时序图               |
+| DES-SCN-020 | 单对话 PTT(含 020.2~020.8)      |
+| DES-SCN-030 | 与 Power 键、模式切换边界            |
+| DES-SCN-050 | 服务端事件对照                     |
+
+
index 8204a6a..6b5bfc8 100644 (file)
@@ -127,9 +127,13 @@ class AI_led(Abstract):
         self._st_charging = "none"   # none/charging/full
         self._st_battery = "normal"  # normal/low/empty
         self._st_signal = "normal"   # normal/weak
+        self._st_alert = None        # None / rb_flash_2(REQ-ERR-01/03)
 
         self._timer = osTimer()
-        self._mode = "off"  # off/red_on/green_on/green_blink/blue_blink/red_blink/bg_alt
+        self._mode = "off"  # off/red_on/green_on/green_blink/blue_blink/red_blink/bg_alt/alert_rb
+        self._alert_steps_left = 0
+        self._alert_rb_phase = 0
+        self._alert_period_ms = 250
         self._blink_on = 0
         self._bg_phase = 0
         self._period_ms = 1000
@@ -248,6 +252,29 @@ class AI_led(Abstract):
         self._set_rgb(0, 0, 1)
         self._timer.start(self._period_ms, 1, self._timer_cb)
 
+    def _set_alert_rb_flash(self, cycles=2, period_ms=250):
+        """REQ-ERR-01/03:红蓝快闪(约 2 次/秒),结束后回到 _apply_by_table。"""
+        try:
+            cycles = int(cycles or 2)
+        except Exception:
+            cycles = 2
+        if cycles < 1:
+            cycles = 1
+        try:
+            period_ms = int(period_ms or 250)
+        except Exception:
+            period_ms = 250
+        if period_ms < 100:
+            period_ms = 100
+        self._mode = "alert_rb"
+        self._st_alert = "rb_flash_2"
+        self._alert_period_ms = period_ms
+        self._alert_steps_left = cycles * 2
+        self._alert_rb_phase = 0
+        self._timer_stop()
+        self._set_rgb(1, 0, 0)
+        self._timer.start(self._alert_period_ms, 0, self._timer_cb)
+
     def _timer_cb(self, *args, **kwargs):
         EventMesh.publish("publish_AI_btnInfo", {"func": self._timer_cb_})
 
@@ -277,11 +304,38 @@ class AI_led(Abstract):
                 self._set_rgb(0, 0, 1)
                 self._bg_phase = 0
             return
+        if self._mode == "alert_rb":
+            self._alert_steps_left -= 1
+            if self._alert_steps_left <= 0:
+                self._st_alert = None
+                try:
+                    self._apply_by_table()
+                except Exception:
+                    self._set_off()
+                return
+            if self._alert_rb_phase == 0:
+                self._set_rgb(0, 0, 1)
+                self._alert_rb_phase = 1
+            else:
+                self._set_rgb(1, 0, 0)
+                self._alert_rb_phase = 0
+            try:
+                self._timer.start(self._alert_period_ms, 0, self._timer_cb)
+            except Exception:
+                pass
+            return
 
     def set_led_state(self, event=None, msg=None):
-        # msg: dict {ai_state, charging, battery, signal}
+        # msg: dict {ai_state, charging, battery, signal, alert}
         try:
             if isinstance(msg, dict):
+                if "alert" in msg:
+                    aval = msg.get("alert")
+                    if aval in ("rb_flash_2", "rb_flash", "rb_flash2"):
+                        self._set_alert_rb_flash(2, 250)
+                        return
+                    if aval in (None, "", "none", "clear"):
+                        self._st_alert = None
                 if "ai_state" in msg:
                     self._st_ai = msg.get("ai_state") or "idle"
                 if "charging" in msg:
@@ -292,9 +346,37 @@ class AI_led(Abstract):
                     self._st_signal = msg.get("signal") or "normal"
         except Exception:
             pass
+        if self._mode == "alert_rb":
+            return
+        if self._led_state_debug_enabled() and isinstance(msg, dict):
+            try:
+                print("[LED] set_led_state in={}".format(msg))
+            except Exception:
+                pass
         self._apply_by_table(msg)
 
+    def _led_state_debug_enabled(self):
+        try:
+            if "vc_config" in globals() and vc_config:
+                return vc_config.get_bool("LED_STATE_DEBUG", True)
+        except Exception:
+            pass
+        return True
+
+    def _led_log(self, tag, extra=""):
+        if not self._led_state_debug_enabled():
+            return
+        try:
+            print("[LED] {} ai={} chg={} bat={} sig={} -> mode={} period_ms={} {}".format(
+                tag, self._st_ai, self._st_charging, self._st_battery, self._st_signal,
+                self._mode, self._period_ms, extra))
+        except Exception:
+            pass
+
     def _apply_by_table(self, _msg=None):
+        if self._mode == "alert_rb":
+            return
+        _prev_mode = self._mode
         # P1~P3:AI 阶段(播放 > 思考 > 聆听)
         try:
             if self._st_ai == "playing":
@@ -305,8 +387,14 @@ class AI_led(Abstract):
                 self._set_bg_alt(500)
                 return
             if self._st_ai == "listening":
-                # 「慢闪」:整颗亮灭整周期约 2×period_ms → period_ms=1000 约 2 s 一亮灭
-                self._set_blue_blink(1000)
+                # 长对话蓝慢闪(≈1s);单对话 PTT 蓝快闪(≈2次/s,REQ-AI-07)
+                period_ms = 1000
+                try:
+                    if EventMesh.publish("get_storage_config", "VC_mode") == 1:
+                        period_ms = 250
+                except Exception:
+                    pass
+                self._set_blue_blink(period_ms)
                 return
         except Exception:
             pass
@@ -341,26 +429,36 @@ class AI_led(Abstract):
                 return
         except Exception:
             pass
-        # 空闲待机:绿灯慢闪(亮 LED_IDLE_GREEN_BLINK_MS / 灭 LED_IDLE_GREEN_BLINK_OFF_MS;未配 OFF 则与亮时间对称)
+        # 空闲待机:默认灭灯(REQ-BAT-01 / GAP-005 方案甲);LED_IDLE_MODE=green_blink 可恢复旧工程绿灯慢闪
         try:
-            idle_on = 1000
+            idle_mode = "off"
             if "vc_config" in globals() and vc_config:
-                idle_on = int(vc_config.get_int("LED_IDLE_GREEN_BLINK_MS", 1000))
-            idle_off = idle_on
-            if "vc_config" in globals() and vc_config:
-                _ioff = vc_config.get_attr("LED_IDLE_GREEN_BLINK_OFF_MS", None)
-                if _ioff is not None:
-                    try:
-                        idle_off = int(_ioff)
-                    except Exception:
-                        idle_off = idle_on
-            if idle_on < 400:
-                idle_on = 400
-            if idle_off < 400:
-                idle_off = 400
-            self._set_green_blink(idle_on, idle_off)
+                _im = vc_config.get_attr("LED_IDLE_MODE", None)
+                if _im is not None:
+                    idle_mode = str(_im).strip().lower()
+            if idle_mode in ("green_blink", "blink", "green"):
+                idle_on = 1000
+                if "vc_config" in globals() and vc_config:
+                    idle_on = int(vc_config.get_int("LED_IDLE_GREEN_BLINK_MS", 1000))
+                idle_off = idle_on
+                if "vc_config" in globals() and vc_config:
+                    _ioff = vc_config.get_attr("LED_IDLE_GREEN_BLINK_OFF_MS", None)
+                    if _ioff is not None:
+                        try:
+                            idle_off = int(_ioff)
+                        except Exception:
+                            idle_off = idle_on
+                if idle_on < 400:
+                    idle_on = 400
+                if idle_off < 400:
+                    idle_off = 400
+                self._set_green_blink(idle_on, idle_off)
+            else:
+                self._set_off()
         except Exception:
-            self._set_green_blink(1000, 2000)
+            self._set_off()
+        if self._mode != _prev_mode:
+            self._led_log("apply_table", "in={}".format(_msg))
 
     def get_led_debug_state(self, event=None, msg=None):
         # 供按键/状态机打印当前 LED 聚合状态与最终模式
@@ -465,8 +563,8 @@ class _BatteryCfg(object):
                 self.charge_full_mv = int(vc_config.get_int("VBATT_CHARGE_FULL_MV", 4290))
                 self.low_mv = int(vc_config.get_int("VBATT_LOW_MV", 3699))
                 self.empty_mv = int(vc_config.get_int("VBATT_EMPTY_MV", 3629))
-                self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 20))
-                self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 5))
+                self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 30))
+                self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 10))
                 _ocv_cfg = _parse_battery_ocv_map(vc_config.get_attr("BATTERY_OCV_MAP", None))
                 if _ocv_cfg:
                     self.ocv_map = _ocv_cfg
@@ -2257,10 +2355,10 @@ class AI_dev(Abstract):
         return int(soc)
     def audio_montage_play(self,event,msg):
         self.wakeup_BT()
-        # 抢占规则:流播放(TTS)可打断本地;本地提示音不可打断流播放
+        # 仅服务端 TTS PCM 在播时跳过;FSM 处于 TtsReceiving 但尚未出声时不挡本地 montage
         try:
-            if EventMesh.publish("get_vc_stream_active") == 1:
-                print("[audio-arb] skip local montage_play during stream")
+            if EventMesh.publish("get_vc_tts_playback_active") == 1:
+                print("[audio-arb] skip local montage_play during tts_pcm")
                 return
         except Exception:
             pass
@@ -3364,10 +3462,16 @@ class VCStateMachine:
 
 
 class AI_VC(Abstract):
+    def get_vc_tts_playback_active(self, *args, **kwargs):
+        try:
+            return 1 if self._vc_tts_playback_active() else 0
+        except Exception:
+            return 0
+
     def get_vc_stream_active(self, *args, **kwargs):
         # 供本地提示音侧查询:TTS 流播放期间本地不可抢占。
         try:
-            if getattr(self, "VC_Play_flag", 0) == 1:
+            if self._vc_tts_playback_active():
                 return 1
         except Exception:
             pass
@@ -3424,6 +3528,20 @@ class AI_VC(Abstract):
             pass
         return False
 
+    def _vc_tts_playback_active(self):
+        """仅服务端 TTS/PCM 流在播(VC_Play_Start);aud.play 本地短音不算。"""
+        try:
+            if getattr(self, "VC_Play_flag", 0) == 1:
+                return True
+        except Exception:
+            pass
+        try:
+            if getattr(self, "play_pcm", None) is not None:
+                return True
+        except Exception:
+            pass
+        return False
+
     def _vc_vad_uplink_stop_eligible(self):
         # 对应旧 VC_state >= SpeechStart
         try:
@@ -3573,7 +3691,7 @@ class AI_VC(Abstract):
                 except Exception:
                     pass
                 try:
-                    EventMesh.publish("set_led_state", {"ai_state": "idle"})
+                    self._vc_publish_led("idle", "ptt_short_press/stop_tts")
                 except Exception:
                     pass
                 return True
@@ -3581,15 +3699,62 @@ class AI_VC(Abstract):
             pass
         return True
 
-    def _ai_key_press_long_dialogue_tts_busy(self):
-        """长对话:播放态短按打断续聊(RequestToSpeak)。"""
-        if not self._vc_tts_pipeline_busy():
+    def _ai_key_press_long_dialogue_interrupt(self):
+        """长对话:TTS/播放忙或 turn==Thinking 时短按 → RequestToSpeak(先 cancel,canceled 后再开麦)。"""
+        if self.VC_mode != 0:
             return False
-        print("【打断播放】设备正在说话,仅中断播放,不发起新对话")
+        try:
+            if getattr(self, "AI_client_VC_isconnect", 0) != 1 or getattr(self, "_vc_session_started", 0) != 1:
+                return False
+        except Exception:
+            return False
+        need_cancel = False
+        if self._vc_tts_pipeline_busy():
+            need_cancel = True
+        else:
+            try:
+                if getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking:
+                    need_cancel = True
+            except Exception:
+                pass
+        if not need_cancel:
+            return False
+        print("[KEY] long interrupt -> RequestToSpeak (cancel then uplink)")
         self.is_ruppt_tts = 1
         EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})
         return True
 
+    def _ai_key_press_long_dialogue_wake(self):
+        """长对话未连接或未 started:直接 AI_VC_start,勿 Close+sleep。"""
+        if self.VC_mode != 0:
+            return False
+        if getattr(self, "voice_flag", 0) == 1 or getattr(self, "meet_flag", 0) == 1 or getattr(self, "trans_flag", 0) == 1:
+            return False
+        try:
+            if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+                return False
+        except Exception:
+            pass
+        print("[KEY] long wake AI_VC_start (no Close)")
+        try:
+            self.Close_flag = 0
+        except Exception:
+            pass
+        self.get_config_timer_count = 0
+        self.key_state = 1
+        self.AI_TTS_str = ""
+        self.AI_ASR_str = ""
+        self.AI_ASR_str_tmp = ""
+        self.AI_VC_start()
+        self.AI_key_long_press_flag = 0
+        return True
+
+    def _vc_led_alert_rb_flash(self):
+        try:
+            EventMesh.publish("set_led_state", {"alert": "rb_flash_2"})
+        except Exception:
+            pass
+
     def _vc_clear_stale_record_state(self, reason=""):
         """record_state_flag 非 0 但可能无真实录音时先停录复位,避免 uplink 门控拒开麦。"""
         try:
@@ -3607,6 +3772,11 @@ class AI_VC(Abstract):
             return False
         try:
             if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+                # 模式切换提示音会置 Close_flag=1;用户再短按须恢复可开麦
+                try:
+                    self.Close_flag = 0
+                except Exception:
+                    pass
                 self._vc_clear_stale_record_state("key.press/continue")
                 self.request_uplink_start("key.press/continue")
                 return True
@@ -3618,7 +3788,16 @@ class AI_VC(Abstract):
         """单对话 PTT 松手:仅在上行录音中时 end_speech;禁止误触发 AI_VC_start(否则新 sid / cancel 竞态)。"""
         if self.AI_key_long_press_flag == 1:
             if getattr(self, "VC_record_state_flag", 0) != 0:
-                print("AI_key_release:asr_str:{}".format(self.AI_ASR_str))
+                try:
+                    gap = 200
+                    if "vc_config" in globals() and vc_config:
+                        gap = int(vc_config.get_int("PTT_RELEASE_END_SPEECH_DELAY_MS", 200))
+                    if gap > 0:
+                        utime.sleep_ms(gap)
+                except Exception:
+                    pass
+                print("[PTT] release end_speech asr_str={!r} rec={}".format(
+                    self.AI_ASR_str, getattr(self, "VC_record_state_flag", -1)))
                 self.AI_client_VC_end_speech()
             else:
                 self.AI_key_long_press_flag = 0
@@ -3651,6 +3830,145 @@ class AI_VC(Abstract):
     # 语义:
     # - 长按:无论当前处于 TTS/Thinking/任何中间态,立即打断并进入新一轮录音上行
     # - 松开:停止录音并等待服务端返回(由 AI_client_VC_end_speech 触发)
+    def _ptt_end_led_suppress(self):
+        try:
+            self._vc_play_end_suppress_idle_led = 0
+        except Exception:
+            pass
+
+    def _vc_enqueue_btn(self, func, data=None):
+        """统一经 publish_AI_btnInfo 入队,在 btnInfo_queue_thread 串行执行。"""
+        try:
+            if data is not None:
+                EventMesh.publish("publish_AI_btnInfo", {"func": func, "data": data})
+            else:
+                EventMesh.publish("publish_AI_btnInfo", {"func": func})
+        except Exception:
+            pass
+
+    def _vc_ptt_cancel_wait_ms(self):
+        try:
+            if "vc_config" in globals() and vc_config:
+                return int(vc_config.get_int("PTT_CANCEL_WAIT_MS", 2500))
+        except Exception:
+            pass
+        return 2500
+
+    def _vc_ptt_need_interrupt_wait(self):
+        """单对话:服务端仍在思考/收 TTS/播报时,开录前须先 cancel 并等 canceled。"""
+        try:
+            if getattr(self, "VC_mode", -1) != 1:
+                return False
+            turn = getattr(self.fsm, "turn", VCTurnState.Idle)
+            if self._vc_tts_pipeline_busy():
+                return True
+            if turn in (
+                VCTurnState.Thinking, VCTurnState.TtsReceiving,
+                VCTurnState.Playing, VCTurnState.AwaitPlayedAck,
+            ):
+                return True
+        except Exception:
+            pass
+        return False
+
+    def _vc_begin_ptt_cancel_wait(self, reason="ptt_interrupt"):
+        try:
+            self._ptt_pending_arm = 1
+            self._vc_serial_canceling = 1
+            try:
+                self._vc_cancel_deadline_ms = utime.ticks_add(
+                    utime.ticks_ms(), self._vc_ptt_cancel_wait_ms())
+            except Exception:
+                self._vc_cancel_deadline_ms = 0
+            json_data = {"event": "cancel", "payload": {}}
+            self.AI_client_VC_send(ujson.dumps(json_data))
+            try:
+                self.fsm.reset_rx_phases()
+            except Exception:
+                pass
+            self._drop_tts_audio = 1
+            print("[VC][serial] cancel_wait reason={} turn={}".format(
+                reason, getattr(getattr(self, "fsm", None), "turn", "?")))
+        except Exception:
+            pass
+
+    def _vc_on_ptt_cancel_settled(self, via="canceled"):
+        """btn 线程:cancel 已收尾,执行挂起的 PTT 开录。"""
+        try:
+            if not getattr(self, "_vc_serial_canceling", 0) and not getattr(self, "_ptt_pending_arm", 0):
+                return
+            self._vc_serial_canceling = 0
+            self._vc_cancel_deadline_ms = 0
+            if getattr(self, "_ptt_pending_arm", 0):
+                self._ptt_pending_arm = 0
+                print("[VC][serial] ptt_arm after {}".format(via))
+                self._ptt_arm_uplink()
+        except Exception:
+            pass
+
+    def _vc_tick_serial_cancel_timeout(self):
+        if not getattr(self, "_vc_serial_canceling", 0):
+            return
+        try:
+            dl = getattr(self, "_vc_cancel_deadline_ms", 0)
+            if dl and utime.ticks_diff(utime.ticks_ms(), dl) >= 0:
+                print("[VC][serial] cancel_wait timeout -> arm")
+                self._vc_on_ptt_cancel_settled(via="timeout")
+        except Exception:
+            pass
+
+    def _vc_fsm_dispatch_cb(self, data):
+        """btn 线程:处理服务端 FSM 事件(单对话串行)。"""
+        try:
+            ev, jsload = data
+        except Exception:
+            return
+        try:
+            actions = self.fsm.on_server_event(ev, jsload)
+            self._fsm_apply_actions(actions)
+        except Exception as e:
+            usys.print_exception(e)
+        if ev == "canceled":
+            self._vc_on_ptt_cancel_settled(via="canceled")
+
+    def _vc_dispatch_server_event(self, ev, jsload_data=None):
+        if getattr(self, "VC_mode", -1) == 1:
+            self._vc_enqueue_btn(self._vc_fsm_dispatch_cb, (ev, jsload_data))
+        else:
+            try:
+                self._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))
+            except Exception:
+                pass
+
+    def _ptt_arm_uplink(self, t0=0):
+        """btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""
+        try:
+            self._ptt_audio_created = 0
+        except Exception:
+            pass
+        try:
+            self.AI_ASR_flag = 0
+        except Exception:
+            pass
+        try:
+            self.AI_client_VC_send_speech()
+        except Exception:
+            pass
+        try:
+            self.VC_Speech_Start_now()
+        except Exception:
+            pass
+        try:
+            self._vc_publish_led("listening", "ptt_begin/recording")
+        except Exception:
+            pass
+        self._ptt_end_led_suppress()
+        try:
+            if t0:
+                print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))
+        except Exception:
+            pass
+
     def ptt_begin(self, reason="key.long_press"):
         try:
             if self.VC_mode != 1:
@@ -3658,9 +3976,10 @@ class AI_VC(Abstract):
         except Exception:
             return
 
-        # PTT 新一轮开始:清理“提示音播完退出”等标志,避免 Close_flag 残留影响当前回合
+        # PTT 新一轮开始:清理模式切换/提示音退出标志
         try:
             self.Close_flag = 0
+            self._ptt_await_server = 0
         except Exception:
             pass
         # 打断上一段 TTS 后可能仍有残留音频帧下发:短时间丢弃,避免“断断续续继续播”
@@ -3675,6 +3994,16 @@ class AI_VC(Abstract):
         except Exception:
             t0 = 0
 
+        # 先亮聆听灯,再停播;VC_init/VC_Play_End 全程抑制 idle,避免开录前灭灯
+        try:
+            self._vc_play_end_suppress_idle_led = 1
+        except Exception:
+            pass
+        try:
+            self._vc_publish_led("listening", "ptt_begin/pre")
+        except Exception:
+            pass
+
         # 本地先停播并清空队列,避免“边播边录/回声触发/状态机乱跳”
         try:
             self.VC_init()
@@ -3687,6 +4016,10 @@ class AI_VC(Abstract):
                 self.VC_Play_End()
         except Exception:
             pass
+        try:
+            self._vc_publish_led("listening", "ptt_begin/armed")
+        except Exception:
+            pass
 
         # 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错
         try:
@@ -3695,23 +4028,13 @@ class AI_VC(Abstract):
         except Exception:
             pass
 
-        # 服务端仍在出 TTS/思考:发 cancel 打断本轮回复(不换 sid),避免与新一轮 audio.create 竞态
+        # 已在等 canceled:只刷新「待开录」,勿重复 audio.create
         try:
-            turn = getattr(self.fsm, "turn", VCTurnState.Idle)
-            if self._vc_tts_pipeline_busy() or turn in (
-                VCTurnState.Thinking, VCTurnState.TtsReceiving, VCTurnState.Playing
-            ):
-                json_data = {"event": "cancel", "payload": {}}
-                self.AI_client_VC_send(ujson.dumps(json_data))
-                try:
-                    self.fsm.reset_rx_phases()
-                except Exception:
-                    pass
-                try:
-                    self._drop_tts_audio = 1
-                except Exception:
-                    pass
-                print("[PTT] cancel before new uplink turn={}".format(turn))
+            if getattr(self, "_vc_serial_canceling", 0):
+                self._ptt_pending_arm = 1
+                print("[PTT] cancel in progress, refresh pending arm")
+                self._ptt_end_led_suppress()
+                return
         except Exception:
             pass
 
@@ -3720,6 +4043,7 @@ class AI_VC(Abstract):
             if getattr(self, "AI_client_VC_isconnect", 0) != 1:
                 self._ptt_force_record_on_started = 1
                 self.AI_VC_start()
+                self._ptt_end_led_suppress()
                 return
         except Exception:
             self._ptt_force_record_on_started = 1
@@ -3727,6 +4051,7 @@ class AI_VC(Abstract):
                 self.AI_VC_start()
             except Exception:
                 pass
+            self._ptt_end_led_suppress()
             return
 
         # 会话还没 initialized:先发 Start,请求服务端进入 started
@@ -3741,39 +4066,110 @@ class AI_VC(Abstract):
                 self.AI_client_VC_start_requset()
             except Exception:
                 pass
+            self._ptt_end_led_suppress()
+            return
+
+        # 服务端仍在思考/TTS:先 cancel,在 btn 队列收到 canceled 后再开录(方案甲串行)
+        if self._vc_ptt_need_interrupt_wait():
+            self._vc_begin_ptt_cancel_wait("ptt_begin")
+            self._ptt_end_led_suppress()
             return
 
-        # 不打 cancel:保持当前会话 sid/dialog,仅本地停播后用 audio.create 开新一轮上行
-        # (cancel 会导致服务端撤销本轮,与「打断 TTS 不换 sid」及电源键 Close 才换会话矛盾)
-        # 开录提示音仅由 VC_Speech_Start/RECORD_START_TONE_FILE 播放,此处不再叠短嘟声以免冲突
+        self._ptt_arm_uplink(t0)
+
+    def _vc_led_state_debug_enabled(self):
         try:
-            self._ptt_audio_created = 0
+            if "vc_config" in globals() and vc_config:
+                return vc_config.get_bool("LED_STATE_DEBUG", True)
         except Exception:
             pass
+        return True
+
+    def _vc_led_should_standby_idle(self):
+        """未录音且非 TTS 播中:待机/提示音/模式切换/单对话轮间应灭灯(DES-L3 idle),勿被 FSM.turn=Listening 误导。"""
         try:
-            self.AI_ASR_flag = 0
+            if getattr(self, "VC_record_state_flag", 0) != 0:
+                return False
+            if self._vc_tts_playback_active():
+                return False
+            if getattr(self, "_ptt_await_server", 0) == 1:
+                return False
+            t = getattr(self.fsm, "turn", VCTurnState.Idle)
+            if t in (
+                VCTurnState.Thinking,
+                VCTurnState.TtsReceiving,
+                VCTurnState.Playing,
+                VCTurnState.AwaitPlayedAck,
+            ):
+                return False
+            if getattr(self, "Close_flag", 0) == 1:
+                return True
+            if getattr(self, "VC_mode", -1) == 1:
+                return True
+            k = getattr(self, "_last_tts_kind", "") or ""
+            if k in ("prompt", "boot_prompt", "mode_switch_long", "mode_switch_ptt"):
+                return True
         except Exception:
             pass
+        return False
+
+    def _vc_publish_led(self, ai_state, reason=""):
         try:
-            self.AI_client_VC_send_speech()
+            if self._vc_led_state_debug_enabled():
+                print("[LED] publish ai={} reason={} VC_mode={} rec={} turn={}".format(
+                    ai_state, reason, getattr(self, "VC_mode", -1),
+                    getattr(self, "VC_record_state_flag", -1),
+                    getattr(getattr(self, "fsm", None), "turn", "?")))
         except Exception:
             pass
         try:
-            self.VC_Speech_Start_now()
+            EventMesh.publish("set_led_state", {"ai_state": ai_state})
         except Exception:
             pass
+
+    def _vc_led_sync_from_fsm(self):
+        """按 FSM/真实 TTS 播放恢复 AI 灯;本地 aud.play 短音不参与。"""
         try:
-            if t0:
-                print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))
+            if self._vc_led_should_standby_idle():
+                self._vc_publish_led("idle", "fsm_sync/standby_idle")
+                return
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
+                self._vc_publish_led("thinking", "fsm_sync/ptt_await")
+                return
+        except Exception:
+            pass
+        try:
+            if self._vc_tts_playback_active():
+                self._vc_publish_led("playing", "fsm_sync/tts")
+                return
         except Exception:
             pass
+        try:
+            t = getattr(self.fsm, "turn", VCTurnState.Idle)
+            if t in (VCTurnState.Thinking, VCTurnState.TtsReceiving):
+                self._vc_publish_led("thinking", "fsm_sync/thinking")
+            elif t in (VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
+                # 服务端已标记播放态但本地 PCM 尚未起播:仍显示思考,勿误绿灯
+                self._vc_publish_led("thinking", "fsm_sync/await_tts")
+            elif t in (VCTurnState.Listening, VCTurnState.SpeechEnded, VCTurnState.SpeechStreaming):
+                self._vc_publish_led("listening", "fsm_sync/listening")
+            else:
+                self._vc_publish_led("idle", "fsm_sync/idle")
+        except Exception:
+            try:
+                self._vc_publish_led("idle", "fsm_sync/fallback")
+            except Exception:
+                pass
 
     def _uplink_request_start(self, reason="", *args, **kwargs):
         # 统一入口:避免多处同时 start 造成冲突(尤其是 TTS/播放阶段误开麦导致回声触发与状态紊乱)
         try:
             if self.Close_flag == 1:
                 try:
-                    if reason in ("canceled", "local.play_end", "audio.played"):
+                    if reason in ("canceled", "local.play_end", "audio.played", "key.press/continue"):
                         print("[VC][uplink_ctrl] Deny START by {} cause=Close_flag".format(reason))
                 except Exception:
                     pass
@@ -3927,6 +4323,11 @@ class AI_VC(Abstract):
                 elif a[0] == "uplink_start":
                     self.request_uplink_start(a[1] if len(a) > 1 else "")
                 elif a[0] == "play_start":
+                    if getattr(self, "VC_mode", -1) == 1:
+                        if getattr(self, "_vc_serial_canceling", 0) or getattr(self, "_ptt_pending_arm", 0):
+                            if self._vc_led_state_debug_enabled():
+                                print("[VC][serial] skip play_start (cancel_wait)")
+                            continue
                     try:
                         self.fsm.on_local_play_start()
                     except Exception:
@@ -4028,9 +4429,19 @@ class AI_VC(Abstract):
         # VC 会话是否已进入 started(服务端 session initialized)
         # 重要:在收到 started 前发送音频帧会触发服务端 "session not initialized" → failed → 端侧重连死循环
         self._vc_session_started = 0
+        # 开机/提示音:本轮 started 不自动开麦(避免问候 TTS 前误进 listening)
+        self._skip_started_uplink_once = 0
+        self._last_tts_kind = ""
+        self._pending_tts_kind = ""
         # Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create
         # 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed
         self._ptt_audio_created = 0
+        # PTT:松手等服务器期间保持 thinking 勿回蓝闪聆听
+        self._ptt_await_server = 0
+        # 方案甲 btnInfo 串行:打断时先 cancel,收到 canceled(或超时)后再 audio.create
+        self._vc_serial_canceling = 0
+        self._ptt_pending_arm = 0
+        self._vc_cancel_deadline_ms = 0
         # 端侧主动发 synthesis 后到收到 tts.started 之间的窗口标志,用于阻止误开下一轮录音上行
         self._tts_pending = 0
         self._tts_pending_ms = 0
@@ -4345,6 +4756,7 @@ class AI_VC(Abstract):
         EventMesh.subscribe("get_AI_audio_queue", self.get_AI_audio_queue)
         EventMesh.subscribe("get_VC_satrt_flag", self.get_VC_satrt_flag)
         EventMesh.subscribe("get_vc_stream_active", self.get_vc_stream_active)
+        EventMesh.subscribe("get_vc_tts_playback_active", self.get_vc_tts_playback_active)
         EventMesh.subscribe("VC_error", self.VC_error)
         EventMesh.subscribe("audio_test", self.audio_test)
         EventMesh.subscribe("audio_test_close", self.audio_test_close)
@@ -4438,6 +4850,12 @@ class AI_VC(Abstract):
                     pass
                     self.AI_TTS_str = self.power_on_tone.replace("bot_name", self.bot_name)
                     print("开机提示:{}".format(self.AI_TTS_str))
+                    try:
+                        self._pending_tts_kind = "boot_prompt"
+                        self._last_tts_kind = "boot_prompt"
+                        self._skip_started_uplink_once = 1
+                    except Exception:
+                        pass
                     EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})   
 
         except Exception as e:
@@ -4628,20 +5046,34 @@ class AI_VC(Abstract):
             self.audio_play_state =1
             #EventMesh.publish("set_audio_state", 1)
             print('audio-play start.')
-            # 本地播放真正开始:统一进入 playing(绿灯态由 AI_led 表决定)
-            try:
-                EventMesh.publish("set_led_state", {"ai_state": "playing"})
-            except Exception:
-                pass
+            # 本地 aud.play(开录/思考短音等)不改动 AI 灯,仅 TTS 流由 VC_Play_Start 控绿灯
+            if self._vc_led_state_debug_enabled():
+                print("[LED] audio_cb start skip (local_play)")
+            return
         elif event == 7:
             self.audio_play_state = 0
             #EventMesh.publish("set_audio_state", 0)   
             print('audio-play finish.!!!!!!')
-            # 本地播放真正结束:统一退出 playing
             try:
-                EventMesh.publish("set_led_state", {"ai_state": "idle"})
+                if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
+                    self._vc_publish_led("thinking", "audio_cb/finish/ptt_await")
+                    return
+            except Exception:
+                pass
+            try:
+                if getattr(self, "VC_record_state_flag", 0) != 0:
+                    self._vc_publish_led("listening", "audio_cb/finish/recording")
+                    return
             except Exception:
                 pass
+            try:
+                if self._vc_tts_playback_active():
+                    if self._vc_led_state_debug_enabled():
+                        print("[LED] audio_cb finish skip (tts_playback)")
+                    return
+            except Exception:
+                pass
+            self._vc_led_sync_from_fsm()
     def set_tts_volume(self,event,msg):
         EventMesh.publish("set_storage_config",["VC_TTS_volume",msg])#音量配置
         self.VC_TTS_volume = msg
@@ -4745,13 +5177,19 @@ class AI_VC(Abstract):
                 return
         except Exception:
             pass
-        if self._ai_key_press_long_dialogue_tts_busy():
+        if self._ai_key_press_long_dialogue_interrupt():
             return
         try:
             if self._ai_key_press_long_dialogue_when_session_ready():
                 return
         except Exception:
             pass
+        try:
+            if self._ai_key_press_long_dialogue_wake():
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)
+                return
+        except Exception:
+            pass
 
         EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
         if EventMesh.publish("get_sos_flag")== 1:
@@ -4772,30 +5210,7 @@ class AI_VC(Abstract):
             return
         
         EventMesh.publish("stop_voice_record") #关闭语音录音
-        print("AI_key_press")
-        self.get_config_timer_count = 0
-        self.key_state = 1
-            # EventMesh.publish("audio_montage_play",["chenggong.mp3"])  
-            # self.chenggong_tone() #播放按键音
-        utime.sleep_ms(500)
-        self.Close()
-
-        # if dev_type == "7PIN":
-        #     self.chenggong_tone()
-        #self.aud.aud_tone_play(10,200)#播放按键音
-        self.AI_TTS_str = ""#"你好,有什么吩咐"
-        self.AI_ASR_str = ""
-        self.AI_ASR_str_tmp = ""
-        # if self.VC_state != VC_State_None:
-        #     EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) 
-        #     EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
-        self._uplink_request_stop("BT_AI_key_press/compat")
-        self._uplink_request_start("BT_AI_key_press/compat")
-        self.AI_VC_start() 
-        
-        
-        # self.Close()
-        # self.AI_VC_start()
+        print("AI_key_press: unhandled mode branch")
         self.AI_key_long_press_flag = 0
     def BT_AI_key_press(self, event=None, msg = None):#按键按下
         EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
@@ -4903,6 +5318,10 @@ class AI_VC(Abstract):
     def pwk_key_double_press(self,event,msg):#pwk双击
         print("come 双击")
         # 切换模式:必须打断当前对话/录音/播放,回到 idle,等待按键触发新一轮
+        try:
+            self.Close_flag = 1
+        except Exception:
+            pass
         try:
             EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
         except Exception:
@@ -5023,21 +5442,9 @@ class AI_VC(Abstract):
                               self.VC_record_thread_id))
                 except Exception:
                     pass
-                # 录音开始:聆听灯(蓝慢闪)
+                # 录音开始:聆听灯(PTT 蓝快闪 / 长对话蓝慢闪)
                 try:
-                    EventMesh.publish("set_led_state", {"ai_state": "listening"})
-                except Exception:
-                    pass
-                # 单对话长按开麦:可选开录提示音(RECORD_START_TONE_FILE)
-                try:
-                    if self.VC_mode == 1 and "vc_config" in globals() and vc_config:
-                        rtf = vc_config.get_attr("RECORD_START_TONE_FILE", None)
-                        if rtf:
-                            try:
-                                if EventMesh.publish("get_vc_stream_active") != 1:
-                                    self.aud.play(1, 1, rtf)
-                            except Exception:
-                                pass
+                    self._vc_publish_led("listening", "VC_Speech_Start")
                 except Exception:
                     pass
                 
@@ -5098,9 +5505,14 @@ class AI_VC(Abstract):
                 self.AI_key_long_press_flag = 0
         except Exception:
             pass
-        # å½\95é\9f³å\81\9cæ­¢ï¼\9aå\85³é\97­è\81\86å\90¬ç\81¯ï¼\88æ\81¢å¤\8d baseï¼\89
+        # å½\95é\9f³å\81\9cæ­¢ï¼\9aå\8d\95对è¯\9dè¿\9bå\85¥æ\80\9dè\80\83æ\80\81ï¼\88ç­\89æ\9c\8då\8a¡å\99¨ï¼\89ï¼\8cå\8b¿ç»§ç»­è\93\9dé\97ªè\81\86å\90¬
         try:
-            EventMesh.publish("set_led_state", {"ai_state": "idle"})
+            if getattr(self, "VC_mode", -1) == 1:
+                self._ptt_await_server = 1
+                self._vc_publish_led("thinking", "VC_Speech_End/ptt")
+            else:
+                self._ptt_await_server = 0
+                self._vc_publish_led("idle", "VC_Speech_End")
         except Exception:
             pass
         print("[VC] Speech_End record_state={}".format(self.VC_record_state_flag))
@@ -5786,7 +6198,7 @@ class AI_VC(Abstract):
                         try:
                             ev = jsload_data.get("event", "")
                             if ev:
-                                self._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))
+                                self._vc_dispatch_server_event(ev, jsload_data)
                         except Exception:
                             pass
                         
@@ -5808,14 +6220,23 @@ class AI_VC(Abstract):
                             self._turn_has_tts_started = 0
                             self._llm_completed_ms = 0
                             if self.VC_mode == 0 and getattr(self, "Close_flag", 0) == 0:
-                                # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后
-                                # 出现首轮无音频上行导致“第一次对话没有”的情况。
-                                if self.record_data_save_flag == 0:
-                                    if self.VC_record_state_flag == 0:
-                                        self.request_uplink_start("server.started")
-                                    else:
-                                        self._vc_clear_stale_record_state("server.started")
-                                        self.request_uplink_start("server.started/retry")
+                                _skip_uplink = 0
+                                try:
+                                    if getattr(self, "_skip_started_uplink_once", 0) == 1:
+                                        self._skip_started_uplink_once = 0
+                                        _skip_uplink = 1
+                                        print("[VC][uplink_ctrl] Deny START by server.started cause=boot_prompt")
+                                except Exception:
+                                    pass
+                                if not _skip_uplink:
+                                    # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后
+                                    # 出现首轮无音频上行导致“第一次对话没有”的情况。
+                                    if self.record_data_save_flag == 0:
+                                        if self.VC_record_state_flag == 0:
+                                            self.request_uplink_start("server.started")
+                                        else:
+                                            self._vc_clear_stale_record_state("server.started")
+                                            self.request_uplink_start("server.started/retry")
                             
                             print("VC:Started!!!!!!!!\r\n")
                             print("TTS音频为:{},语音下发地址为:{}".format(self.AI_TTS_str,self.AI_new_voice_url))
@@ -5935,6 +6356,7 @@ class AI_VC(Abstract):
                             print("[VC][ASR] {}".format(self.AI_ASR_str))
                             if self.AI_ASR_str == "":#没识别到
                                 self.AI_TTS_str = "我没听清,请再说一遍"
+                                self._vc_led_alert_rb_flash()
                         elif jsload_data["event"] == "asr.completed" :#asr 终止的标识 
                             self._turn_has_asr_completed = 1
                             # 本地 think 提示:每轮 ASR 最多一次,且不得与流式 TTS/PCM 播放竞态(避免长对话偶发连播「滴」)
@@ -5944,7 +6366,7 @@ class AI_VC(Abstract):
                                 else:
                                     stream_on = False
                                     try:
-                                        stream_on = EventMesh.publish("get_vc_stream_active") == 1
+                                        stream_on = EventMesh.publish("get_vc_tts_playback_active") == 1
                                     except Exception:
                                         stream_on = False
                                     pcm_busy = getattr(self, "play_pcm", None) is not None
@@ -6005,6 +6427,10 @@ class AI_VC(Abstract):
                                 if self.VC_record_state_flag != 0 and self._vc_vad_uplink_stop_eligible():
                                     self.request_uplink_stop("vad.silence")
                         elif jsload_data["event"] == "llm.created" :#思考(LLM)
+                            try:
+                                self._ptt_await_server = 0
+                            except Exception:
+                                pass
                             try:
                                 EventMesh.publish("set_led_state", {"ai_state": "thinking"})
                             except Exception:
@@ -6032,6 +6458,10 @@ class AI_VC(Abstract):
                                 if self.VC_record_state_flag == 1:#llm返回数据还没停止,就先停止
                                     self.request_uplink_stop("llm.delta")
                         elif jsload_data["event"] == "tts.started" :#tts开始
+                            try:
+                                self._ptt_await_server = 0
+                            except Exception:
+                                pass
                             self._server_vad_silence = 1
                             self._turn_has_tts_started = 1
                             self._tts_pending = 0
@@ -6057,7 +6487,11 @@ class AI_VC(Abstract):
                                         self._last_tts_kind = k
                                     else:
                                         cur = getattr(self, "_last_tts_kind", "") or ""
-                                        if cur in ("mode_switch_long", "mode_switch_ptt"):
+                                        if cur in (
+                                            "mode_switch_long",
+                                            "mode_switch_ptt",
+                                            "boot_prompt",
+                                        ):
                                             pass
                                         elif not cur:
                                             self._last_tts_kind = "prompt"
@@ -6440,7 +6874,13 @@ class AI_VC(Abstract):
                 self.AI_VC_send_data_queue.clear()
             if self.AI_ASR_send_data_queue.empty()!=True:
                 self.AI_ASR_send_data_queue.clear()
-            EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+            try:
+                if is_close_VC:
+                    EventMesh.publish("set_led_state", {"ai_state": "idle"})
+                else:
+                    EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+            except Exception:
+                pass
             self.AI_ASR_send_buf_len = 0
             self._uplink_request_stop("play_tts_cb")
             self.websocket_deconnect_VC()
@@ -7109,7 +7549,7 @@ class AI_VC(Abstract):
                     pass
                 self.VC_Play_flag = 1
                 try:
-                    EventMesh.publish("set_led_state", {"ai_state": "playing"})
+                    self._vc_publish_led("playing", "VC_Play_Start/tts")
                 except Exception:
                     pass
             except Exception as e:
@@ -7149,9 +7589,15 @@ class AI_VC(Abstract):
         if self.AI_audio_queue.empty()!=True:
             self.AI_audio_queue.clear()
 
-        # 播放结束后:退出“播报中”绿灯态(恢复 base)。
+        # TTS 播完:按真实 FSM/录音态恢复灯;本地短音不走此路径
         try:
-            EventMesh.publish("set_led_state", {"ai_state": "idle"})
+            if getattr(self, "_vc_play_end_suppress_idle_led", 0):
+                if self._vc_led_state_debug_enabled():
+                    print("[LED] VC_Play_End skip led (suppress)")
+            elif getattr(self, "VC_record_state_flag", 0) != 0:
+                self._vc_publish_led("listening", "VC_Play_End/recording")
+            else:
+                self._vc_led_sync_from_fsm()
         except Exception:
             pass
         # 关键日志:定位“tts 结束绿灯仍不灭”究竟是 base 态(如 charging/full)还是 playing 未退出
@@ -7237,13 +7683,18 @@ class AI_VC(Abstract):
                                 age_ms, restart_threshold_ms))
                         except Exception:
                             pass
+                        self._vc_led_alert_rb_flash()
                         # thinking_timeout 策略:重启会话(而不是抢开麦),避免干扰服务端 LLM/TTS 流程
                         EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
         except Exception:
             pass
 
-    def _1S_timer_cb_(self):    
-    
+    def _1S_timer_cb_(self):
+        try:
+            self._vc_tick_serial_cancel_timeout()
+        except Exception:
+            pass
+
         EventMesh.publish("system_sleep_rest")#防止黑屏
         # --- Level 1:仅长对话(VC_mode==0),无业务下行 idle_sec 后结束会话(需再次按键进入)
         # close_timerout:保活清除策略见 _vc_rx_resets_long_idle_timer(排除文本 ping/pong、heartbeat 等)
@@ -8399,6 +8850,10 @@ class AI_VC(Abstract):
         self._tts_pending_ms = 0
         self._vc_session_started = 0
         self._ptt_audio_created = 0
+        try:
+            self._skip_started_uplink_once = 0
+        except Exception:
+            pass
         # Power 短按停止对话后,再次按 AI 需要新建 dialog_id:清理本轮 dialog/session 标识
         try:
             self._cur_dialog_id = ""
index 1978dd3..243f326 100644 (file)
@@ -40,6 +40,7 @@ LANG = "zh"
 # 硬件差异项(由每个设备的 default_config 固定)\r
 # --------------------\r
 LED_DEBUG = True\r
+LED_STATE_DEBUG = True\r
 KEY_LED_DEBUG = True\r
 HW_STATUS_DEBUG = True\r
 \r
@@ -51,8 +52,8 @@ USE_SIMPLE_VBATT_THRESH = False
 VBATT_CHARGE_FULL_MV = 4290   # USB 在位且电压≥此值视为充满(get_battery / AI_hw_status / AI_dev 共用)\r
 VBATT_LOW_MV = 3699           # 低电电压门限;simple 模式与 AI_dev 低电提示用\r
 VBATT_EMPTY_MV = 3629         # 空电电压门限;AI_dev 低电关机等\r
-BATTERY_LOW_SOC_PERCENT = 20  # OCV 模式:soc < 此值 → battery=low\r
-BATTERY_EMPTY_SOC_PERCENT = 5 # OCV 模式:soc < 此值 → battery=empty\r
+BATTERY_LOW_SOC_PERCENT = 30  # OCV 模式:soc < 此值 → battery=low(REQ-BAT-02:10%~30%)\r
+BATTERY_EMPTY_SOC_PERCENT = 10  # OCV 模式:soc < 此值 → battery=empty(REQ-BAT-03:<10%)\r
 HW_STATUS_PERIOD_MS = 10000\r
 SIGNAL_WEAK_CSQ = 10\r
 \r
@@ -141,6 +142,9 @@ BT_OTA_ENABLED = False
 # 是否在“开始上传语音”时播放成功提示音(原逻辑:非 7PIN/7PIN_1_5/7PIN_coze 才播)\r
 UPLOAD_START_TONE_ENABLED = False\r
 \r
+# 正常待机 idle 灯效:off=灭灯(REQ-BAT-01);green_blink=旧工程绿灯慢闪\r
+LED_IDLE_MODE = "off"\r
+\r
 # LLM thinking 阶段 LED 策略:flicker_blue_green / open_red_green\r
 THINKING_LED_MODE = "flicker_blue_green"\r
 \r
@@ -186,8 +190,13 @@ KEY_PRESS_TONE_FILE = None
 # AI 键按下时:延迟播放按键音(ms),规避蓝牙刚连上不出声等(与 7PIN_1_5 一致)\r
 KEY_PRESS_TONE_DELAY_MS = 300\r
 \r
-# 单对话长按开麦:短提示音路径(与短按 KEY_PRESS_TONE 分离,见 ai_VC)\r
-RECORD_START_TONE_FILE = "U:/static/start.mp3"\r
+# 单对话长按开麦:开录提示音(None=不播,流畅优先;可设 U:/static/start.mp3)\r
+RECORD_START_TONE_FILE = None\r
+RECORD_START_TONE_GAP_MS = 180\r
+# 单对话松手后延迟(ms)再发 audio.completed,利于尾包/ASR 收敛\r
+PTT_RELEASE_END_SPEECH_DELAY_MS = 200\r
+# 单对话打断:发 cancel 后等待 canceled 的最长时间(ms),超时仍开录\r
+PTT_CANCEL_WAIT_MS = 2500\r
 \r
 # PTT 提示语(单对话模式,未长按时的提示;是什么语言就写什么)\r
 PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
index 1978dd3..243f326 100644 (file)
@@ -40,6 +40,7 @@ LANG = "zh"
 # 硬件差异项(由每个设备的 default_config 固定)\r
 # --------------------\r
 LED_DEBUG = True\r
+LED_STATE_DEBUG = True\r
 KEY_LED_DEBUG = True\r
 HW_STATUS_DEBUG = True\r
 \r
@@ -51,8 +52,8 @@ USE_SIMPLE_VBATT_THRESH = False
 VBATT_CHARGE_FULL_MV = 4290   # USB 在位且电压≥此值视为充满(get_battery / AI_hw_status / AI_dev 共用)\r
 VBATT_LOW_MV = 3699           # 低电电压门限;simple 模式与 AI_dev 低电提示用\r
 VBATT_EMPTY_MV = 3629         # 空电电压门限;AI_dev 低电关机等\r
-BATTERY_LOW_SOC_PERCENT = 20  # OCV 模式:soc < 此值 → battery=low\r
-BATTERY_EMPTY_SOC_PERCENT = 5 # OCV 模式:soc < 此值 → battery=empty\r
+BATTERY_LOW_SOC_PERCENT = 30  # OCV 模式:soc < 此值 → battery=low(REQ-BAT-02:10%~30%)\r
+BATTERY_EMPTY_SOC_PERCENT = 10  # OCV 模式:soc < 此值 → battery=empty(REQ-BAT-03:<10%)\r
 HW_STATUS_PERIOD_MS = 10000\r
 SIGNAL_WEAK_CSQ = 10\r
 \r
@@ -141,6 +142,9 @@ BT_OTA_ENABLED = False
 # 是否在“开始上传语音”时播放成功提示音(原逻辑:非 7PIN/7PIN_1_5/7PIN_coze 才播)\r
 UPLOAD_START_TONE_ENABLED = False\r
 \r
+# 正常待机 idle 灯效:off=灭灯(REQ-BAT-01);green_blink=旧工程绿灯慢闪\r
+LED_IDLE_MODE = "off"\r
+\r
 # LLM thinking 阶段 LED 策略:flicker_blue_green / open_red_green\r
 THINKING_LED_MODE = "flicker_blue_green"\r
 \r
@@ -186,8 +190,13 @@ KEY_PRESS_TONE_FILE = None
 # AI 键按下时:延迟播放按键音(ms),规避蓝牙刚连上不出声等(与 7PIN_1_5 一致)\r
 KEY_PRESS_TONE_DELAY_MS = 300\r
 \r
-# 单对话长按开麦:短提示音路径(与短按 KEY_PRESS_TONE 分离,见 ai_VC)\r
-RECORD_START_TONE_FILE = "U:/static/start.mp3"\r
+# 单对话长按开麦:开录提示音(None=不播,流畅优先;可设 U:/static/start.mp3)\r
+RECORD_START_TONE_FILE = None\r
+RECORD_START_TONE_GAP_MS = 180\r
+# 单对话松手后延迟(ms)再发 audio.completed,利于尾包/ASR 收敛\r
+PTT_RELEASE_END_SPEECH_DELAY_MS = 200\r
+# 单对话打断:发 cancel 后等待 canceled 的最长时间(ms),超时仍开录\r
+PTT_CANCEL_WAIT_MS = 2500\r
 \r
 # PTT 提示语(单对话模式,未长按时的提示;是什么语言就写什么)\r
 PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r