整体架构优化,拆分仲裁等单独逻辑。
authorsnickersun <snickersun@163.com>
Mon, 25 May 2026 03:44:43 +0000 (11:44 +0800)
committersnickersun <snickersun@163.com>
Mon, 25 May 2026 03:44:43 +0000 (11:44 +0800)
更新操作逻辑详细设计文档
添加项目默认agent规则

29 files changed:
.cursor/rules/001-base.mdc [new file with mode: 0644]
.cursor/rules/002-design.mdc [new file with mode: 0644]
.cursor/rules/003-bugfix.mdc [new file with mode: 0644]
.cursor/rules/project.mdc [new file with mode: 0644]
1.doc/K8_diaozhui/design/AI吊坠VC架构重构与迭代计划.md [new file with mode: 0644]
1.doc/K8_diaozhui/design/AI吊坠VC模式执行手册-长对话与单对话.md [new file with mode: 0644]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)交互规范_new.md
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)详设-对照与决策.md
1.doc/K8_diaozhui/design/VC_SESSION_ARCH.md
1.doc/K8_diaozhui/design/VC实现审查与简化架构.md [new file with mode: 0644]
1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/Makefile
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/main_init.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/protocol.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/test/tts_local_mp3_concurrent_test.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_buttons.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_common.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_interrupt.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_key_router.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_media_arb.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_mode_strategy.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_notify.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_overlay.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_present.py [new file with mode: 0644]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_ux.py [new file with mode: 0644]

diff --git a/.cursor/rules/001-base.mdc b/.cursor/rules/001-base.mdc
new file mode 100644 (file)
index 0000000..bde50c0
--- /dev/null
@@ -0,0 +1,9 @@
+---\r
+alwaysApply: true\r
+---\r
+# AI 行为基础规则\r
+1. 所有代码必须符合项目架构,不随意新增文件\r
+2. 命名、注释、格式统一\r
+3. 必须做边界判断、错误处理\r
+4. 不破坏原有功能\r
+5. 回答简洁,直接给代码+关键注释
\ No newline at end of file
diff --git a/.cursor/rules/002-design.mdc b/.cursor/rules/002-design.mdc
new file mode 100644 (file)
index 0000000..0d59d0d
--- /dev/null
@@ -0,0 +1,52 @@
+---
+description: K8吊坠 VC 录音/上传/播放/LED/按键设计动作矩阵(7PIN_1_5)
+alwaysApply: true
+globs: 2.sdk/project/7PIN_1_5/**/vc_*.py,2.sdk/project/7PIN_1_5/**/protocol.py
+---
+
+# VC 录音上传与播放 — 设计动作(7PIN_1_5)
+
+**权威设计稿**:`1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md`。实现:`vc_ux` / `vc_key_router` / `vc_media_arb`。
+
+## 术语 ↔ 代码
+
+| 设计列 | 实现 |
+|--------|------|
+| **llm tts** | `play_pcm` / `tts.started` |
+| **notify tts** | `play_tts`(`vc_notify` 种类) |
+| **local play** | `_vc_local_play`、`audio_montage_play` |
+
+**仲裁**:llm 播 → notify/local skip;notify 播 → 关 llm+local;local 播 → 关 llm+notify;边录边播 local **不停录**。
+
+## 录音 / LED
+
+- **开始上传**:以首帧 PCM 上行(`VC_Speech_Start_now`)为起点;此前三路 media end。
+- **结束**:VAD → `asr.completed`;PTT 松手 `audio.completed`。
+- **灯**:上传→listening;ASR 后 LLM→thinking;`tts.started`→playing;否则 idle。
+
+## 按键 — AI 键(`VC_mode` 0/1)
+
+| UX | 长对话单击 | PTT 单击 | PTT 长按 |
+|----|------------|----------|----------|
+| idle | 开录;**仅 Power 短按后再开用新 sid**,否则旧 sid | 提醒长按;idle | press 开录 / release 结束 |
+| recording | 停录立即重开,旧 sid | (无) | 同单击 |
+| thinking | 打断后重开,旧 sid | 停对话→idle | 停对话后开录 |
+| tts | 停播后重开,旧 sid | 停播→idle | 停播后开录 |
+
+**PTT 异常**:录音时长不足 → **notify TTS** + idle(`PTT_RECORD_TOO_SHORT_TTS`)。
+
+## 按键 — Power 键
+
+| UX | 短按 |
+|----|------|
+| idle | cancel(如需)→ Close WS → idle/休眠 → **清 sid** |
+| recording | cancel 本次 ASR 上传 → Close → 清 sid |
+| thinking / tts | cancel 对话/播报 → Close → 清 sid |
+
+**长按 Power 3s**:关机提示音 + 断电(`vc_buttons`)。
+
+## 代码锚点
+
+- `KeyRouter.dispatch_press` / `handle_ptt_release` / `handle_power_short`
+- `_vc_restart_record_same_dialog`、`_ptt_stop_dialog_to_idle`、`_ptt_abort_short_record`
+- `Close()` 清 `_cur_dialog_id`(Power 后下次 AI 为新对话)
diff --git a/.cursor/rules/003-bugfix.mdc b/.cursor/rules/003-bugfix.mdc
new file mode 100644 (file)
index 0000000..01fb226
--- /dev/null
@@ -0,0 +1,9 @@
+---\r
+alwaysApply: true\r
+---\r
+# 修复 Bug 强制规则\r
+1. 先定位根因,再给修复方案\r
+2. 最小改动,不扩散影响范围\r
+3. 必须说明:原因 → 影响 → 修复点\r
+4. 修复后要验证边界 case\r
+5. 不改无关代码、不引入新问题
\ No newline at end of file
diff --git a/.cursor/rules/project.mdc b/.cursor/rules/project.mdc
new file mode 100644 (file)
index 0000000..2499777
--- /dev/null
@@ -0,0 +1,11 @@
+---\r
+description: EG810 MicroPython 项目约定\r
+alwaysApply: true\r
+---\r
+所有生成代码必须:\r
+- 你是高级嵌入式工程师,代码必须遵循最佳实践和最佳设计原则\r
+- 本工程是micropython工程,\r
+- 2.sdk/Helios_SDK是编译路径,2.sdk/project/7PIN_1_5是项目路径 ,代码优先在项目路径里找,没有的通用代码才在编译路径里。\r
+- 编译指令是 new_clean.bat k8 1 \r
+- 分析日志解决问题时,要考虑根因,不要只是靠简单判断条件进行兜底修复。\r
+- 每次修改都要考虑副作用,不能拆东墙补西墙
\ No newline at end of file
diff --git a/1.doc/K8_diaozhui/design/AI吊坠VC架构重构与迭代计划.md b/1.doc/K8_diaozhui/design/AI吊坠VC架构重构与迭代计划.md
new file mode 100644 (file)
index 0000000..a14ecab
--- /dev/null
@@ -0,0 +1,268 @@
+# AI吊坠 VC 架构重构与迭代计划\r
+\r
+> **关联文档**:[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)、[详设-对照与决策](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)、[VC_SESSION_ARCH](./VC_SESSION_ARCH.md)  \r
+> **实现真源**:`2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/`  \r
+> **状态**:I0~I3 已落地;**用户开录统一入口**(start.mp3 + 长/PTT 分流)已落地(2026-05-22)\r
+\r
+---\r
+\r
+## 0. 问题陈述\r
+\r
+固件处于 **FSM 半迁移** 阶段:协议轮次(`fsm.turn`)、媒体硬件标志(`VC_Play_flag`)、打断补丁标志(`is_ruppt_tts`、`_ptt_idle_latch`…)、模式切换子状态(`_vc_ms_phase`)**四套状态并行可写**,且 LED 存在「业务直推 + FSM 纠偏 + AI_led 锁」三条路径,导致越改越乱。\r
+\r
+---\r
+\r
+## 1. 设计原则(准绳)\r
+\r
+| # | 原则 |\r
+|---|------|\r
+| P1 | **唯一编排线程**:除 EXTI 仅投递事件外,副作用只在 `publish_AI_btnInfo` 队列执行 |\r
+| P2 | **FSM 只管协议轮次**:`session` / `turn` / `await_played` / `tts_rx_phase` |\r
+| P3 | **硬件态归 Media**:`VC_record_state_flag` / `VC_Play_flag` 由采播层维护,编排层只读或通过 action 改 |\r
+| P4 | **LED 只读派生**:业务设 `_led_override` 或调 `vc_led_sync`;`set_led_state` 由 Presenter 聚合后单次下发 |\r
+| P5 | **打断用 InterruptContext + turn_gen**:避免 `is_ruppt_tts` 等布尔堆叠 |\r
+| P6 | **模式切换用 OverlayState**:按键屏蔽单入口,不分散 `_vc_ms_keys_blocked` |\r
+| P7 | **用户开录单入口、模式分流**:长对话与 PTT 提示音/上行时序不同,禁止一套 defer 逻辑硬套两种模式 |\r
+\r
+---\r
+\r
+## 2. 目标架构\r
+\r
+```\r
+GPIO/WS/Timer ──► publish_AI_btnInfo ──► AI_VC.handle / _fsm_apply_actions\r
+                                              │\r
+                    ┌─────────────────────────┼─────────────────────────┐\r
+                    ▼                         ▼                         ▼\r
+             VCStateMachine            InterruptContext            VCOverlayState\r
+             (vc_fsm)                  (vc_interrupt)              (vc_overlay)\r
+                    │                         │\r
+                    └────────────┬────────────┘\r
+                                 ▼\r
+              VCModeStrategy (vc_mode_strategy) ──► 按键 press/release/long\r
+                                 │\r
+                                 ▼\r
+                          LedPresenter (vc_present)\r
+                                 │\r
+                                 ▼\r
+                            set_led_state → AI_led\r
+```\r
+\r
+### 2.1 新增 / 重构模块\r
+\r
+| 文件 | 职责 |\r
+|------|------|\r
+| `vc_interrupt.py` | `InterruptContext`:打断轮次、同步 legacy 标志 |\r
+| `vc_present.py` | `build_present_state` / `present_to_led_msg`:LED 唯一推导 |\r
+| `vc_overlay.py` | `OVERLAY_*` 常量、`overlay_keys_blocked`:模式切换按键屏蔽 |\r
+| `vc_notify.py` | `NotifyScheduler`:`play_tts` takeover/defer、查找设备等 |\r
+| `vc_mode_strategy.py` | `LongDialogueStrategy` / `PTTStrategy`:AI 键分发 |\r
+| `vc_session_media.py` | **用户开录**:`_vc_user_record_entry*`、上行门控、`VC_Speech_*` |\r
+\r
+### 2.2 副作用单出口(I1)\r
+\r
+`_fsm_apply_actions` 支持的动作:\r
+\r
+| action | 说明 |\r
+|--------|------|\r
+| `uplink_start` / `uplink_stop` | 经 `_vc_uplink_start_recording` → `_vc_user_record_entry`(长对话)或 PTT 直开 |\r
+| `play_start` | 已有 |\r
+| `send_audio_played` | 已有 |\r
+| `led_sync` | 调用 `_vc_led_present_sync` |\r
+| `stop_downlink` | `_vc_stop_downlink_for_interrupt` |\r
+| `send_cancel` | WebSocket `cancel` |\r
+\r
+`vc_session_io` 禁止直接 `set_led_state`(thinking 等),改为 `vc_led_sync`。\r
+\r
+---\r
+\r
+## 3. 迭代计划与落地状态\r
+\r
+### I0 止血 ✅\r
+\r
+- [x] `_uplink_request_start` 删除 **coerce turn→Listening**(改由 `on_server_canceled` / 打断路径设 turn)\r
+- [x] 长对话 EXTI:`vc_long_dialogue_key_down` **再入队**到 btn 线程,EXTI 不直接改标志\r
+- [x] `overlay_keys_blocked` 统一模式切换按键屏蔽(兼容 `_vc_ms_phase`)\r
+- [x] 打断路径去掉提前 `_long_dialogue_publish_listening` / 重复 `_vc_arm_key_press_tone`\r
+- [x] `_uplink_request_start` 末尾不再重复播按键音(避免与 `user_record` 双播)\r
+\r
+> **演进说明(I0 之后)**:7PIN_1_5 开录提示音由 I0 阶段的 `RECORD_START_TONE=None` 调整为 **`start.mp3`**,见下文「用户开录」。\r
+\r
+### I1 副作用收口 ✅\r
+\r
+- [x] 扩展 `_fsm_apply_actions`:`led_sync` / `stop_downlink` / `send_cancel`\r
+- [x] `vc_session_io` 思考灯改为 `vc_led_sync`\r
+- [x] 订阅 `vc_led_sync` → `_vc_led_present_sync`\r
+\r
+### I2 Presenter + InterruptContext ✅\r
+\r
+- [x] `InterruptContext` 接管长对话打断 begin/end,同步 `is_ruppt_tts` 等 legacy 字段\r
+- [x] `_vc_publish_led` / `_vc_led_sync_from_fsm` 收敛为 Presenter 路径\r
+- [x] `get_vc_present_state` 供调试与 `AI_led` 查询\r
+\r
+### I3 NotifyChannel + ModeStrategy ✅\r
+\r
+- [x] `play_tts`:`NotifyScheduler.classify_play_tts` → `takeover`(断连重连)/ `defer`(对话中排队)\r
+- [x] `server.started`:`NotifyScheduler.plan_on_started` + `_vc_apply_notify_started_plan`\r
+- [x] `vc_mode_strategy.py`:`LongDialogueStrategy` / `PTTStrategy`,`AI_key_press/release/long_press` 经 `_vc_get_strategy()`\r
+- [x] `_notify_queue`:`VC_Play_End` 后 `_vc_notify_queue_flush`\r
+\r
+### 用户开录统一入口 ✅(2026-05-22)\r
+\r
+长对话 **AI 键打断/续聊** 与单对话 **长按开麦** 共用 `_vc_user_record_entry(reason)`,按 `VC_mode` 分流;提示音统一为 **`RECORD_START_TONE_FILE`(start.mp3)**,与短按「滴」分离。\r
+\r
+#### 7PIN_1_5 配置(`products/configs/7PIN_1_5/default_config.py`)\r
+\r
+| 配置项 | 值 | 用途 |\r
+|--------|-----|------|\r
+| `KEY_PRESS_TONE_FILE` | `U:/static/key_prompt.mp3` | REQ-AI-01:短按操作确认「滴」(非开录) |\r
+| `KEY_PRESS_TONE_DELAY_MS` | `300` | 短按音相关延时 |\r
+| `RECORD_START_TONE_FILE` | `U:/static/start.mp3` | 用户开录 / 打断续聊提示音 |\r
+| `RECORD_START_TONE_GAP_MS` | `180` | 长对话:提示音结束后再开上行定时器基准 |\r
+\r
+#### 录音上行状态(既有机制)\r
+\r
+| `VC_record_state_flag` | 含义 |\r
+|------------------------|------|\r
+| `0` | 未录音 |\r
+| `1` | 硬件已启,PCM 进 `AI_record_tmp_queue`,**不上传** |\r
+| `2` | `VC_Speech_Start_now()` 后,允许 `AI_client_VC_send` 上行 |\r
+\r
+#### API 与内部标志(`vc_session_media.py` / `vc_session.py`)\r
+\r
+| 符号 | 说明 |\r
+|------|------|\r
+| `_vc_user_record_entry(reason)` | 统一入口;`VC_mode==1` → PTT,否则 → 长对话 |\r
+| `_vc_user_record_entry_long` | 长对话:defer 上行 + defer 蓝灯 |\r
+| `_vc_user_record_entry_ptt` | PTT:立即 `Start` + `Start_now` + 蓝快闪 |\r
+| `_vc_user_record_uplink_open` | 长对话:滴声结束后 `Start_now` + `listening` |\r
+| `_vc_defer_listening_led` | `1` 时 `VC_Speech_Start` 不抢先发 listening |\r
+| `_vc_skip_local_play_stop_record` | 播 start.mp3 期间 `_vc_local_play` 不 `VC_Speech_End` |\r
+| `_vc_uplink_start_recording` | `request_uplink_start` 最终调用 `user_record_entry` |\r
+\r
+#### 时序对照\r
+\r
+| 模式 | 步骤 | LED |\r
+|------|------|-----|\r
+| **长对话** 打断/续聊 | ① `VC_Speech_Start`(flag=1 仅缓冲)→ ② start.mp3 → ③ 定时器 `_vc_user_record_uplink_open` → `VC_Speech_Start_now` | ③ 与上行同时蓝**慢闪** |\r
+| **单对话** PTT 长按 | ① 立即 `VC_Speech_Start` + `VC_Speech_Start_now`(flag=2 上行)→ ② 并行 start.mp3 | ① 即蓝**快闪**;**松手** `end_speech` 停传 → 思考灯等 ASR |\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant U as 用户按键\r
+    participant E as _vc_user_record_entry\r
+    participant L as entry_long\r
+    participant P as entry_ptt\r
+    participant M as 媒体/WS\r
+\r
+    U->>E: uplink / ptt_arm\r
+    alt VC_mode 长对话\r
+        E->>L: defer LED + 仅缓冲\r
+        L->>M: VC_Speech_Start flag=1\r
+        L->>L: start.mp3\r
+        L->>M: uplink_open Start_now flag=2\r
+        L->>L: listening 慢闪\r
+    else VC_mode 单对话 PTT\r
+        E->>P: 立即上行\r
+        P->>M: Start + Start_now flag=2\r
+        P->>L: listening 快闪\r
+        P->>P: start.mp3 并行\r
+        U->>M: 松手 end_speech\r
+        M->>M: thinking 等 ASR\r
+    end\r
+```\r
+\r
+#### 调用链入口\r
+\r
+| 场景 | 入口 |\r
+|------|------|\r
+| 长对话 `request_uplink_start`(含 `canceled` 后续聊) | `_vc_uplink_start_recording` → `_vc_user_record_entry_long` |\r
+| PTT `ptt_begin` 已 started | `_ptt_arm_uplink` → `_vc_user_record_entry_ptt` |\r
+| PTT 等 `server.started` 后首轮 | `OP_PTT_ROUND` → `_vc_user_record_entry("ptt_begin/started")` |\r
+| PTT `asr.started` 且 `_ptt_defer_speech_until_asr` | `_vc_on_server_record_uplink_ready` → `_vc_user_record_entry_ptt` |\r
+\r
+#### 联调日志关键字\r
+\r
+| 模式 | 期望日志 |\r
+|------|----------|\r
+| 长对话 | `[VC][user_record] entry_long` → `[VC] record_start_tone` → `[VC][user_record] uplink_open` |\r
+| PTT | `[VC][user_record] entry_ptt rec=2` → `[PTT] release end_speech` |\r
+\r
+#### PTT 轮间 LED(TTS 播完应 idle)\r
+\r
+| 阶段 | LED |\r
+|------|-----|\r
+| 松手等 ASR/LLM | `_ptt_await_server=1` → thinking |\r
+| TTS 播放 | `stream_active` → playing |\r
+| TTS 本地播完 | `play_end` 先 `on_local_play_end`,再 `VC_Play_End` → **idle**(`VC_Play_End/ptt_round`),等下次长按 |\r
+\r
+根因修复:勿在 `VC_Play_End` 时仅凭 `FSM.turn=Playing` 再亮思考灯。\r
+\r
+> 全量场景表(按键/打断/一轮对话/通知/Close 等)见 **[模式执行手册](./AI吊坠VC模式执行手册-长对话与单对话.md)**。\r
+\r
+---\r
+\r
+### 7PIN_1_5 机型说明(无闹钟 / IoT 查找设备)\r
+\r
+| 能力 | 实现 |\r
+|------|------|\r
+| 闹钟/日程 | **不启用**(`AI_ALARM_ENABLED=False`,`main_init` 不加载 `ai_alarm`) |\r
+| IoT 查找设备 | `protocol.IOTWeb_find_deviceProtoCol.recv` → `play_tts([文案, 1, "find_device"])` |\r
+| 通道 | `is_close_VC=1` + `find_device` → **takeover**(立即断对话会话、重连 WS 播 TTS,播完 Close) |\r
+| 对话中收到查找 | 不打 defer 队列(与闹钟不同,查找须马上响) |\r
+\r
+联调日志:`[VC][notify]` 不应出现查找设备 defer;应走 `play_tts_cb` → `websocket_deconnect` → `AI_VC_start`。\r
+\r
+---\r
+\r
+## 4. 状态不变式(联调日志)\r
+\r
+开发模式可在 `_1S_timer_cb_` 打印违反项:\r
+\r
+| ID | 不变式 |\r
+|----|--------|\r
+| INV-1 | `turn==Playing` → `VC_Play_flag==1` 或打断丢弃中 |\r
+| INV-2 | `VC_record_state_flag!=0` → turn 为 Listening/Speech* 或 InterruptContext.active |\r
+| INV-3 | `_vc_pending_interrupt_uplink==1` → `is_ruppt_tts==1` |\r
+| INV-4 | `Close` 后 InterruptContext/overlay/PTT latch 已清零 |\r
+| INV-5 | 同一编排 tick 内 `set_led_state` 至多 1 次(Presenter) |\r
+| INV-6 | 长对话 `entry_long` 期间 `VC_record_state_flag==2` 前应已播 start.mp3(或配置 None 时立即 uplink_open) |\r
+| INV-7 | PTT 长按按住期间 `VC_record_state_flag==2`;松手后应 `end_speech` 且非误触发 `AI_VC_start` |\r
+\r
+---\r
+\r
+## 5. 联调检查清单(每迭代必跑)\r
+\r
+| # | 长对话 | 单对话(PTT) |\r
+|---|--------|----------------|\r
+| 1 | TTS/思考中短按打断:500ms 内无残余 TTS | 长按 `ptt_begin` 可打断下行 TTS |\r
+| 2 | `VC_init` 后队列深度 0 | 同左 |\r
+| 3 | 开录前会话已 `started` | 未 started 时走 `audio.create`,勿 SKIP |\r
+| 4 | 打断续聊:先听到 **start.mp3**,再蓝慢闪且对方收到 PCM | **按住即上行**;松手 thinking → TTS playing → **播完 idle** |\r
+| 5 | `canceled` 后 800ms 内开麦(`entry_long`→`uplink_open`) | 松手→思考灯→再长按新一轮 |\r
+| 6 | 日志含 `[KEY][FSM][LED][VC][user_record]` | 含 `[PTT] release end_speech` |\r
+| 7 | 短按仅 **key_prompt 滴**,非 start.mp3 | 短按停播 TTS 不误 `end_speech` |\r
+\r
+编译:`new_clean.bat k8 1`(项目路径 `7PIN_1_5`)。\r
+\r
+---\r
+\r
+## 6. 与交互规范 REQ 的映射\r
+\r
+| REQ | 架构落点 |\r
+|-----|----------|\r
+| REQ-AI-01 | 短按操作确认:`KEY_PRESS_TONE_FILE`(key_prompt.mp3),与开录 start.mp3 分离 |\r
+| REQ-AI-02~04 | `InterruptContext` + cancel 串行 + 长对话 `entry_long` 开麦 |\r
+| REQ-AI-05~06 | PTT `_ptt_idle_latch` → Presenter `force_idle`;松手 `end_speech` |\r
+| REQ-AI-07 | 开录提示音 `RECORD_START_TONE_FILE=start.mp3`;长对话 **defer 上行**,PTT **立即上行** |\r
+| REQ-NOTE-02 | Presenter 表驱动 listening/thinking/playing(慢闪/快闪) |\r
+| REQ-SUP-01 | AI_led 充电灯被 Presenter 阶段压制 |\r
+| REQ-PWR-05 | OverlayState 模式切换屏蔽按键 |\r
+| IoT 查找 | `vc_notify` takeover,无闹钟 defer |\r
+\r
+---\r
+\r
+## 7. 引用\r
+\r
+- **模式执行手册(操作×协议×音×录×灯×TTS 顺序 + 实现标注)**:[AI吊坠VC模式执行手册-长对话与单对话.md](./AI吊坠VC模式执行手册-长对话与单对话.md)\r
+- 评审入口:[详设-对照与决策 §3~§4](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)\r
+- 场景步骤:[详设-场景与协议流程](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)\r
diff --git a/1.doc/K8_diaozhui/design/AI吊坠VC模式执行手册-长对话与单对话.md b/1.doc/K8_diaozhui/design/AI吊坠VC模式执行手册-长对话与单对话.md
new file mode 100644 (file)
index 0000000..4e8296f
--- /dev/null
@@ -0,0 +1,293 @@
+# AI吊坠 VC 模式执行手册(长对话 / 单对话)\r
+\r
+> **真源代码**:`2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/`  \r
+> **关联**:[迭代计划](./AI吊坠VC架构重构与迭代计划.md)、[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)、[详设-场景与协议](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)  \r
+> **机型示例**:7PIN_1_5(`RECORD_START_TONE=start.mp3`,无闹钟)\r
+\r
+---\r
+\r
+## 0. 图例与全局约束\r
+\r
+### 0.1 实现状态标注\r
+\r
+| 标记 | 含义 |\r
+|------|------|\r
+| ✅ | 已在 7PIN_1_5 项目路径落地,I0~I3 + 用户开录/PTT 轮间灯已联调修过 |\r
+| ⚠️ | 有实现但属兼容/遗留分支,或依赖配置关闭 |\r
+| 🔲 | 规范有述,本机型/本迭代未启用 |\r
+\r
+### 0.2 模式与协议差异(`vc_fsm.VCMode`)\r
+\r
+| 项 | 长对话 `VC_mode=0` | 单对话 PTT `VC_mode=1` |\r
+|----|-------------------|------------------------|\r
+| VAD | 服务端 VAD 开(`disable_vad=false`) | VAD 关(`disable_vad=true`) |\r
+| 上行包裹 | 直接 PCM 帧(`started` 后) | 必须 **`audio.create` → 帧 → `audio.completed`** |\r
+| 开麦触发 | 短按续聊 / 打断后开麦 / TTS 播完自动开麦(dialog) | **仅长按** `ptt_begin` |\r
+| 停麦 | `vad.silence` → `uplink_stop`;或本地 `VC_Speech_End` | **松手** → `AI_client_VC_end_speech()` |\r
+| 策略类 | `LongDialogueStrategy` | `PTTStrategy` |\r
+\r
+### 0.3 编排线程(所有副作用顺序的前提)\r
+\r
+| 步骤 | 说明 | 代码 |\r
+|------|------|------|\r
+| 1 | GPIO/EXTI 只投递 `EventMesh` / `publish_AI_btnInfo` | `vc_buttons.py`、`vc_long_dialogue_key_down` 再入队 ✅ |\r
+| 2 | **btn 队列**内执行:`AI_key_*`、`ptt_begin`、`request_uplink_*`、`VC_Play_End`、FSM actions | `vc_session.py` `_vc_enqueue_btn` ✅ |\r
+| 3 | WS 收包可在 IO 线程解析,**改 FSM/灯/上行** 须经 `on_server_event` → actions 或再入队 btn | `vc_session_io.py` ✅ |\r
+\r
+### 0.4 录音上行三态(共用)\r
+\r
+| `VC_record_state_flag` | 硬件 | WS 上行 | 典型设置点 |\r
+|------------------------|------|---------|------------|\r
+| `0` | 停 | 无 | `VC_Speech_End` |\r
+| `1` | 开,PCM 进 `AI_record_tmp_queue` | **禁止** | `VC_Speech_Start` |\r
+| `2` | 开 | **允许** `AI_client_VC_send` | `VC_Speech_Start_now` |\r
+\r
+### 0.5 提示音配置(7PIN_1_5)\r
+\r
+| 配置 | 文件 | 触发场景 | 状态 |\r
+|------|------|----------|------|\r
+| `KEY_PRESS_TONE_FILE` = key_prompt.mp3 | 短按操作确认「滴」 | 长对话短按(非开录主路径) | ✅ |\r
+| `RECORD_START_TONE_FILE` = start.mp3 | 用户开录 | 长对话 `entry_long`;PTT `entry_ptt` 并行播 | ✅ |\r
+| `PTT_RECORD_TOO_SHORT_TTS` | notify 合成 | PTT 长按录音 < `PTT_MIN_RECORD_MS` | ✅ |\r
+\r
+### 0.5.1 按键与 sid(见 `录音上传与播放-演示文稿.md`)\r
+\r
+| 键 | 要点 | 代码 |\r
+|----|------|------|\r
+| **AI 单击 idle** | 开录;**仅 Power 短按 Close 后**下次为新 sid,否则续旧 sid | `KeyRouter._long_ai_press` |\r
+| **AI 单击 recording** | 停录立即重开,旧 sid | `_vc_restart_record_same_dialog` |\r
+| **PTT 单击 idle** | `key_prompt` + idle | `KeyRouter.handle_ptt_release` |\r
+| **PTT 录音过短** | notify TTS + idle | `KeyRouter.ptt_release_after_long_press` |\r
+| **Power 单击** | cancel → Close → 清 `_cur_dialog_id` | `KeyRouter.handle_power_short` |\r
+| **Power 长按 3s** | 关机音 + 断电 | `vc_buttons.pwk_key_Long_Press` |\r
+\r
+实现收敛:`vc_ux` + `vc_key_router` + `vc_media_arb`(见 `VC实现审查与简化架构.md`)。\r
+\r
+### 0.6 LED 推导优先级(`vc_present.present_to_led_msg`)\r
+\r
+从高到低(先命中先生效):\r
+\r
+1. `_led_override`(`_vc_publish_led`  imperative)✅  \r
+2. `force_idle`(`_ptt_should_force_idle_led` / `_vc_led_should_standby_idle`)✅  \r
+3. `hold_listening`(录音 / PTT 打断锁 / `_vc_defer_listening_led` 等)✅  \r
+4. PTT 且 `_ptt_await_server` → **thinking** ✅  \r
+5. `stream_active`(`VC_Play_flag` / TTS 播中)→ **playing** ✅  \r
+6. FSM `turn` → thinking / listening / idle;PTT 且已停播的 Playing → **idle** ✅  \r
+\r
+灯物理节奏:`VC_mode=0` 蓝**慢闪**;`VC_mode=1` 蓝**快闪**(`mode=blue_blink` / period 250ms)✅  \r
+\r
+---\r
+\r
+## 1. 服务端事件 → FSM → 编排动作(共用)\r
+\r
+WS 事件统一入口:`vc_session_io` → `fsm.on_server_event` → `_fsm_apply_actions` ✅  \r
+\r
+| 服务端 event | FSM.turn 变化 | 典型 actions / 副作用 | 长对话 | PTT |\r
+|--------------|---------------|----------------------|--------|-----|\r
+| `started` | → `Listening` | `NotifyScheduler.plan_on_started`(媒体/开麦计划) | ✅ | ✅ |\r
+| `asr.started` | `speech_uplink_open=1` | PTT:可触发 `_vc_user_record_entry_ptt`(defer 场景) | ✅ | ✅ |\r
+| `asr.*` / `asr.completed` | 结果写 `AI_ASR_str` | 配置/cmd 可能 `Close`+重连 | ✅ | ✅ |\r
+| `vad.silence` | → `SpeechEnded` | **`uplink_stop`**(录音中) | ✅ 主路径 | 🔲 VAD 关 |\r
+| `llm.created` | → `Thinking` | `_ptt_await_server=0`;`vc_led_sync` | ✅ | ✅ |\r
+| `llm.completed` | 可能补 Thinking | 计时兜底 | ✅ | ✅ |\r
+| `tts.started` | → `TtsReceiving` | **`uplink_stop`** + **`play_start`**;`_ptt_await_server=0` | ✅ | ✅ |\r
+| `tts.completed` | 保持 `Playing`,`await_played=1` | 本地播完由 `play_end` 收尾 | ✅ | ✅ |\r
+| `completed` | 无 TTS 时 → `Listening` | 长对话可 `uplink_start`;有 TTS 时不抢跑 | ✅ | 仅收尾 |\r
+| `canceled` | → `Listening` | 长对话:`uplink_start`(打断后续聊);PTT:`_vc_on_ptt_cancel_settled` | ✅ | ✅ |\r
+\r
+**TTS 本地播完顺序(2026-05-22 修)** ✅  \r
+\r
+```\r
+tts.completed → (缓冲播完) → _vc_fire_local_play_end\r
+  → ① fsm.on_local_play_end()(含 send_audio_played、长对话 dialog 可 uplink_start)\r
+  → ② _ptt_await_server=0\r
+  → ③ VC_Play_End()(PTT → idle / VC_Play_End/ptt_round)\r
+  → ④ fsm.reset_rx_phases()\r
+```\r
+\r
+---\r
+\r
+## 2. 长对话模式(`VC_mode=0`)\r
+\r
+### 2.1 按键与入口\r
+\r
+| # | 用户操作 | 前置条件 | 执行顺序(协议·音·录·灯·TTS) | 实现 |\r
+|---|----------|----------|------------------------------|------|\r
+| L1 | **短按** 唤醒 | 未连接或未 `started` | ① `AI_VC_start` ② WS `Start` ③ 等 `started` ④ `NotifyScheduler` 可能 `OP_LONG_UPLINK` / boot 提示 ⑤ 灯 **listening 慢闪** | ✅ `_ai_key_press_long_dialogue_wake` |\r
+| L2 | **短按** 续聊 | 已 `started`,非 TTS/思考忙 | ① `request_uplink_start`→`_vc_user_record_entry_long` ② `VC_Speech_Start`(flag=1) ③ **start.mp3** ④ 定时 `_vc_user_record_uplink_open`→`Start_now`(flag=2) ⑤ 灯 **listening** ⑥ 上行 PCM | ✅ `key.press/continue` |\r
+| L3 | **短按** 打断 | TTS/思考/播中(`_long_dialogue_media_busy`) | ① `InterruptContext.begin` / cancel ② `_vc_stop_downlink` ③ WS **`cancel`** ④ 等 **`canceled`**(或超时 800ms)⑤ `request_uplink_start`→**entry_long**(同 L2) | ✅ `_long_dialogue_interrupt_on_short_press` |\r
+| L4 | **长按** | 同短按分发 | 与 L1/L2/L3 相同(`KeyRouter.dispatch_press` 入队) | ✅ |\r
+| L5 | **松手** | 长对话 | 无 PTT 式 end_speech;停麦靠 **vad.silence** 或本地逻辑 | ✅ |\r
+| L6 | EXTI 按下 | 长对话 | 仅 `publish_AI_btnInfo` 再入队,不直接改标志 | ✅ `vc_long_dialogue_key_down` |\r
+\r
+**短按打断时序(精简)**  \r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant U as 用户短按\r
+    participant B as btn队列\r
+    participant W as WS\r
+    participant M as 媒体\r
+\r
+    U->>B: interrupt_on_short_press\r
+    B->>W: cancel\r
+    W-->>B: canceled\r
+    B->>M: entry_long Start flag=1\r
+    M->>M: start.mp3\r
+    M->>M: uplink_open Start_now flag=2\r
+    M->>W: PCM 上行\r
+```\r
+\r
+### 2.2 服务端驱动一轮(VAD 开)\r
+\r
+| 阶段 | 协议/本地顺序 | LED | 实现 |\r
+|------|---------------|-----|------|\r
+| 聆听 | 用户说话 → PCM 上行 | listening 慢闪 | ✅ |\r
+| 停说 | `vad.silence` → `uplink_stop` → `VC_Speech_End`(若本地在录) | 仍 listening 或 **listening**(`VC_Speech_End/long_wait`) | ✅ |\r
+| 思考 | `llm.created` | **thinking**(`vc_led_sync`) | ✅ |\r
+| TTS 下发 | `tts.started` → `uplink_stop` + `VC_Play_Start` | **playing** | ✅ |\r
+| TTS 播完 | `play_end` → 长对话 **dialog** 可自动 `uplink_start`(续聊) | 回 **listening** 或保持策略 | ✅ `on_local_play_end` |\r
+| 无 TTS | `completed` 且非 await_played | 可 `uplink_start` | ✅ |\r
+\r
+### 2.3 长对话 · 提示音 / 通知 / 关闭\r
+\r
+| # | 场景 | 顺序 | 实现 |\r
+|---|------|------|------|\r
+| L7 | 模式切换 / 开机提示 TTS | `play_tts` **takeover** → 断 WS → 播完 `Close` | ✅ `vc_notify` |\r
+| L8 | 对话中 IoT 查询类 TTS | **defer** 队列,`VC_Play_End` 后 flush | ✅ |\r
+| L9 | **查找设备** | **takeover**(立即),非 defer | ✅ |\r
+| L10 | 空闲超时 `VC_IDLE_AUTO_CLOSE_SEC` | `Close` → 灯 idle | ✅ |\r
+| L11 | `Close()` | 停录停播 → `websocket_deconnect` → `VC_init` → 灯 idle | ✅ |\r
+\r
+---\r
+\r
+## 3. 单对话 PTT 模式(`VC_mode=1`)\r
+\r
+### 3.1 按键与入口\r
+\r
+| # | 用户操作 | 前置条件 | 执行顺序(协议·音·录·灯·TTS) | 实现 |\r
+|---|----------|----------|------------------------------|------|\r
+| P1 | **长按** 首轮 | 未连接 | ① `_ptt_force_record_on_started=1` ② `AI_VC_start` ③ `started` ④ `OP_PTT_ROUND`:`audio.create`+`entry_ptt` | ✅ `ptt_begin` |\r
+| P2 | **长按** 已 started | 无 TTS/思考占用 | ① `ptt_begin`→`_ptt_arm_uplink` ② **`audio.create`** ③ `entry_ptt`:`Start`+**`Start_now`**(flag=2) ④ 并行 **start.mp3** ⑤ 灯 **listening 快闪** | ✅ |\r
+| P3 | **长按** 打断 TTS/思考 | `_vc_ptt_need_interrupt_wait` | ① `_vc_begin_ptt_cancel_wait` **`cancel`** ② 灯 listening(`ptt_cancel_wait`)③ 收 **`canceled`**→`_ptt_arm_uplink`(同 P2) | ✅ |\r
+| P4 | **按住** 说话 | 录音中 flag=2 | 持续 PCM 上行(须已 `audio.create`) | ✅ |\r
+| P5 | **松手** | `AI_key_long_press_flag==1` 且正在录 | ① 可选延时 `PTT_RELEASE_END_SPEECH_DELAY_MS` ② **`audio.completed`**(`AI_client_VC_end_speech`)③ `VC_Speech_End` ④ `_ptt_await_server=1` ⑤ 灯 **thinking** | ✅ `_ai_key_release_handle_ptt` |\r
+| P6 | **短按**(未长按) | TTS 播中 | ① `cancel` ② 停播 ③ `_ptt_idle_latch=1` ④ 灯 **idle**(不再被 Play_End 拉回 thinking) | ✅ `_ptt_stop_tts_on_short_press` |\r
+| P7 | **短按** | 空闲 | 按下由策略吞掉;松手无 end_speech | ✅ `PTTStrategy.on_key_press` |\r
+| P8 | **松手** 无录音 | 误长按未开录 | `cancel` 清理 pending `audio.create` | ✅ |\r
+| P9 | **松手** 录音过短 | `< PTT_MIN_RECORD_MS` | notify TTS + idle | ✅ `ptt_release_after_long_press` |\r
+| P10 | **Power 短按** | 任意 UX | cancel → Close → 清 sid | ✅ `KeyRouter.handle_power_short` |\r
+\r
+**PTT 一轮完整时序**  \r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant U as 用户\r
+    participant B as btn\r
+    participant W as WS\r
+    participant M as 媒体/灯\r
+\r
+    U->>B: 长按 ptt_begin\r
+    B->>W: audio.create\r
+    B->>M: entry_ptt Start+Start_now\r
+    M->>M: start.mp3 并行\r
+    M->>M: listening 快闪\r
+    U->>B: 松手\r
+    B->>W: audio.completed\r
+    B->>M: VC_Speech_End\r
+    M->>M: thinking (_ptt_await_server)\r
+    W-->>M: llm.created / tts.started\r
+    M->>M: playing\r
+    W-->>M: tts.completed\r
+    M->>M: play_end → on_local_play_end → VC_Play_End\r
+    M->>M: idle 等下次长按\r
+```\r
+\r
+### 3.2 服务端驱动一轮(VAD 关)\r
+\r
+| 阶段 | 协议顺序 | LED | 实现 |\r
+|------|----------|-----|------|\r
+| 松手后 | `asr.*` → `llm.created` | thinking(await 或 FSM) | ✅ |\r
+| TTS | `tts.started` → 下行 PCM → `VC_Play_Start` | **playing** | ✅ |\r
+| 播完 | `tts.completed` → 本地播完 → **idle** | **idle**(非 thinking) | ✅ 2026-05-22 修 |\r
+| 轮间 | 会话保持 `started`,**不**自动开麦 | idle,等 **P2** | ✅ |\r
+\r
+### 3.3 PTT · 提示音 / 关闭\r
+\r
+| # | 场景 | 顺序 | 实现 |\r
+|---|------|------|------|\r
+| P9 | 模式切换提示 | takeover TTS,播完 Close | ✅ |\r
+| P10 | 空闲深睡 `VC_IDLE_DEEP_SLEEP_SEC` | `Close`(`VC_IDLE_AUTO_CLOSE_SEC_PTT=0` 不累加旧逻辑) | ✅ |\r
+| P11 | `Close` | 同共用 L11 | ✅ |\r
+\r
+---\r
+\r
+## 4. 用户开录统一 API(跨模式)\r
+\r
+| API | 长对话 | PTT | 文件 |\r
+|-----|--------|-----|------|\r
+| `_vc_user_record_entry` | → `_vc_user_record_entry_long` | → `_vc_user_record_entry_ptt` | `vc_session_media.py` ✅ |\r
+| 上行时机 | start.mp3 后 `uplink_open` | 立即 `Start_now` | ✅ |\r
+| 灯 | defer → 与 uplink 同刻 listening | 立即 listening 快闪 | ✅ |\r
+\r
+**调用链汇总**  \r
+\r
+| 场景 | reason / 入口 |\r
+|------|----------------|\r
+| 长对话续聊 | `request_uplink_start` → `key.press/continue` |\r
+| 长对话 canceled 后 | `uplink_start` / `canceled` |\r
+| PTT arm | `_ptt_arm_uplink` → `ptt_begin/arm` |\r
+| PTT 等 started | `OP_PTT_ROUND` → `ptt_begin/started` |\r
+| PTT asr.started defer | `asr.started/defer` |\r
+\r
+---\r
+\r
+## 5. 共用能力(与模式正交)\r
+\r
+| # | 能力 | 行为摘要 | 实现 |\r
+|---|------|----------|------|\r
+| S1 | **NotifyScheduler** | `play_tts`:takeover vs defer;`server.started` plan | `vc_notify.py` ✅ |\r
+| S2 | **InterruptContext** | 长对话短按打断;同步 `is_ruppt_tts` | `vc_interrupt.py` ✅ |\r
+| S3 | **Overlay 模式切换** | `_vc_ms_phase` / `overlay_keys_blocked` 屏蔽 AI 键 | `vc_overlay.py` ✅ |\r
+| S4 | **FSM actions** | uplink_start/stop、play_start、send_cancel、led_sync、stop_downlink | `vc_session.py` `_fsm_apply_actions` ✅ |\r
+| S5 | **Presenter 灯** | 唯一 `set_led_state` 聚合 | `vc_present.py` ✅ |\r
+| S6 | **闹钟** | 7PIN_1_5 **不加载** `ai_alarm` | 🔲 `AI_ALARM_ENABLED=False` |\r
+| S7 | **蓝牙键 BT_AI_key_press** | 部分走旧 compat `AI_VC_start`+arm tone | ⚠️ 兼容路径 |\r
+\r
+---\r
+\r
+## 6. 代码索引(按职责)\r
+\r
+| 模块 | 路径 | 职责 |\r
+|------|------|------|\r
+| 编排主类 | `vc_session.py` (`AI_VC`) | 按键、PTT、长对话、Close、灯 latch |\r
+| 媒体/上行 | `vc_session_media.py` | `VC_Speech_*`、`VC_Play_*`、`user_record_*` |\r
+| WS/协议 | `vc_session_io.py` | 事件解析、`llm/tts` 副作用 |\r
+| FSM | `vc_fsm.py` | turn/session、event→action |\r
+| 按键策略 | `vc_mode_strategy.py` | Long / PTT 分发 |\r
+| LED | `vc_present.py` | 灯态推导 |\r
+| 通知 | `vc_notify.py` | started plan / play_tts 分流 |\r
+| 打断 | `vc_interrupt.py` | InterruptContext |\r
+| 配置 | `products/configs/7PIN_1_5/default_config.py` | _tone / idle 秒数 |\r
+\r
+---\r
+\r
+## 7. 联调日志速查\r
+\r
+| 模式 | 关键日志 |\r
+|------|----------|\r
+| 长对话开录 | `[VC][user_record] entry_long` → `record_start_tone` → `uplink_open` |\r
+| 长对话打断 | `[KEY] long_dialogue interrupt -> cancel` → `canceled` → `entry_long` |\r
+| PTT 开录 | `[VC][user_record] entry_ptt rec=2` |\r
+| PTT 松手 | `[PTT] release end_speech` |\r
+| PTT 播完 | `[TTS] play_end_fire` → `VC_Play_End/ptt_round` 或 `present_sync idle` |\r
+| 异常 | `VC_Play_End led=... thinking`(播完仍思考 → 查 PTT idle 修复是否合入) |\r
+\r
+---\r
+\r
+## 8. 修订记录\r
+\r
+| 日期 | 内容 |\r
+|------|------|\r
+| 2026-05-22 | 首版:长/PTT 分表、协议顺序、提示音/灯/TTS 顺序、实现标注 |\r
+| 2026-05-22 | 补充 PTT TTS 播完 idle、`play_end` 与 FSM 顺序 |\r
index e547318..983b513 100644 (file)
 | **[交互规范-新旧变更说明.md](./AI吊坠(双按键+三色指示灯)交互规范-新旧变更说明.md)** | 相对旧版 `交互规范.md` 的**实质行为变更**(不含 REQ-ID 等结构差异) |
 | **[详设-对照与决策.md](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)** | 矩阵 §3、**已实现 IMP §4**、开放 GAP §5、状态报告 §8(**评审入口**) |
 | **[详设-场景与协议流程.md](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)** | 按键/协议步骤、时序图(联调深读) |
+| **[AI吊坠VC架构重构与迭代计划.md](./AI吊坠VC架构重构与迭代计划.md)** | I0~I2 架构原则、模块划分、联调清单(I3 待做) |
 
 上表 `REQ-*` 编号与详设主文档第 3、4 章一一对应。
 
index b7e854e..840c65d 100644 (file)
-# AI吊坠(双按键+三色指示灯)详设 — 对照与决策\r
-\r
-> **需求文档**:[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)(产品行为准绳)  \r
-> **场景详设**:[详设-场景与协议流程](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)(按键/协议步骤展开)  \r
-> **实现真源**:`2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py`(`AI_led`、`AI_hw_status`、`AI_dev`)\r
-\r
----\r
-\r
-## 0. 如何使用本文档\r
-\r
-### 0.1 三角对照\r
-\r
-\r
-| 角      | 文档                             | 回答的问题    |\r
-| ------ | ------------------------------ | -------- |\r
-| **需求** | `交互规范_new.md` + `REQ-`*        | 用户要什么?   |\r
-| **设计** | 本文第 2 章 `DES-`*、场景 `DES-SCN-`* | 固件应怎么做?  |\r
-| **实现** | `ai_VC.py` 等                   | 代码现在怎么做? |\r
-\r
-\r
-评审流程:**第 3 章矩阵** 标 `状态` → 已关闭差异见 **第 4 章 IMP(实现项)** → 仍开放项见 **第 5 章 GAP**。\r
-\r
-### 0.2 准绳优先级\r
-\r
-1. 已关闭的 **IMP 决策**(第 4 章,最高)\r
-2. 需求文档 `交互规范_new.md`(已与 IMP 同步)\r
-3. 本文设计规格 `DES-*`(工程应然)\r
-4. 当前代码(开放 GAP 项除外,以 IMP 为准)\r
-\r
-### 0.3 要点摘要(评审速览)\r
-\r
-- **长对话**:短按打断 TTS / **思考中** → `RequestToSpeak` → `VC_init` → `cancel` → `canceled` → `uplink_start`(**IMP-001**,见 `DES-SCN-010.2`~`010.4`)。  \r
-- **单对话**:长按开录 / 松手 `audio.completed`;TTS 中短按仅停播(**IMP-003**,见 `DES-SCN-020.6`)。  \r
-- **待机 / 停对话灯**:正常待机与 Power 短按结束对话均为 **灭灯**(**IMP-005/009**)。  \r
-- **播放灯**:TTS 阶段 **绿常亮**(**IMP-010**)。  \r
-- **电量阈值**:低电 `soc<30%`、空电 `soc<10%`(**IMP-007**)。  \r
-- **异常灯**:`alert=rb_flash_2`(**IMP-006**:ASR 空、thinking 超时)。\r
-\r
----\r
-\r
-## 1. 系统架构(一页)\r
-\r
-```mermaid\r
-flowchart TB\r
-  gpio[GPIO_btn_device]\r
-  dev[AI_dev_去抖串行]\r
-  vc[AI_VC_按键与VCStateMachine]\r
-  ws[WebSocket_V2_5]\r
-  aud[录音播放队列]\r
-  led[AI_led]\r
-  hw[AI_hw_status]\r
-  gpio -->|EventMesh| dev\r
-  dev -->|AI_key_press等| vc\r
-  vc --> ws\r
-  vc --> aud\r
-  vc -->|set_led_state| led\r
-  hw -->|set_led_state| led\r
-```\r
-\r
-\r
-\r
-### 1.1 模块职责\r
-\r
-\r
-| 模块               | 文件         | 职责                                         |\r
-| ---------------- | ---------- | ------------------------------------------ |\r
-| `AI_dev`         | `ai_VC.py` | 按键 ExtInt、去抖、`publish_AI_btnInfo` 串行执行     |\r
-| `AI_VC`          | `ai_VC.py` | WebSocket 会话、按键路由、音频上下行                    |\r
-| `VCStateMachine` | `ai_VC.py` | `session` / `turn` / `tts_rx_phase` 唯一轮次真源 |\r
-| `AI_hw_status`   | `ai_VC.py` | 10s 轮询 + `hw_usb_state` → 充电/电量/信号         |\r
-| `AI_led`         | `ai_VC.py` | `set_led_state` 聚合 → GPIO 灯效               |\r
-\r
-\r
-### 1.2 EventMesh 主题(常用)\r
-\r
-\r
-| 主题                                                      | 发布方                    | 订阅方            | 说明                  |\r
-| ------------------------------------------------------- | ---------------------- | -------------- | ------------------- |\r
-| `AI_key_press` / `AI_key_long_press` / `AI_key_release` | `AI_dev`               | `AI_VC`        | AI 键                |\r
-| `pwk_key_double_press`                                  | 电源键模块                  | `AI_VC`        | Power 双击切 `VC_mode` |\r
-| `set_led_state`                                         | `AI_VC`、`AI_hw_status` | `AI_led`       | LED 唯一入口            |\r
-| `hw_usb_state`                                          | 底层                     | `AI_hw_status` | USB 插拔即时刷新          |\r
-| `get_led_debug_state`                                   | 调试                     | `AI_led`       | 返回聚合灯态              |\r
-\r
-\r
-### 1.3 设计原则(文档化)\r
-\r
-- 轮次/会话判断只读 `fsm.*`,不引入并行整型状态。  \r
-- 打断 TTS:**先** `VC_init`(本地停播清队列)**再** `cancel` / `audio.create`(见 `DES-SCN-003`)。  \r
-- LED 只经 `set_led_state`;AI 阶段灯优先于充电/电量(见 `DES-L3-01`)。\r
-\r
----\r
-\r
-## 2. 设计规格(按层)\r
-\r
-### 2.1 L1 输入与按键(`DES-L1`)\r
-\r
-\r
-| ID        | 规格                                        | 实现锚点                                             |\r
-| --------- | ----------------------------------------- | ------------------------------------------------ |\r
-| DES-L1-01 | 长对话:AI **短按** 唤醒/打断/续聊                    | `AI_key_press` → `_ai_key_press_long_dialogue_`* |\r
-| DES-L1-02 | 单对话:AI **长按** 开录、**松开** `audio.completed` | `ptt_begin`、`_ai_key_release_handle_ptt`         |\r
-| DES-L1-03 | 单对话:TTS 中 **短按** 仅停播,不开录                  | `_ai_key_press_ptt_mode`                         |\r
-| DES-L1-04 | Power **双击** 切换 `VC_mode` 并 TTS 播报        | `pwk_key_double_press`                           |\r
-| DES-L1-05 | Power 交互中 **短按** `Close` 断会话进待机(灯灭)        | `_handle_pwk_short_release`→`Close`→`idle`       |\r
-\r
-\r
-### 2.2 L2 会话与协议(`DES-L2`)\r
-\r
-\r
-| ID        | 规格                                                      | 实现锚点                                       |\r
-| --------- | ------------------------------------------------------- | ------------------------------------------ |\r
-| DES-L2-01 | `VC_mode=0`:VAD 开,一般无 `audio.create`/`completed`        | `VCMode.Realtime`                          |\r
-| DES-L2-02 | `VC_mode=1`:VAD 关,必须 `audio.create`→帧→`audio.completed` | `VCMode.PushToTalk`                        |\r
-| DES-L2-03 | 会话层:`Disconnected`→`Connecting`→`Starting`→`Started`    | `VCSessionState`                           |\r
-| DES-L2-04 | 轮次层:`Idle`→`Listening`→…→`Playing`→续轮                   | `VCTurnState`、`VCStateMachine`             |\r
-| DES-L2-05 | 长对话打断 TTS:`cancel`→`canceled`→`uplink_start`            | `RequestToSpeak`、`on_server_canceled`      |\r
-| DES-L2-06 | 长对话播完门控:`audio.played` 后可自动开麦                           | `fsm.await_played`、`on_local_audio_played` |\r
-\r
-\r
-步骤级展开见 [场景详设](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)。\r
-\r
-### 2.3 L3 呈现:灯 / 音 / 硬件态(`DES-L3`)\r
-\r
-#### DES-L3-01 LED 优先级(`AI_led._apply_by_table`)\r
-\r
-\r
-| 优先级 | 输入         | 取值            | 灯效    | `period_ms`               |\r
-| --- | ---------- | ------------- | ----- | ------------------------- |\r
-| P1  | `ai_state` | `playing`     | 绿常亮   | —                         |\r
-| P2  | `ai_state` | `thinking`    | 蓝绿交替  | 500                       |\r
-| P3  | `ai_state` | `listening`   | 蓝慢闪   | 1000                      |\r
-| B1  | `charging` | `full`        | 绿常亮   | —                         |\r
-| B2  | `charging` | `charging`    | 红常亮   | —                         |\r
-| B3  | `battery`  | `low`/`empty` | 红慢闪   | 1000                      |\r
-| B4  | `signal`   | `weak`        | 蓝绿慢交替 | 1000                      |\r
-| 默认  | —          | `idle`        | **灭灯** | `LED_IDLE_MODE=off`(可选 `green_blink`) |\r
-| P0  | `alert`    | `rb_flash_2`  | 红蓝快闪 2 次 | `250`(约 2 次/秒)后回表 |\r
-\r
-\r
-`set_led_state` 键:`ai_state` / `charging` / `battery` / `signal` / `alert`。\r
-\r
-#### DES-L3-02 硬件态采样(`AI_hw_status`)\r
-\r
-\r
-| USB | SoC(非充电) | `charging`        | `battery`  |\r
-| --- | -------- | ----------------- | ---------- |\r
-| 否   | ≥30%     | `none`            | `normal`   |\r
-| 否   | 10%~30%  | `none`            | `low`      |\r
-| 否   | <10%     | `none`            | `empty`    |\r
-| 是   | —        | `charging`/`full` | 不按 SoC 刷低电 |\r
-\r
-\r
-弱网:`0 ≤ CSQ < SIGNAL_WEAK_CSQ`(默认 10)→ `signal=weak`。\r
-\r
-#### DES-L3-03 灯与 FSM 对应\r
-\r
-\r
-| `ai_state`  | 典型触发                |\r
-| ----------- | ------------------- |\r
-| `listening` | 开麦、续聊               |\r
-| `thinking`  | `llm.created`       |\r
-| `playing`   | `tts.started` / 本地播 |\r
-| `idle`      | 交互结束、异常恢复           |\r
-\r
-\r
----\r
-\r
-## 3. 需求 — 设计 — 实现对照矩阵\r
-\r
-\r
-| REQ-ID      | 需求摘要                      | 设计规格                    | 实现锚点                         | 状态    | IMP-ID  |\r
-| ----------- | ------------------------- | ----------------------- | ---------------------------- | ----- | ------- |\r
-| REQ-PWR-01  | 关机时长按 Power 3s 开机,绿灯常亮 3s | (电源模块)                  | 电源键 / `ui`                   | 需求未定义 | —       |\r
-| REQ-PWR-02  | 死机长按 8s 强制关机,无灯无音         | (电源模块)                  | 电源键                          | 需求未定义 | —       |\r
-| REQ-PWR-03  | 待机短按 Power 进休眠,无灯         | DES-L1-05               | `pm.autosleep` 等             | 部分一致  | —       |\r
-| REQ-PWR-04  | AI 交互中短按 Power 停对话进待机,灯灭 | DES-L1-05               | `Close`→`ai_state=idle`      | 一致    | IMP-009 |\r
-| REQ-PWR-05  | Power 双击切换单/长对话并播报        | DES-L1-04               | `pwk_key_double_press`       | 一致    | —       |\r
-| REQ-AI-01   | 长对话待机短按:蓝慢闪+滴,开麦          | DES-L1-01、DES-SCN-010.5 | `_ai_key_press_long_dialogue_wake`→`AI_VC_start` | 一致    | IMP-002 |\r
-| REQ-AI-02   | 长对话聆听中短按:重开麦              | DES-SCN-010.3           | `request_uplink_start`       | 一致    | —       |\r
-| REQ-AI-03   | 长对话 TTS 中短按:打断并聆听         | DES-SCN-010.2           | `_ai_key_press_long_dialogue_interrupt` | 一致    | —       |\r
-| REQ-AI-04   | 长对话思考中短按:打断并聆听            | DES-SCN-010.4           | `_ai_key_press_long_dialogue_interrupt` | 一致    | IMP-001 |\r
-| REQ-AI-05   | 单对话 TTS 中短按:仅停播           | DES-L1-03、DES-SCN-020.3 | `_ai_key_press_ptt_mode`     | 一致    | —       |\r
-| REQ-AI-06   | 单对话非 TTS 短按:无操作           | DES-L1-03               | `return True` 吞掉             | 一致    | IMP-003 |\r
-| REQ-AI-07   | 单对话长按:蓝快闪+开录音             | DES-L1-02、DES-SCN-020.2 | `ptt_begin`                  | 一致    | —       |\r
-| REQ-AI-08   | 单对话松手:思考灯+层音+completed    | DES-L1-02               | `AI_client_VC_end_speech`    | 一致    | —       |\r
-| REQ-BAT-01  | 正常待机灯灭                    | DES-L3-01 默认            | `_set_off`(`LED_IDLE_MODE=off`) | 一致    | IMP-005 |\r
-| REQ-BAT-02  | 低电 10%~30% 红慢闪+双滴         | DES-L3-02               | `battery=low`(SoC<30%)       | 一致    | IMP-007 |\r
-| REQ-BAT-03  | 电量<10% 红快闪 3 次后关机         | DES-L3-02               | `battery=empty`(SoC<10%)     | 一致    | IMP-007 |\r
-| REQ-BAT-04  | 充电中红常亮+滴                  | DES-L3-02               | `charging=charging`          | 一致    | IMP-008 |\r
-| REQ-BAT-05  | 充满红→绿常亮+长滴                | DES-L3-02               | `charging=full`              | 一致    | —       |\r
-| REQ-ERR-01  | ASR 失败:红蓝快闪 2 次→待机灭       | DES-L3 P0 `alert`         | `_vc_led_alert_rb_flash`(asr 空) | 一致    | IMP-006 |\r
-| REQ-ERR-02  | 网络异常:弱网慢交替+语音提示           | DES-L3-02 B4            | `AI_hw_status`/`websocket_error` | 部分一致  | GAP-011 |\r
-| REQ-ERR-03  | 思考超时:红蓝闪→重启/待机灭          | DES-L3 P0 `alert`         | `thinking_timeout`→`alert`+`AI_VC_start` | 一致    | IMP-006 |\r
-| REQ-NOTE-01 | 备注:会话空闲后进入休眠前灯态           | DES-L3 默认灭灯;可选绿闪      | `LED_IDLE_MODE`              | 一致    | IMP-005 |\r
-| REQ-NOTE-02 | 备注:聆听蓝闪、思考交替、播放绿常亮        | DES-L3-01 P2/P3/P1      | `set_led_state`              | 一致    | IMP-010 |\r
-| REQ-NOTE-03 | 休眠无灯光                     | (休眠策略)                  | `ui.py` / `pm`               | 部分一致  | —       |\r
-| REQ-SUP-01  | 充电中 AI 交互优先 AI 灯          | DES-L3-01               | `_apply_by_table`            | 一致    | IMP-008 |\r
-| REQ-SUP-02  | 按键后灯音延迟≤100ms             | DES-SCN-003             | `VC_init` 优先                 | 一致    | —       |\r
-\r
-\r
----\r
-\r
-## 4. 已实现决策项(IMP)\r
-\r
-> 原 **GAP-001~010** 均已决策并关闭;需求文档 `交互规范_new.md` 已按下列决策同步(2026-05-16)。\r
-\r
-\r
-| IMP-ID  | 原 GAP   | 决策摘要 | 关联 REQ | 落地说明 / 实现锚点 |\r
-| ------- | ------ | ---- | ------ | ------------- |\r
-| IMP-001 | GAP-001 | 采纳甲 | REQ-AI-04 | `turn==Thinking` 走 `_ai_key_press_long_dialogue_interrupt`→`RequestToSpeak`→`cancel`→开麦 |\r
-| IMP-002 | GAP-002 | 采纳甲 | REQ-AI-01 | `_ai_key_press_long_dialogue_wake` 直接 `AI_VC_start`,去掉 `Close`+`sleep` 兜底 |\r
-| IMP-003 | GAP-003 | 采纳甲 | REQ-AI-06 | 单对话非 TTS 短按 `return True` 吞掉;需求表已写明 |\r
-| IMP-005 | GAP-005 | 采纳甲 | REQ-BAT-01、NOTE-01 | `LED_IDLE_MODE=off`,待机灭灯 |\r
-| IMP-006 | GAP-006 | 采纳甲 + 改需求 | REQ-ERR-01、ERR-03 | `alert=rb_flash_2`;ASR 空、thinking 超时;ERR-03 不再要求「蓝常亮前置」 |\r
-| IMP-007 | GAP-007 | 采纳甲 | REQ-BAT-02、03 | `BATTERY_LOW_SOC_PERCENT=30`、`EMPTY=10` |\r
-| IMP-008 | GAP-008 | 采纳甲 | REQ-BAT-04、SUP-01 | AI 交互灯优先于充电指示 |\r
-| IMP-009 | GAP-009 | **改需求(乙)** | REQ-PWR-04 | 停对话后 **灭灯**(与 BAT-01 一致),非绿慢闪 |\r
-| IMP-010 | GAP-010 | **改需求(乙)** | REQ-NOTE-02 | 播放阶段 **绿常亮**(`playing`→`green_on`),非绿闪 |\r
-\r
-### 4.1 IMP-001:`RequestToSpeak` 必须先 `cancel` 再开麦\r
-\r
-长对话在 `turn==Thinking` 或 TTS 播放中,若直接 `uplink_start`,`_uplink_request_start` 会因 `turn` 非 `Listening/SpeechEnded` 而 **Deny START**;若先开麦再 `cancel`,本地/服务端旧轮下行可能与新一轮上行竞态。\r
-\r
-**方案甲时序(与 DES-SCN-010.2/010.4 一致)**:\r
-\r
-1. 短按 → `AI_client_VC_RequestToSpeak`:`VC_init` 停播清队列 + WebSocket 发 `cancel`。  \r
-2. 服务端 `server.canceled` → `on_server_canceled` 将 `turn` 置为 `Listening`。  \r
-3. `on_server_event("canceled")` 追加 `uplink_start` → 开麦、蓝慢闪。\r
-\r
-因此「cancel 在后」是指 **开麦动作在 canceled 事件之后**,而非不发 cancel。\r
-\r
----\r
-\r
-## 5. 开放差异(GAP)\r
-\r
-> 仅保留 **未关闭** 或需联调收敛项。新差异请用 GAP-012 起编号。\r
-\r
-\r
-| GAP-ID  | 关联 REQ   | 说明 | 建议 |\r
-| ------- | -------- | --- | --- |\r
-| GAP-011 | REQ-ERR-02 | 弱网:`AI_hw_status` 可驱动 `signal=weak`→蓝绿慢交替;`websocket_error` 以 TTS+重连为主,与「每 5 秒循环弱网语音」表述需联调确认 | 实机测 CSQ&lt;10 与断网两种场景,必要时统一 TTS 间隔文案 |\r
-| —       | REQ-PWR-03 | 待机短按进休眠 vs `Close` 路径边界 | 对照 `ui.py`/`pm` 用例 |\r
-| —       | REQ-NOTE-03 | 2 分钟无交互进休眠、休眠无灯 | 测 `idle_L2` + `sys_sleep_cb` |\r
-| —       | REQ-PWR-01/02 | 3s 开机 / 8s 强关 | 电源模块单独需求,不在 `ai_VC` |\r
-\r
----\r
-\r
-## 6. 联调索引\r
-\r
-### 6.1 检查清单(原 E8)\r
-\r
-\r
-| #   | 检查项                    | 长对话                                 | 单对话                    |\r
-| --- | ---------------------- | ----------------------------------- | ---------------------- |\r
-| 1   | TTS 中按键后 500ms 内无残余播报  | 短按+cancel                           | 长按 `ptt_begin`         |\r
-| 2   | `AI_audio_queue` 深度为 0 | `VC_init` 后 `[VC] init audio_queue` | 同左                     |\r
-| 3   | 开录前已 `started`         | 是                                   | 否则 `audio.create` SKIP |\r
-| 4   | 开录提示音不叠按键音             | 短按提示音已关                             | `ptt_begin` 内不叠短嘟      |\r
-| 5   | 打断后能快速第二轮              | `canceled`→开麦                       | 松手→播完→再长按              |\r
-| 6   | 串口日志                   | `[KEY]`、`[FSM]`、`[VC][proto]`       | 同左                     |\r
-\r
-\r
-### 6.2 日志关键字(原 E12)\r
-\r
-\r
-| 日志                                               | 含义                        |\r
-| ------------------------------------------------ | ------------------------- |\r
-| `[KEY] AI_key_press VC_mode=`                    | 按键模式与 FSM 快照              |\r
-| `[PTT] cancel before new uplink`                 | PTT 长按前已发 cancel          |\r
-| `[PTT] wait server.started`                      | 等 started 后再 audio.create |\r
-| `[VC][proto] audio.create send` / `SKIP`         | PTT 开轮是否成功                |\r
-| `[VC][uplink_ctrl] Request START` / `Deny START` | 长对话开麦门控                   |\r
-| `[FSM] server.canceled`                          | 打断成功,长对话可续麦               |\r
-| `[VC] init audio_queue`                          | 清播放队列成功                   |\r
-\r
-\r
-### 6.3 调试 API\r
-\r
-`EventMesh.publish("get_led_debug_state")` → `{ ai, charging, battery, signal, mode, period_ms }`\r
-\r
----\r
-\r
-## 7. 引用与场景索引\r
-\r
-\r
-| 文档       | 路径                                                                  |\r
-| -------- | ------------------------------------------------------------------- |\r
-| 需求规范     | [交互规范_new.md](./AI吊坠(双按键+三色指示灯)交互规范_new.md)                         |\r
-| 场景详设     | [详设-场景与协议流程.md](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)                     |\r
-| 源码       | `2.sdk/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py` |\r
-| 旧附录(已迁移) | [交互规范_附录-工程与代码审查.md](./AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md)           |\r
-\r
-\r
-### 7.1 场景详设索引(`DES-SCN-`*)\r
-\r
-\r
-| ID          | 内容                          |\r
-| ----------- | --------------------------- |\r
-| DES-SCN-000 | 场景速查与索引                     |\r
-| DES-SCN-001 | 模式总览                        |\r
-| DES-SCN-002 | 协议消息语义                      |\r
-| DES-SCN-003 | 打断 TTS 本地清缓存顺序              |\r
-| DES-SCN-010 | 长对话 AI 键(含 010.2~010.7 子场景) |\r
-| DES-SCN-011 | 长对话打断 TTS 时序图               |\r
-| DES-SCN-020 | 单对话 PTT(含 020.2~020.8)      |\r
-| DES-SCN-030 | 与 Power 键、模式切换边界            |\r
-| DES-SCN-050 | 服务端事件对照                     |\r
-\r
-\r
----\r
-\r
-## 8. 实现状态报告(代码审查)\r
-\r
-> **审查基准**:`2.sdk/project/7PIN_1_5/.../ai_VC.py` + `2.sdk/new_*.bat`  \r
-> **审查日期**:2026-05-16(**需求已与 IMP 决策同步**)  \r
-> **产品配置**:`product_k8.ini`,`PRODUCT_NAME=k8`\r
-\r
-### 8.1 总览\r
-\r
-\r
-| 维度 | 一致/已实现 | 部分一致 | 未定义/非本模块 |\r
-| --- | ---: | ---: | ---: |\r
-| 交互需求(第 3 章,不含 PWR-01/02) | **22** | **2**(PWR-03、NOTE-03) | 2 |\r
-| IMP 已关闭(原 GAP-001~010) | **10** | 0 | — |\r
-| 开放 GAP(第 5 章) | — | **1**(GAP-011) | 3 项联调备注 |\r
-| 版本与构建链 | **8** ✅ | 1 ⚠️ 遗留脚本 | — |\r
-\r
-\r
-**结论**:需求—设计—实现三角已对齐 **IMP-001~010**;`交互规范_new.md` 中 PWR-04、NOTE-02 播放行、ERR-02/03 已按决策改写。后续联调聚焦 **GAP-011(弱网)**、休眠(PWR-03/NOTE-03)、电源 3s/8s。\r
-\r
-### 8.2 版本与构建(摘要)\r
-\r
-| 项 | 状态 |\r
-| --- | --- |\r
-| `k8_V1.2_yyyy_MM_dd_HH_mm` + `version_util` frozen | ✅ |\r
-| `new_version.bat` 单次调用、`new_clean` release 路径、QP_LOG 默认关 | ✅ |\r
-| `ai_VC_coze.py` / `usr/test1.py` 旧版本串 | ⚠️ 非主路径,建议清理 |\r
-\r
-### 8.3 联调优先级(更新)\r
-\r
-| 优先级 | 项 |\r
-| ---: | --- |\r
-| P0 | 长对话打断、单对话 PTT(IMP-001~003) |\r
-| P1 | GAP-011 弱网 CSQ vs `websocket_error` |\r
-| P1 | OTA 登记 `k8_V1.2_*` |\r
-| P2 | PWR-03/NOTE-03 休眠;清理遗留版本脚本 |\r
-\r
+# 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 章 IMP(实现项)** → 仍开放项见 **第 5 章 GAP**。
+
+### 0.2 准绳优先级
+
+1. 已关闭的 **IMP 决策**(第 4 章,最高)
+2. 需求文档 `交互规范_new.md`(已与 IMP 同步)
+3. 本文设计规格 `DES-*`(工程应然)
+4. 当前代码(开放 GAP 项除外,以 IMP 为准)
+
+### 0.3 要点摘要(评审速览)
+
+- **长对话**:短按打断 TTS / **思考中** → `RequestToSpeak` → `VC_init` → `cancel` → `canceled` → `uplink_start`(**IMP-001**,见 `DES-SCN-010.2`~`010.4`)。  
+- **单对话**:长按开录 / 松手 `audio.completed`;TTS 中短按仅停播(**IMP-003**,见 `DES-SCN-020.6`)。  
+- **待机 / 停对话灯**:正常待机与 Power 短按结束对话均为 **灭灯**(**IMP-005/009**)。  
+- **播放灯**:TTS 阶段 **绿常亮**(**IMP-010**)。  
+- **电量阈值**:低电 `soc<30%`、空电 `soc<10%`(**IMP-007**)。  
+- **异常灯**:`alert=rb_flash_2`(**IMP-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` 断会话进待机(灯灭)        | `_handle_pwk_short_release`→`Close`→`idle`       |
+
+
+### 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      | 需求摘要                      | 设计规格                    | 实现锚点                         | 状态    | IMP-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`→`ai_state=idle`      | 一致    | IMP-009 |
+| 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` | 一致    | IMP-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` | 一致    | IMP-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` 吞掉             | 一致    | IMP-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`) | 一致    | IMP-005 |
+| REQ-BAT-02  | 低电 10%~30% 红慢闪+双滴         | DES-L3-02               | `battery=low`(SoC<30%)       | 一致    | IMP-007 |
+| REQ-BAT-03  | 电量<10% 红快闪 3 次后关机         | DES-L3-02               | `battery=empty`(SoC<10%)     | 一致    | IMP-007 |
+| REQ-BAT-04  | 充电中红常亮+滴                  | DES-L3-02               | `charging=charging`          | 一致    | IMP-008 |
+| REQ-BAT-05  | 充满红→绿常亮+长滴                | DES-L3-02               | `charging=full`              | 一致    | —       |
+| REQ-ERR-01  | ASR 失败:红蓝快闪 2 次→待机灭       | DES-L3 P0 `alert`         | `_vc_led_alert_rb_flash`(asr 空) | 一致    | IMP-006 |
+| REQ-ERR-02  | 网络异常:弱网慢交替+语音提示           | DES-L3-02 B4            | `AI_hw_status`/`websocket_error` | 部分一致  | GAP-011 |
+| REQ-ERR-03  | 思考超时:红蓝闪→重启/待机灭          | DES-L3 P0 `alert`         | `thinking_timeout`→`alert`+`AI_VC_start` | 一致    | IMP-006 |
+| REQ-NOTE-01 | 备注:会话空闲后进入休眠前灯态           | DES-L3 默认灭灯;可选绿闪      | `LED_IDLE_MODE`              | 一致    | IMP-005 |
+| REQ-NOTE-02 | 备注:聆听蓝闪、思考交替、播放绿常亮        | DES-L3-01 P2/P3/P1      | `set_led_state`              | 一致    | IMP-010 |
+| REQ-NOTE-03 | 休眠无灯光                     | (休眠策略)                  | `ui.py` / `pm`               | 部分一致  | —       |
+| REQ-SUP-01  | 充电中 AI 交互优先 AI 灯          | DES-L3-01               | `_apply_by_table`            | 一致    | IMP-008 |
+| REQ-SUP-02  | 按键后灯音延迟≤100ms             | DES-SCN-003             | `VC_init` 优先                 | 一致    | —       |
+
+
+---
+
+## 4. 已实现决策项(IMP)
+
+> 原 **GAP-001~010** 均已决策并关闭;需求文档 `交互规范_new.md` 已按下列决策同步(2026-05-16)。
+
+
+| IMP-ID  | 原 GAP   | 决策摘要 | 关联 REQ | 落地说明 / 实现锚点 |
+| ------- | ------ | ---- | ------ | ------------- |
+| IMP-001 | GAP-001 | 采纳甲 | REQ-AI-04 | `turn==Thinking` 走 `_ai_key_press_long_dialogue_interrupt`→`RequestToSpeak`→`cancel`→开麦 |
+| IMP-002 | GAP-002 | 采纳甲 | REQ-AI-01 | `_ai_key_press_long_dialogue_wake` 直接 `AI_VC_start`,去掉 `Close`+`sleep` 兜底 |
+| IMP-003 | GAP-003 | 采纳甲 | REQ-AI-06 | 单对话非 TTS 短按 `return True` 吞掉;需求表已写明 |
+| IMP-005 | GAP-005 | 采纳甲 | REQ-BAT-01、NOTE-01 | `LED_IDLE_MODE=off`,待机灭灯 |
+| IMP-006 | GAP-006 | 采纳甲 + 改需求 | REQ-ERR-01、ERR-03 | `alert=rb_flash_2`;ASR 空、thinking 超时;ERR-03 不再要求「蓝常亮前置」 |
+| IMP-007 | GAP-007 | 采纳甲 | REQ-BAT-02、03 | `BATTERY_LOW_SOC_PERCENT=30`、`EMPTY=10` |
+| IMP-008 | GAP-008 | 采纳甲 | REQ-BAT-04、SUP-01 | AI 交互灯优先于充电指示 |
+| IMP-009 | GAP-009 | **改需求(乙)** | REQ-PWR-04 | 停对话后 **灭灯**(与 BAT-01 一致),非绿慢闪 |
+| IMP-010 | GAP-010 | **改需求(乙)** | REQ-NOTE-02 | 播放阶段 **绿常亮**(`playing`→`green_on`),非绿闪 |
+
+### 4.1 IMP-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. 开放差异(GAP)
+
+> 仅保留 **未关闭** 或需联调收敛项。新差异请用 GAP-012 起编号。
+
+
+| GAP-ID  | 关联 REQ   | 说明 | 建议 |
+| ------- | -------- | --- | --- |
+| GAP-011 | REQ-ERR-02 | 弱网:`AI_hw_status` 可驱动 `signal=weak`→蓝绿慢交替;`websocket_error` 以 TTS+重连为主,与「每 5 秒循环弱网语音」表述需联调确认 | 实机测 CSQ&lt;10 与断网两种场景,必要时统一 TTS 间隔文案 |
+| —       | REQ-PWR-03 | 待机短按进休眠 vs `Close` 路径边界 | 对照 `ui.py`/`pm` 用例 |
+| —       | REQ-NOTE-03 | 2 分钟无交互进休眠、休眠无灯 | 测 `idle_L2` + `sys_sleep_cb` |
+| —       | REQ-PWR-01/02 | 3s 开机 / 8s 强关 | 电源模块单独需求,不在 `ai_VC` |
+
+---
+
+## 6. 联调索引
+
+### 6.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]`       | 同左                     |
+
+
+### 6.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`                          | 清播放队列成功                   |
+
+
+### 6.3 调试 API
+
+`EventMesh.publish("get_led_debug_state")` → `{ ai, charging, battery, signal, mode, period_ms }`
+
+---
+
+## 7. 引用与场景索引
+
+
+| 文档       | 路径                                                                  |
+| -------- | ------------------------------------------------------------------- |
+| 需求规范     | [交互规范_new.md](./AI吊坠(双按键+三色指示灯)交互规范_new.md)                         |
+| 场景详设     | [详设-场景与协议流程.md](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md)                     |
+| 源码       | `2.sdk/project/7PIN_1_5/.../core/`(`vc_session*.py`、`vc_interrupt`、`vc_present`) |
+| 架构重构   | [AI吊坠VC架构重构与迭代计划.md](./AI吊坠VC架构重构与迭代计划.md) |
+| 旧附录(已迁移) | [交互规范_附录-工程与代码审查.md](./AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md)           |
+
+
+### 7.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 | 服务端事件对照                     |
+
+
+---
+
+## 8. 实现状态报告(代码审查)
+
+> **审查基准**:`2.sdk/project/7PIN_1_5/.../ai_VC.py` + `2.sdk/new_*.bat`  
+> **审查日期**:2026-05-16(**需求已与 IMP 决策同步**)  
+> **产品配置**:`product_k8.ini`,`PRODUCT_NAME=k8`
+
+### 8.1 总览
+
+
+| 维度 | 一致/已实现 | 部分一致 | 未定义/非本模块 |
+| --- | ---: | ---: | ---: |
+| 交互需求(第 3 章,不含 PWR-01/02) | **22** | **2**(PWR-03、NOTE-03) | 2 |
+| IMP 已关闭(原 GAP-001~010) | **10** | 0 | — |
+| 开放 GAP(第 5 章) | — | **1**(GAP-011) | 3 项联调备注 |
+| 版本与构建链 | **8** ✅ | 1 ⚠️ 遗留脚本 | — |
+
+
+**结论**:需求—设计—实现三角已对齐 **IMP-001~010**;`交互规范_new.md` 中 PWR-04、NOTE-02 播放行、ERR-02/03 已按决策改写。后续联调聚焦 **GAP-011(弱网)**、休眠(PWR-03/NOTE-03)、电源 3s/8s。
+
+### 8.2 版本与构建(摘要)
+
+| 项 | 状态 |
+| --- | --- |
+| `k8_V1.2_yyyy_MM_dd_HH_mm` + `version_util` frozen | ✅ |
+| `new_version.bat` 单次调用、`new_clean` release 路径、QP_LOG 默认关 | ✅ |
+| `ai_VC_coze.py` / `usr/test1.py` 旧版本串 | ⚠️ 非主路径,建议清理 |
+
+### 8.3 联调优先级(更新)
+
+| 优先级 | 项 |
+| ---: | --- |
+| P0 | 长对话打断、单对话 PTT(IMP-001~003) |
+| P1 | GAP-011 弱网 CSQ vs `websocket_error` |
+| P1 | OTA 登记 `k8_V1.2_*` |
+| P2 | PWR-03/NOTE-03 休眠;清理遗留版本脚本 |
+
index 7f1f900..fc28bc2 100644 (file)
@@ -17,6 +17,14 @@ core/
 ├── vc_session_media.py      # 【媒体】ASR 上行/录音、TTS/播放队列、本地采播
 ├── vc_session_imports.py    # 子模块共享 import(仅 session/io/media)
 ├── vc_fsm.py                # 纯 FSM
+├── vc_interrupt.py          # InterruptContext(打断轮次)
+├── vc_present.py            # LED Presenter(推导 ai_state)
+├── vc_overlay.py            # 模式切换叠加态 / 按键屏蔽
+├── vc_notify.py             # NotifyScheduler / play_tts 通道
+├── vc_ux.py                 # UX 四态推导(idle/recording/thinking/tts)
+├── vc_key_router.py         # 按键表(AI / Power / PTT release)
+├── vc_media_arb.py          # 媒体三车道仲裁
+├── vc_mode_strategy.py      # 长对话 / PTT 按键策略(转发 KeyRouter)
 ├── vc_auth / vc_buttons / vc_led / …   # 外围,不 import vc_session
 ├── protocol.py              # IOT 心跳 WS(≠ VC WS)
 └── main_init.py             # import ai_VC
diff --git a/1.doc/K8_diaozhui/design/VC实现审查与简化架构.md b/1.doc/K8_diaozhui/design/VC实现审查与简化架构.md
new file mode 100644 (file)
index 0000000..512f43b
--- /dev/null
@@ -0,0 +1,54 @@
+# VC 实现审查与简化架构(7PIN_1_5)\r
+\r
+> 权威:`录音上传与播放-演示文稿.md`(含 **Power 键**、**sid 规则**、**PTT 录音过短 notify**)\r
+\r
+## 1. 设计稿要点(2025 更新)\r
+\r
+### 媒体三车道\r
+\r
+| 车道 | 抢占 |\r
+|------|------|\r
+| LLM PCM | notify/local defer |\r
+| Notify TTS | 关 llm + local |\r
+| Local MP3 | 关 llm + notify;录音中可并行 |\r
+\r
+### sid 规则\r
+\r
+- **AI  idle 开录**:默认 **旧 sid**(同 WS 会话续 `dialog_id`)。\r
+- **仅 Power 短按 Close 后**:清 `_cur_dialog_id`,下次 `AI_VC_start` 为 **新 sid**。\r
+\r
+### Power 短按\r
+\r
+各 UX 态均:**协议 cancel(按需)→ Close WS → idle/休眠 → 清 sid**。\r
+\r
+### PTT 异常\r
+\r
+长按松手且录音 < `PTT_MIN_RECORD_MS`:cancel、**play_tts notify**、idle。\r
+\r
+## 2. 代码结构(三件套)\r
+\r
+| 模块 | 文件 |\r
+|------|------|\r
+| UX 四态 | `vc_ux.py` → `derive_ux_state` |\r
+| 按键表 | `vc_key_router.py` → AI / Power / PTT release |\r
+| 媒体仲裁 | `vc_media_arb.py` → `before_local` / `before_notify` / `before_llm_pcm` |\r
+\r
+`vc_mode_strategy` 仅转发 `KeyRouter`;`vc_present` 用 UX 映 LED。\r
+\r
+## 3. 符合度(当前)\r
+\r
+| 项 | 状态 |\r
+|----|------|\r
+| UX + KeyRouter + MediaArbiter 骨架 | ✅ |\r
+| 长对话 recording 重开 | ✅ |\r
+| PTT idle/thinking/tts 短按 | ✅ |\r
+| Power 短按按 UX cancel+Close | ✅ |\r
+| PTT 录音过短 notify | ✅ |\r
+| notify `play_tts` 仲裁入口 | ✅ |\r
+| `vc_session_io` 仅 FSM actions | 🔲 后续 |\r
+\r
+## 4. 迁移剩余\r
+\r
+- 收敛 `play_tts_cb` 断 WS:区分 takeover notify 与会话内 synthesis\r
+- 录音灯挂 `VC_Speech_Start_now` 首帧\r
+- 删与 `InterruptContext` 重复的 `_ptt_*` latch\r
diff --git a/1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md b/1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md
new file mode 100644 (file)
index 0000000..7b92ccb
--- /dev/null
@@ -0,0 +1,54 @@
+# 录音上传与播放
+
+> 由演示文稿 `录音上传与播放-演示文稿.pptx` 自动转换
+
+## 幻灯片 2:录音场景
+
+
+| 场景     | 内容                        | llm tts | notify tts | local play |
+| ------ | ------------------------- | ------- | ---------- | ---------- |
+| 开始录音上传 | 第一帧音频数据上传的时间点。            | tts end | tts end    | end        |
+| 录音结束   | 收到服务器VAD人声结束,ASR complete |         |            |            |
+
+
+## 幻灯片 4:播放场景
+
+
+| 类型         | 内容                             | llm tts            | notify tts             | local play                 |
+| ---------- | ------------------------------ | ------------------ | ---------------------- | -------------------------- |
+| LLM TTS    | LLM回话                          | -                  | 缓存,skip                | 缓存,skip                    |
+| Notify TTS | 我在这 语音指令反馈 电量低请充电 开机欢迎词 模式切换提醒 | 关闭llm,播放notify tts | -                      | 关闭local play, 播放notify tts |
+| Local Play | 录音start提醒 think提醒 开机 关机 充电提醒   | 关闭llm,播放local      | 关闭 notify tts, 播放local | -                          |
+
+
+## 幻灯片 5:LED场景
+
+
+| 指示灯       | 内容                     |
+| --------- | ---------------------- |
+| 录音指示灯     | 开始录音上传                 |
+| think指示灯  | ASR complete,llm start |
+| tts 播报指示灯 | tts start              |
+| 关闭指示灯     | idle                   |
+
+
+## 幻灯片 6:按键场景
+
+
+| 按键   | 当前状态      | 长对话模式                                 | 单对话模式                      | 单对话异常处理                 |
+| ---- | --------- | ------------------------------------- | -------------------------- | -------------------- |
+| 单击Ai | idle      | 开始录音进行对话,只有power键单击后,采用新sid,否则还是用旧sid | 提醒,长按圆点按钮对话。进入idle         |                      |
+| 单击Ai | recording | 停止录音,重新开始录音对话,采用旧sid                  | 不存在这种状态                    |                      |
+| 单击Ai | thinking  | 停止对话,重新开始录音对话,采用旧sid                  | 停止对话,进入idle                |                      |
+| 单击Ai | tts       | 停止播报,重新开始录音对话,采用旧sid                  | 停止播报,进入idle                |                      |
+| 长按Ai | idle      | 按单击逻辑响应                               | press开始录音上传,release结束录音上传。 | 录音时间不足,取消后继操作,播放tts notify,进入idle |
+| 长按Ai | recording | 按单击逻辑响应                               | 不存在这种状态                    | 录音时间不足,取消后继操作,播放tts notify,进入idle |
+| 长按Ai | thinking  | 按单击逻辑响应                               | 停止对话,开始录音上传                | 录音时间不足,取消后继操作,播放tts notify,进入idle |
+| 长按Ai | tts       | 按单击逻辑响应                               | 停止播报,开始录音上传                | 录音时间不足,取消后继操作,播放tts notify,进入idle |
+| 单击power键 | idle      | close ws,退出idle,进入休眠状态 ,清除当前sid,下次开始对话采用新sid  | 操作同长对话 |  |
+| 单击power键 | recording | 协议发指令取消本次asr数据上传,close ws,进入休眠状态,清除当前sid,下次开始对话采用新sid  | 不存在此状态|  |
+| 单击power键 | thinking  | 协议发指令取消本次对话,close ws,进入休眠状态,清除当前sid,下次开始对话采用新sid | 操作同长对话                |  |
+| 单击power键 | tts       | 协议发指令取消本次tts播报,close ws,进入休眠状态,清除当前sid,下次开始对话采用新sid| 操作同长对话    |  |
+| 长按power键3秒 | 任意状态      | 播放关机提示音,关闭设备电源  | 操作同长对话 |  |
+
+
index 152fe30..1c8fa20 100644 (file)
@@ -978,6 +978,14 @@ frozen_mpy_list =  $(BUILD)/_boot.mpy \
                                        $(BUILD)/vc_auth.mpy \
                                        $(BUILD)/vc_ota.mpy \
                                        $(BUILD)/vc_fsm.mpy \
+                                       $(BUILD)/vc_interrupt.mpy \
+                                       $(BUILD)/vc_present.mpy \
+                                       $(BUILD)/vc_ux.mpy \
+                                       $(BUILD)/vc_key_router.mpy \
+                                       $(BUILD)/vc_media_arb.mpy \
+                                       $(BUILD)/vc_overlay.mpy \
+                                       $(BUILD)/vc_notify.mpy \
+                                       $(BUILD)/vc_mode_strategy.mpy \
                                        $(BUILD)/vc_session_imports.mpy \
                                        $(BUILD)/vc_session_io.mpy \
                                        $(BUILD)/vc_session_media.mpy \
index e1439b1..e43dcaa 100644 (file)
@@ -246,10 +246,20 @@ def main_init():
             usys.print_exception(e) 
 
         try:
-            import ai_alarm
-            app.append_manager(ai_alarm.AI_alarm())
-            app.append_manager(ai_alarm.schedule())
-            print("AI_alarm:{}".format(utime.localtime()))
+            _alarm_on = True
+            try:
+                import vc_config
+                if vc_config:
+                    _alarm_on = vc_config.get_bool("AI_ALARM_ENABLED", True)
+            except Exception:
+                pass
+            if _alarm_on:
+                import ai_alarm
+                app.append_manager(ai_alarm.AI_alarm())
+                app.append_manager(ai_alarm.schedule())
+                print("AI_alarm:{}".format(utime.localtime()))
+            else:
+                print("AI_alarm: skipped (AI_ALARM_ENABLED=False)")
         except Exception as e:
             usys.print_exception(e) 
 
index a06287d..e7c2e84 100644 (file)
@@ -1,65 +1,72 @@
-# 设备分支配置:7PIN_1_5\r
-# 仅用于编译/打包阶段选择,不作为运行时加载入口。\r
-\r
-DEV_TYPE = "7PIN_1_5"\r
-\r
-LED_GPIO = ("GPIO28", "GPIO44", "GPIO30")  # (R, G, B)\r
-LED_IDLE_GREEN_BLINK_MS = 1000   # idle 绿灯常亮时长(ms)\r
-LED_IDLE_GREEN_BLINK_OFF_MS = 2000  # idle 绿灯熄灭时长(ms)\r
-UART_SUPPORTED = True\r
-LANG = "zh"\r
-\r
-BT_POWER_MOTOR_ENABLED = True\r
-PIN_BT_ON_GPIO = "GPIO31"\r
-PIN_MOT_GPIO = "GPIO36"\r
-MOTOR_PIN_GPIO = "GPIO36"\r
-MOTOR_BOOT_VIBRATE_MS = 500\r
-AUDIO_QDAI_CFG = (1, 0, 0, 2, 0, 1, 1, 1)\r
-\r
-EXTINT_AI_GPIO = "GPIO43"\r
-PWK_KEY_GPIO = "GPIO41"\r
-\r
-BT_WAKEUP_4G_ENABLED = True\r
-EXTINT_WAKEUP_CTA1_GPIO = "GPIO22"\r
-PIN_WAKEUP_BT_GPIO = "GPIO21"\r
-\r
-# ---- 按键/防抖差异 ----\r
-KEY_RELEASE_DEBOUNCE_COUNT = 5\r
-AI_LONG_PRESS_POWER_OFF_ENABLED = False\r
-\r
-# ---- 开机提示/语音参数默认值 ----\r
-POWER_ON_TONE_ENABLED = True\r
-POWER_ON_TONE_DEFAULT = "你好,我来了"\r
-VAD_SILENCE_DEFAULT = 1000\r
-VC_TTS_VOICE_TYPE_DEFAULT = None\r
-VC_TTS_VOICE_KEY_DEFAULT = None\r
-\r
-# VC WebSocket 路径(ws://{AMP_SERVER}/{VC_WS_PATH}...)\r
-VC_WS_PATH = "vc/agent"\r
-\r
-# 是否支持蓝牙 OTA\r
-BT_OTA_ENABLED = True\r
-UPLOAD_START_TONE_ENABLED = False\r
-THINKING_LED_MODE = "flicker_blue_green"\r
-VAD_ACTIVE_BLUE_LED = True\r
-CMD_TOGGLE_MODE_TO_PTT_TTS = "已切换到单对话模式"\r
-CMD_TOGGLE_MODE_TO_LONG_TTS = "已切换到长对话模式"\r
-BT_VOICE_FEATURES_ENABLED = True\r
-POWEROFF_CMD_CLOSE_ONLY = True\r
-KWS_PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
-DIALOG_BOT_OPTION = None\r
-DIALOG_PROVIDER_DEFAULT = None\r
-KEYWORDS_FROM_VOICE_TYPE = False\r
-\r
-# ---- 按键音/提示音策略 ----\r
-KEY_PRESS_TONE_FILE = None\r
-KEY_PRESS_TONE_DELAY_MS = 300\r
-RECORD_START_TONE_FILE = "U:/static/start.mp3"\r
-# VC 空闲/超时(ai_VC._1S_timer_cb_);长对话无下行超时\r
-VC_IDLE_AUTO_CLOSE_SEC = 20\r
-# 单对话不再用「无 WS 帧」单独关会话;统一用 idle 累计 + VC_IDLE_DEEP_SLEEP_SEC\r
-VC_IDLE_AUTO_CLOSE_SEC_PTT = 0\r
-VC_IDLE_DEEP_SLEEP_SEC = 60\r
-VC_IDLE_DEEP_SLEEP_ENABLE = 1\r
-PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
-\r
+# 设备分支配置:7PIN_1_5
+# 仅用于编译/打包阶段选择,不作为运行时加载入口。
+
+DEV_TYPE = "7PIN_1_5"
+
+# 本机型无闹钟/日程硬件业务;IoT「查找设备」等走 play_tts(find_device)
+AI_ALARM_ENABLED = False
+
+LED_GPIO = ("GPIO28", "GPIO44", "GPIO30")  # (R, G, B)
+LED_IDLE_GREEN_BLINK_MS = 1000   # idle 绿灯常亮时长(ms)
+LED_IDLE_GREEN_BLINK_OFF_MS = 2000  # idle 绿灯熄灭时长(ms)
+UART_SUPPORTED = True
+LANG = "zh"
+
+BT_POWER_MOTOR_ENABLED = True
+PIN_BT_ON_GPIO = "GPIO31"
+PIN_MOT_GPIO = "GPIO36"
+MOTOR_PIN_GPIO = "GPIO36"
+MOTOR_BOOT_VIBRATE_MS = 500
+AUDIO_QDAI_CFG = (1, 0, 0, 2, 0, 1, 1, 1)
+
+EXTINT_AI_GPIO = "GPIO43"
+PWK_KEY_GPIO = "GPIO41"
+
+BT_WAKEUP_4G_ENABLED = True
+EXTINT_WAKEUP_CTA1_GPIO = "GPIO22"
+PIN_WAKEUP_BT_GPIO = "GPIO21"
+
+# ---- 按键/防抖差异 ----
+KEY_RELEASE_DEBOUNCE_COUNT = 5
+AI_LONG_PRESS_POWER_OFF_ENABLED = False
+
+# ---- 开机提示/语音参数默认值 ----
+POWER_ON_TONE_ENABLED = True
+POWER_ON_TONE_DEFAULT = "你好,我来了"
+# 服务端 VAD 断句静音时长(ms):句中短暂停顿勿小于 1200;可 storage vad_silence 覆盖
+VAD_SILENCE_DEFAULT = 1500
+VC_TTS_VOICE_TYPE_DEFAULT = None
+VC_TTS_VOICE_KEY_DEFAULT = None
+
+# VC WebSocket 路径(ws://{AMP_SERVER}/{VC_WS_PATH}...)
+VC_WS_PATH = "vc/agent"
+
+# 是否支持蓝牙 OTA
+BT_OTA_ENABLED = True
+UPLOAD_START_TONE_ENABLED = False
+THINKING_LED_MODE = "flicker_blue_green"
+VAD_ACTIVE_BLUE_LED = True
+CMD_TOGGLE_MODE_TO_PTT_TTS = "已切换到单对话模式"
+CMD_TOGGLE_MODE_TO_LONG_TTS = "已切换到长对话模式"
+BT_VOICE_FEATURES_ENABLED = True
+POWEROFF_CMD_CLOSE_ONLY = True
+KWS_PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+DIALOG_BOT_OPTION = None
+DIALOG_PROVIDER_DEFAULT = None
+KEYWORDS_FROM_VOICE_TYPE = False
+
+# ---- 按键音/提示音策略 ----
+KEY_PRESS_TONE_FILE = "U:/static/key_prompt.mp3"  # REQ-AI-01:短按操作确认「滴」
+KEY_PRESS_TONE_DELAY_MS = 300
+RECORD_START_TONE_FILE = "U:/static/start.mp3"  # 开录/打断续聊提示音
+RECORD_START_TONE_GAP_MS = 180
+# VC 空闲/超时(ai_VC._1S_timer_cb_);长对话无下行超时
+VC_IDLE_AUTO_CLOSE_SEC = 20
+# 单对话不再用「无 WS 帧」单独关会话;统一用 idle 累计 + VC_IDLE_DEEP_SLEEP_SEC
+VC_IDLE_AUTO_CLOSE_SEC_PTT = 0
+VC_IDLE_DEEP_SLEEP_SEC = 60
+VC_IDLE_DEEP_SLEEP_ENABLE = 1
+PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+PTT_MIN_RECORD_MS = 400
+PTT_RECORD_TOO_SHORT_TTS = "录音时间太短,请按住圆形按钮说话"
+
index 8e8a3e8..98b11d4 100644 (file)
-try:\r
-    from qp_log import qp_print as print\r
-except Exception:\r
-    pass\r
-import ujson\r
-import utime\r
-import EventMesh\r
-import constants\r
-from common import Abstract\r
-import uhashlib\r
-import ubinascii\r
-from misc import Power\r
-import audio\r
-from machine import Pin\r
-import ujson\r
-import ql_fs\r
-import dataCall\r
-import net\r
-import usocket\r
-import osTimer\r
-\r
-import machine\r
-import urandom\r
-import _thread\r
-import usys\r
-import request\r
-import umqtt \r
-import modem\r
-SHA256 = uhashlib.sha256\r
-import versions\r
-import version_util\r
-try:\r
-    import vc_config as _vc_cfg\r
-    _NLP_EXTENDED_CMDS = _vc_cfg.get_bool("NLP_EXTENDED_CMDS", False)\r
-except Exception:\r
-    _NLP_EXTENDED_CMDS = False\r
-\r
-from queue import Queue\r
-def unicode_to_utf8(src):\r
-    ref = 0xe08080\r
-    result = ref\r
-    \r
-    #取最后六位\r
-    a = src & 0x3f\r
-    \r
-    #将最后六位放在ref最后六位的空位置\r
-    result = result | a\r
-    src = src >> 6\r
-    \r
-    #取接下来的六位\r
-    a = src & 0x3f\r
-    \r
-    #放在ref对应的六个位置\r
-    result = result | (a << 8)\r
-\r
-    #保留最后四位\r
-    src = src >> 6\r
-\r
-    #放在ref对应的四个位置    \r
-    result = result | (src << 16)\r
-    return result\r
-\r
-def UnicodeHexStr_To_Utf8Str(str):\r
-    odata = 0\r
-\r
-    result = bytes() \r
-    su =str.upper()\r
-\r
-    i = 0\r
-    for c in su:\r
-        tmp=ord(c)\r
-        if tmp <= ord('9') :\r
-            odata = odata << 4\r
-            odata += tmp - ord('0')\r
-\r
-        elif ord('A') <= tmp <= ord('F'):\r
-            odata = odata << 4\r
-            odata += tmp - ord('A') + 10\r
-        if(i%4==3): #每2字节获取一个int,拿到转换后的unicode int\r
-            #print(odata.to_bytes(2, 'big'))\r
-            #result.append(odata)\r
-            if odata > 255:\r
-                tmp = unicode_to_utf8(odata)#拿到转换后的utf8 int\r
-                tmp2 = tmp.to_bytes(3, 'big') #转bytes\r
-            else :\r
-                tmp2 = odata.to_bytes(1, 'big') \r
-            #print("tmp2:{}".format(tmp2))\r
-            result = result + tmp2\r
-            odata = 0 \r
-        i+=1\r
-    return result.decode('utf-8') #返回utf8的str\r
-\r
-def HMAC(k, m):\r
-    SHA1_BLOCK_SIZE = 64\r
-    KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k)))\r
-    KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)\r
-    KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)\r
-    inner_message = KEY_INNER + m\r
-    outer_message = KEY_OUTER + SHA256(inner_message).digest()\r
-    return SHA256(outer_message)\r
-\r
-\r
-def get_hmac_sha256(k, m):\r
-    data = HMAC(k, m).digest()\r
-    # print("HMAC(k, m).digest():", data)\r
-    # print("HMAC(k, m).digest():", ubinascii.hexlify(data))\r
-    hex_data = ubinascii.hexlify(data)\r
-    hex_data = hex_data.decode('ascii')\r
-    return hex_data\r
-\r
-######################IOT######################\r
-import sms\r
-import voiceCall\r
-import quecgnss\r
-import wifiScan\r
-from misc import Power,USB\r
-def wifiScan_cb(args):\r
-    print("wifi_data:{}".format(args))\r
-    EventMesh.publish(constants.EVENT.WIFIINFO, args)\r
-    wifiScan.control(0)\r
-#定位thered\r
-class IOTWebLocationManager(Abstract):\r
-\r
-    def __init__(self):\r
-        self.gps = None\r
-        #self.match = GPSMatch()\r
-        self.timer_read_GPS_info = osTimer()\r
-        self.timer_sos_report_info = osTimer()\r
-        self.wifi_data = []\r
-        self.cell_data = []\r
-        self.location_timer_count = 0\r
-        self.location_check_data_timer_count = 0\r
-        self.GGA_count = 0\r
-        self.RMC_A_count = 0\r
-        self.location_type = 0 #定位类型,0:正常定位,有gps数据或60秒关闭;1:测试模式,只有发CLOSE_LOCATION消息才关闭\r
-        self.location_flag = 0\r
-        self.start_location_flag = 0\r
-        self.location_thread_id  = 0\r
-        self.location_queue = Queue(100)\r
-        self.lpm_fd = None\r
-        self.start_agps = 0\r
-        self.sos_report_flag=0\r
-        quecgnss.gnssEnable(0)\r
-\r
-    def post_processor_after_instantiation(self):\r
-        EventMesh.subscribe(constants.EVENT.START_LOCATION, self.start_location)\r
-        EventMesh.subscribe(constants.EVENT.WIFIINFO, self.WIFI_INFO)\r
-        EventMesh.subscribe(constants.EVENT.WIFISCANOPEN,self.WIFI_scan_open)\r
-        EventMesh.subscribe(constants.EVENT.CLOSE_LOCATION,self.close_location)\r
-        EventMesh.subscribe("get_location_flag",self.get_location_flag)\r
-        #EventMesh.subscribe(constants.EVENT.GPSCLOSE, self.gps_close)\r
-    def get_location_flag(self, event, msg):\r
-        return self.location_flag\r
-    \r
-    def sos_report_info(self):\r
-        EventMesh.debug_log("sos_report_info:")\r
-        if self.sos_report_flag == 1:\r
-            return\r
-        self.soswifi_data = []\r
-        self.soscell_data = []\r
-        self.sos_report_flag = 1\r
-        self.timer_sos_report_info.start(3000, 1, self.sos_timer)\r
-    \r
-    def close_sos(self):\r
-        self.soswifi_data = []\r
-        self.soscell_data = []\r
-        self.sos_report_flag=0\r
-        self.timer_sos_report_info.stop()\r
-\r
-    \r
-    def sos_timer(self, *args, **kwargs):\r
-        if self.sos_report_flag == 1:\r
-            try :   \r
-                #检查wifi数据\r
-                if self.soswifi_data == [] or (self.soswifi_data != [] and self.soswifi_data[0] == 0):#and self.wifi_data[0] <= 0:\r
-                    EventMesh.publish(constants.EVENT.WIFISCANOPEN)\r
-                    \r
-                #检查基站数据\r
-                if self.soscell_data== []:   \r
-                    data_tmp = net.getCellInfo()\r
-                    if(len(data_tmp)>=2):\r
-                        for cell in data_tmp:\r
-                            if cell and cell[0][0] == 0 and len(cell[0]) > 1:\r
-                                self.soscell_data = cell\r
-                                break\r
-                if self.soscell_data != [] and self.soswifi_data != []:\r
-                    self.close_sos()\r
-                    self.Location_send([WEB_CMD_ENUM.SOS_REPORT_S_CMD,self.soswifi_data,self.soscell_data])\r
-\r
-            except Exception as e:\r
-                usys.print_exception(e)\r
-\r
-    def start_location(self, event, msg):\r
-        self.location_queue.put(msg)#测试类型入队\r
-        self.wifi_data = []\r
-        self.cell_data = []\r
-        print("start_location:{} self.location_flag:{}".format(msg,self.location_flag))\r
-        if( self.location_flag == 1):\r
-            return\r
-        self.location_flag = 1\r
-        _thread.stack_size((9+5)*1024)\r
-        self.location_thread_id  = _thread.start_new_thread(self.location_thread, ())\r
-        _thread.stack_size(8*1024)\r
-        # self.timer_read_GPS_info.start(1000, 1, self.location_timer)\r
-        # if msg == WEB_CMD_ENUM.SOS_REPORT_S_CMD:\r
-        #     self.sos_report_info()\r
-        #     pass\r
-        # elif msg == WEB_CMD_ENUM.GPS_REPORT_S_CMD:\r
-        #     self.gps_report_init()\r
-            # pass\r
-    def gps_report_init(self):\r
-\r
-        EventMesh.debug_log("start_location:")\r
-        if self.location_flag == 1:\r
-            return\r
-        self.wifi_data = []\r
-        self.cell_data = []\r
-        self.location_timer_count = 0\r
-        self.location_flag = 1\r
-        self.timer_read_GPS_info.start(3000, 1, self.location_timer)\r
-\r
-\r
-    def close_location(self,event,msg):\r
-        self.location_flag = 0\r
-        if  wifiScan.getState() == True:\r
-            wifiScan.control(0)\r
-        # self.timer_read_GPS_info.stop()\r
-        if self.location_queue.empty() == False:\r
-            self.location_queue.clear()\r
-        if self.location_thread_id != 0:\r
-            try:\r
-                _thread.stop_thread(self.location_thread_id)\r
-            except Exception as e:\r
-                pass\r
-        self.location_thread_id=0\r
-        print("close_location:!!!!!!!!!")\r
-\r
-        \r
-\r
-    def WIFI_INFO(self, event, msg):\r
-        self.wifi_data = msg\r
-\r
-    def WIFI_scan_open(self, event, msg):\r
-\r
-        if wifiScan.getState() == False :\r
-            self.wifi_data = []\r
-            wifiScan.control(1)\r
-            wifiScan.setCfgParam(6, 2, 5,  0)  #设置WiFi参数,10秒上层,两轮,最多6个,6秒底层,PS优先\r
-            wifiScan.setCallback(wifiScan_cb) #注册回调函数\r
-            wifiScan.asyncStart()\r
-            #EventMesh.debug_log("wifiScan.asyncStart")\r
-        \r
-    def location_timer(self, *args, **kwargs):\r
-        if self.location_flag == 1:\r
-            try :   \r
-                #检查wifi数据\r
-                if self.wifi_data == [] or (self.wifi_data != [] and self.wifi_data[0] == 0):#and self.wifi_data[0] <= 0:\r
-                    EventMesh.publish(constants.EVENT.WIFISCANOPEN)\r
-                    \r
-                #检查基站数据\r
-                if self.cell_data == []:   \r
-                    data_tmp = net.getCellInfo()\r
-                    if(len(data_tmp)>=2):\r
-                        for cell in data_tmp:\r
-                            if cell and cell[0][0] == 0 and len(cell[0]) > 1:\r
-                                self.cell_data = cell\r
-                                break\r
-                if self.cell_data != [] and self.wifi_data != []:\r
-                    self.close_location(0,0)\r
-                    self.Location_send([WEB_CMD_ENUM.POST_LOCATION_S_CMD,self.wifi_data,self.cell_data])\r
-\r
-            except Exception as e:\r
-                usys.print_exception(e)\r
-       \r
-    def Location_send(self, msg) :\r
-        EventMesh.debug_log("Location_send1")\r
-        if self.location_queue.empty() == True:\r
-            return\r
-        EventMesh.debug_log("Location_send2")\r
-        print("Location_send1")\r
-        while True:\r
-            if self.location_queue.empty() == False:\r
-                TYPE = self.location_queue.get()\r
-                # (5, [('70:2A:D7:63:BD:B4', -38), ('C0:94:AD:80:21:5F', -61), ('D8:38:0D:05:1F:E3', -65), ('08:9E:84:E7:A6:38', -73), ('B0:44:14:BC:13:B0', -73)])\r
-                wifi_tmp = []\r
-                wifi_num = msg[0][0]\r
-                print("wifi_num:{}".format(wifi_num))\r
-                if  wifi_num == 0:\r
-                    wifi_tmp = []\r
-                else: #([], [], [(0, 69483137, 1120, 1, 385, 30504, 100, -75, -8), (3, 111950593, 1120, 1, 58, 30504, 1650, -98, -19), (3, 219097617, 1120, 17, 76, 30504, 2452, -106, -23), (3, 102345712, 1120, 1, 58, 30504, 3740, -96, -18)])\r
-                    \r
-                    wifi_tmp_data = msg[0][1]\r
-                    print(wifi_tmp_data)\r
-                    for i in range(wifi_num):\r
-                        list_data = {}\r
-                        list_data["mac"] = str(wifi_tmp_data[i][0])\r
-                        list_data["rsi"] = str(wifi_tmp_data[i][1])\r
-                        # list_data = "{},{}".format(wifi_tmp_data[i][0],wifi_tmp_data[i][1])\r
-\r
-                        wifi_tmp.append(list_data)\r
-                    pass\r
-                # ([(flag, cid, mcc, mnc, lac, arfcn, bsic, rssi)], [(flag, cid, licd, mcc, mnc, lac, uarfcn, psc, rssi)], [(flag, cid, mcc, mnc, pci, tac, earfcn, rssi, rsrq),...])\r
-                date_tmp = utime.localtime()\r
-                time_tmp = utime.mktime(date_tmp)\r
-                celList = []\r
-                if msg[1] != []:\r
-                    for CELL in msg[1] :\r
-                        print("CELL:{}".format(CELL))\r
-                        CELL_data = {}\r
-                        CELL_data["mcc"] = str(CELL[2])\r
-                        CELL_data["mnc"] = str(CELL[3])\r
-                        CELL_data["lac"] = str(CELL[4])\r
-                        CELL_data["cid"] = str(CELL[1])\r
-                        CELL_data["rsi"] = str(CELL[7])\r
-\r
-\r
-                        # CELL_data += str(CELL[2]) +"," #MCC\r
-                        # CELL_data += str(CELL[3]) +"," #MNC\r
-                        # CELL_data += str(CELL[4]) +"," #LAC\r
-                        # CELL_data += str(CELL[7]) +"," #RSSI\r
-                        celList.append(CELL_data)\r
-                s_msg=""\r
-                s_cmd = ""\r
-                if TYPE == WEB_CMD_ENUM.POST_LOCATION_S_CMD:\r
-                    s_msg = "上报经纬度"\r
-                    s_cmd = "post_location"\r
-                elif TYPE == WEB_CMD_ENUM.POST_SOS_S_CMD:\r
-                    s_msg = "上报SOS求救"\r
-                    s_cmd = "post_sos"\r
-                send_data = {\r
-                    "cmd":s_cmd,\r
-                    "ts":int(time_tmp),\r
-                    "msg":s_msg,\r
-                    "data":{\r
-                        "gps": {\r
-                            "longitude": "0",\r
-                            "latitude": "0",\r
-                            "located": "0",\r
-                            "speed": "0"\r
-                        },\r
-                        "wifiList":wifi_tmp,\r
-                        "celList":celList\r
-                        \r
-                    }\r
-                }\r
-                print("TYPE{}".format(TYPE))\r
-                EventMesh.publish(TYPE,send_data)\r
-            else:\r
-                self.location_thread_id=0\r
-                EventMesh.publish(constants.EVENT.CLOSE_LOCATION)\r
-                break\r
-\r
-\r
-    def location_thread(self):\r
-        \r
-        self.wifi_data = []\r
-        print("location_thread self.location_flag {}".format(self.location_flag))\r
-        while self.location_flag == 1:\r
-            try :   \r
-\r
-                #检查wifi数据\r
-                if self.wifi_data == [] or (self.wifi_data != [] and self.wifi_data[0] == 0):#and self.wifi_data[0] <= 0:\r
-                    EventMesh.publish(constants.EVENT.WIFISCANOPEN)\r
-                    \r
-                #检查基站数据\r
-                if self.cell_data == []:   \r
-                    print("location_thread")\r
-                    data_tmp = net.getCellInfo()\r
-                    if(len(data_tmp)>=2):\r
-                        for cell in data_tmp:\r
-                            if cell and cell[0][0] == 0 and len(cell[0]) > 1:\r
-                                self.cell_data = cell\r
-                                break\r
-\r
-                if self.cell_data != [] and self.wifi_data != []:\r
-                    self.Location_send([self.wifi_data,self.cell_data])\r
-                    \r
-                    break\r
-                \r
-            except Exception as e:\r
-                usys.print_exception(e)\r
-                EventMesh.publish(constants.EVENT.CLOSE_LOCATION)#关闭定位 \r
-            utime.sleep_ms(1000)\r
-        wifiScan.control(0)\r
-\r
-                    \r
-\r
-import uwebsocket\r
-import sim\r
-# 设备主动上报后台返回cmd的字符串\r
-DEVICE_REPORT_RETURN_STRING = "post_rest" \r
-# 服务器主动下发给设备的cmd的字符串\r
-IOT_AUTO_REPORT_RETURN_STRING = "dispatch_rest" \r
-MAGIC_STR = len("FFFE")\r
-VERSION_STR =len("03")\r
-TYPE_STR = len("03")\r
-PLAYLOAD_STR = len("0000")\r
-PROTOCLO_LEN = MAGIC_STR+VERSION_STR+TYPE_STR+PLAYLOAD_STR+len('{')+len('}')+len("cmd:")+len("data:")+len("ts:")\r
-\r
-class WEB_CMD_ENUM:\r
-    HR_R_CMD = "post_heart_beat_R"\r
-    HR_S_CMD = "post_heart_beat_S"\r
-\r
-    POST_LOCATION_R_CMD = "post_location_R"\r
-    POST_LOCATION_S_CMD = "post_location_S"\r
-\r
-    POST_SOS_R_CMD = "post_sos_R"\r
-    POST_SOS_S_CMD = "post_sos_S"\r
-\r
-    POST_DEVICE_STATE_R_CMD = "post_device_state_R"\r
-    POST_DEVICE_STATE_S_CMD = "post_device_state_S"\r
-\r
-    POST_ALARM_R_CMD = "post_alarm_R"\r
-    POST_ALARM_S_CMD = "post_alarm_S"\r
-\r
-    POST_SCHEDULE_R_CMD = "post_schedule_R"\r
-    POST_SCHEDULE_S_CMD = "post_schedule_S"\r
-\r
-    FIND_DEVCE_R_CMD = "find_device_R"\r
-    FIND_DEVCE_S_CMD = "find_device_S"\r
-\r
-    LOCATION_NOW_R_CMD = "locate_now_R"\r
-    LOCATION_NOW_S_CMD = "locate_now_S"\r
-\r
-    LOCATION_DURATION_R_CMD = "locate_duration_R"\r
-    LOCATION_DURATION_S_CMD = "locate_duration_S"\r
-\r
-    ALARM_CONFIG_R_CMD = "alarm_config_R"\r
-    ALARM_CONFIG_S_CMD = "alarm_config_S"\r
-\r
-    ALARM_LIST_R_CMD = "alarm_list_R"\r
-    ALARM_LIST_S_CMD = "alarm_list_S"\r
-\r
-    SCHEDULE_LIST_R_CMD = "schedule_list_R"\r
-    SCHEDULE_LIST_S_CMD = "schedule_list_S"\r
-    \r
-    SOS_LIST_R_CMD ="sos_list_R"\r
-    SOS_LIST_S_CMD ="sos_list_S"\r
-\r
-    SOS_CONFIG_R_CMD = "sos_config_R"\r
-    SOS_CONFIG_S_CMD = "sos_config_S"\r
-\r
-    DEVICE_SHAKE_STATE_R_CMD = "device_shake_state_R"\r
-    DEVICE_SHAKE_STATE_S_CMD = "device_shake_state_S"\r
-\r
-    RING_TONE_URL_R_CMD = "ring_tone_url_R"\r
-    RING_TONE_URL_S_CMD = "ring_tone_url_S"\r
-\r
-    VOLUME_R_CMD = "volume_R"\r
-    VOLUME_S_CMD = "volume_S"\r
-\r
-    TEXT_MSG_R_CMD = "text_msg_R"\r
-    TEXT_MSG_S_CMD = "text_msg_S"\r
-\r
-    VOICE_RECORD_R_CMD = "voice_record_R"\r
-    VOICE_RECORD_S_CMD = "voice_record_S"\r
-\r
-    GET_RINGTONE_R_CMD = "get_ringtone_R"\r
-    GET_RINGTONE_S_CMD = "get_ringtone_S"\r
-\r
-    RINGTONE_LIST_R_CMD = "ringtone_list_R"\r
-    RINGTONE_LIST_S_CMD = "ringtone_list_S"\r
-\r
-    SCHEDULE_R_CMD = "schedule_R"\r
-    SCHEDULE_S_CMD = "schedule_S"\r
-    \r
-    SWITCH_BT_STATE_R_CMD="switch_bt_state_R"\r
-    SWITCH_BT_STATE_S_CMD="switch_bt_state_S"\r
-\r
-\r
-    BT_SCAN_R_CMD="bt_scan_R"\r
-    BT_SCAN_S_CMD="bt_scan_S"\r
-\r
-    BT_STATE_R_CMD="bt_state_R"\r
-    BT_STATE_S_CMD="bt_state_S"\r
-\r
-    BT_SCAN_REST_R_CMD = "bt_scan_rest_R"\r
-    BT_SCAN_REST_S_CMD = "bt_scan_rest_S"\r
-\r
-    CONNECT_BT_R="connect_bt_R"\r
-    CONNECT_BT_S="connect_bt_S"\r
-\r
-    DISCONNECT_BT_R="disconnect_bt_R"\r
-    DISCONNECT_BT_S="disconnect_bt_S"\r
-\r
-    NEW_VOICE_MSG_R = "new_voice_msg_R"\r
-    NEW_VOICE_MSG_S = "new_voice_msg_S"\r
-\r
-    GET_NEW_VOICE_MSG_R="get_new_voice_msg_R"\r
-    GET_NEW_VOICE_MSG_S="get_new_voice_msg_S"\r
-\r
-    NEW_VOICE_MSG_LIST_R="new_voice_msg_list_R"\r
-    NEW_VOICE_MSG_LIST_S="new_voice_msg_list_S"\r
-\r
-    PUT_VOICE_MSG_STATE_R="put_voice_msg_state_R"\r
-    PUT_VOICE_MSG_STATE_S="put_voice_msg_state_S"\r
-\r
-    GET_BT_STATE_R="get_bt_state_R"\r
-    GET_BT_STATE_S="get_bt_state_S"\r
-\r
-    GET_BT_CONNECT_RECORD_LIST_R = "get_bt_connect_record_list_R"\r
-    GET_BT_CONNECT_RECORD_LIST_S = "get_bt_connect_record_list_S"\r
-\r
-    BT_CONNECT_RECORD_LIST_R="bt_connect_record_list_R"\r
-    BT_CONNECT_RECORD_LIST_S="bt_connect_record_list_S"\r
-\r
-    FENCE_SOS_R="fence_sos_R"\r
-    FENCE_SOS_S="fence_sos_S"\r
-\r
-    PLAY_CONTROL_R="player_control_R"\r
-    PLAY_CONTROL_S="player_control_S"\r
-\r
-    GET_CHAT_CONFIG_R="get_chat_config_R"\r
-    GET_CHAT_CONFIG_S="get_chat_config_S"\r
-\r
-    CHAT_CONFIG_R = "chat_config_R"\r
-    CHAT_CONFIG_S = "chat_config_S"\r
-\r
-    POST_BT_CONNECT_REST_R = "post_bt_connect_rest_R"\r
-    POST_BT_CONNECT_REST_S = "post_bt_connect_rest_S"\r
-\r
-    UNBIND_DEV_R="unbind_dev_R"\r
-    UNBIND_DEV_S="unbind_dev_S"\r
-\r
-    USER_UNBIND_DEV_R="user_unbind_dev_R"\r
-    USER_UNBIND_DEV_S="user_unbind_dev_S"\r
-\r
-    MUSIC_PLAY_R="music_play_R"\r
-    MUSIC_PLAY_S="music_play_S"\r
-\r
-    NLP_PLAY_R = "ai_cmd_R"\r
-    NLP_PLAY_S = "ai_cmd_S"\r
-\r
-    #金三\r
-    GET_DEVICE_STATE_R = "get_device_state_R"\r
-    GET_DEVICE_STATE_S = "get_device_state_S"\r
-class IOTSendCmdManager(Abstract):\r
-    #\r
-    def __init__(self):\r
-        self.send_data = ""\r
-        self.time_tmp = 0\r
-    def post_processor_after_instantiation(self):\r
-        EventMesh.subscribe("GET_IOT_WEB_SEND_CMD", self.get_iot_send_cmd)\r
-        print("IOTSendCmdManager")\r
-\r
-    def get_iot_send_cmd(self, event, msg):\r
-        date_tmp = utime.localtime()\r
-        self.time_tmp = utime.mktime(date_tmp)\r
-        if msg == WEB_CMD_ENUM.HR_S_CMD:\r
-            self.send_data = self.hr_send_data(self.time_tmp)\r
-            return self.send_data\r
-        elif msg == WEB_CMD_ENUM.POST_LOCATION_S_CMD:\r
-            self.send_data = self.gps_send_data()\r
-            return self.send_data\r
-        elif msg == WEB_CMD_ENUM.POST_SOS_S_CMD:\r
-            self.send_data = self.sos_wraing_send_data()\r
-            return self.send_data\r
-        elif msg == WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD:\r
-           self.send_data = self.device_report_state_send_data()\r
-           return self.send_data\r
-        elif msg == WEB_CMD_ENUM.POST_ALARM_S_CMD:\r
-            self.send_data = self.alarm_config_send_data()\r
-        elif msg == WEB_CMD_ENUM.POST_SCHEDULE_S_CMD:\r
-            self.send_data = self.schedule_config_send_data()\r
-        return self.send_data\r
-    \r
-    # 心率上报数据格式\r
-    def hr_send_data(self,time):\r
-        print("hr_send_data")\r
-        battery = (EventMesh.publish("screen_get_battery"))[1]\r
-        if battery >= 100 or battery == None:\r
-            battery = 100\r
-        hr_send_data = {\r
-            "cmd":"post_heart_beat",\r
-            "ts":time,\r
-            "msg":"心跳",\r
-            "data":{\r
-                "battery": int(battery)\r
-            }\r
-        }\r
-        return hr_send_data\r
-    \r
-    # gps上报数据格式\r
-    def gps_send_data(self):\r
-        gps_data = {\r
-            "cmd":"post_location",\r
-            "ts":self.time_tmp,\r
-            "data":{\r
-                "gps":{\r
-                    "longitude":"W,67",\r
-                    "latitude":"S,465",\r
-                    "direction":"FR",\r
-                    "speed":"45",\r
-                    "altude":"56",\r
-                    "count":"3"\r
-                },\r
-                "wifiList":[\r
-                    "89:78:78:67:89:98,-98"\r
-                ],\r
-                "celList":[\r
-                    "MCC,MNC,LAC,CID,RSSI"\r
-                ],\r
-                "btList":[\r
-                    "D4:78:65:67:89:78"\r
-                ]\r
-            }\r
-        }\r
-        return gps_data\r
-    \r
-    # sos警告上报数据格式\r
-    def sos_wraing_send_data(self):\r
-        sos_wraing_data ={\r
-                "cmd":"post_sos",\r
-                "ts":self.time_tmp,\r
-                "data":{\r
-                    "gps":{\r
-                        "longitude":"W,67",\r
-                        "latitude":"S,465",\r
-                        "direction":"FR",\r
-                        "speed":"45",\r
-                        "altude":"56",\r
-                        "count":"3"\r
-                    },\r
-                    "wifiList":[\r
-                        "89:78:78:67:89:98,-98"\r
-                    ],\r
-                    "celList":[\r
-                        "MCC,MNC,LAC,CID,RSSI"\r
-                    ],\r
-                    "btList":[\r
-                        "D4:78:65:67:89:78"\r
-                    ]\r
-                }\r
-            }\r
-        return sos_wraing_data\r
-    \r
-    #设备上报状态上报数据格式\r
-    def device_report_state_send_data(self):\r
-        get_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")\r
-        device_report_data ={\r
-            "cmd":"post_device_state",\r
-            "ts":self.time_tmp,\r
-            "data":{\r
-                "mediaTitle":"haha",\r
-                "mediaState":"stop",\r
-                "cover":"899",\r
-                "volume":get_volume\r
-            }\r
-        }\r
-        # HR_DATA=EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD)\r
-        # print("HR_DATA{}".format(HR_DATA))\r
-        #  EventMesh.publish(WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD,HR_DATA)\r
-        return device_report_data\r
-\r
-    #闹钟配置上报数据格式\r
-    def alarm_config_send_data(self):\r
-        alarm_data = {\r
-            "cmd":"post_alarm",\r
-            "ts":self.time_tmp,\r
-            "msg":"上报闹钟",\r
-            "data":[\r
-                {\r
-                    "name":"喝水",\r
-                    "loop":"0101001",\r
-                    "state":"0",\r
-                    "time":"14:34",\r
-                    "ringTimes":1,\r
-                    "alarmMode":"ring",\r
-                    "shakeType":"short"\r
-                }, \r
-                {\r
-                    "name":"起床",\r
-                    "loop":"0101001",\r
-                    "state":"0",\r
-                    "time":"07:40",\r
-                    "ringTimes":1,\r
-                    "alarmMode":"ring",\r
-                    "shakeType":"short"\r
-                }\r
-            ]\r
-        }\r
-            \r
-        return alarm_data\r
-    #日程配置上报数据格式\r
-    def schedule_config_send_data(self):\r
-        schedule_data = {\r
-            "cmd":"post_schedule",\r
-            "ts":self.time_tmp,\r
-            "msg":"上报日程",\r
-            "data":[\r
-                {\r
-                    "name":"喝水",\r
-                    "loop":"0101001",\r
-                    "state":"0",\r
-                    "time":"14:34",\r
-                    "ringTimes":1,\r
-                    "alarmMode":"ring",\r
-                    "shakeType":"short"\r
-                }, \r
-                {\r
-                    "name":"起床",\r
-                    "loop":"0101001",\r
-                    "state":"0",\r
-                    "time":"07:40",\r
-                    "ringTimes":1,\r
-                    "alarmMode":"ring",\r
-                    "shakeType":"short"\r
-                }\r
-            ]\r
-        }\r
-            \r
-        return schedule_data\r
-#解析收到的数据协议\r
-def playpolad_data(data):\r
-    if(isinstance(data, bytes) == 0 or len(data) <= PROTOCLO_LEN):\r
-        return\r
-\r
-    if data[0] != 0xff and data[1] !=0xfe and data[2] != 0x03 and data[3] !=0x03:\r
-        return\r
-    \r
-    # print(type(data))\r
-    json_bytes = data[8:]\r
-    # print(data[0],data[1],data[2],data[3],data[4],data[5],data[6],data[7])\r
-    # 2. 将二进制JSON转换为字符串(使用UTF-8解码)\r
-    json_str = json_bytes.decode('utf-8')\r
-    print("解析出的JSON字符串:")\r
-    print(json_str)\r
-    print("\n" + "-"*50 + "\n")\r
-\r
-    # 3. 将JSON字符串转换为Python字典\r
-    try:\r
-        data_dict = ujson.loads(json_str)\r
-        print("解析后的字典数据:")\r
-        print(data_dict)\r
-        \r
-        # 可以直接访问字典中的字段\r
-        \r
-        cmd =  data_dict['cmd']\r
-        if cmd !=  None:\r
-            if cmd == DEVICE_REPORT_RETURN_STRING:\r
-                data_cmd =  data_dict['data']\r
-                if "cmd" in data_cmd:\r
-                    cmd =  data_dict['data']["cmd"]\r
-                # {"cmd ":"post_rest","ts":1753870158426,"data":{"cmd":"post_heart_beat","rest":"success","msg":"."}}\r
-                    print("设备主动上报后台返回的数据 设备不需要回复")\r
-                    EventMesh.publish(cmd+"_R",data_dict)\r
-                else:\r
-                    print("返回的data里面没有cmd 返回的数据出错了")\r
-            else:\r
-                # {"cmd":"locate_now","msg":"","data":"876226211472149","ts":1753870224117}\r
-                print("后台主动下发给设备 需要设备回复给后台的数据命令") #后台主动下发的数据不为dict类型\r
-                EventMesh.publish(cmd+"_R",data_dict)\r
-        else:\r
-            print("cmd is None") #下发的指令无不解析\r
-    except Exception as e:\r
-        print("解析JSON出错:", str(e))\r
-date_tmp = utime.localtime()\r
-time_tmp = utime.mktime(date_tmp)\r
-MAGIC =  (0xfffe).to_bytes(2,"big")\r
-VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')\r
-TYPE = (0x03).to_bytes(1,"big")\r
-DATA_LEN = (0x00000000).to_bytes(4,"big")\r
-#magic+version+type+playload_len+playload\r
-#普通协议数据格式根据协议指定\r
-def protocol_send_total_data(data):\r
-    MAGIC =  (0xfffe).to_bytes(2,"big")\r
-    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')\r
-    TYPE = (0x03).to_bytes(1,"big")\r
-    HR_JSON = ujson.dumps(data)\r
-    GPS_JSON_b = HR_JSON.encode('utf-8')\r
-    DATA_LEN = (len(HR_JSON)).to_bytes(4,"big")\r
-    heards_infor = b''\r
-    heards_infor = MAGIC+VERSION+TYPE+DATA_LEN+GPS_JSON_b\r
-    return heards_infor\r
-#音频流数据格式\r
-#magic+version+type+msgid+playload_len+playload\r
-def protocol_audio_ji_playload_data(data,msgid,endflag):\r
-    MAGIC =  (0xfffe).to_bytes(2,"big")\r
-    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')\r
-    TYPE = (0x05).to_bytes(1,"big")\r
-    MSGID = msgid.to_bytes(4,"big")\r
-    ENDFLAG = endflag.to_bytes(1,"big")\r
-    DATA_LEN = (len(data)).to_bytes(4,"big")\r
-    heards_infor = b''\r
-    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data\r
-    return heards_infor\r
-\r
-#语音留言格式\r
-def protocol_yuyin_voice_playload_data(data,msgid,endflag):\r
-    MAGIC =  (0xfffe).to_bytes(2,"big")\r
-    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')\r
-    TYPE = (0x08).to_bytes(1,"big")\r
-    MSGID = msgid.to_bytes(4,"big")\r
-    ENDFLAG = endflag.to_bytes(1,"big")\r
-    DATA_LEN = (len(data)).to_bytes(4,"big")\r
-    heards_infor = b''\r
-    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data\r
-    return heards_infor\r
-\r
-#会议纪要格式\r
-def protocol_meet_playload_data(data,msgid,endflag):\r
-    MAGIC =  (0xfffe).to_bytes(2,"big")\r
-    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')\r
-    TYPE = (0x09).to_bytes(1,"big")\r
-    MSGID = msgid.to_bytes(4,"big")\r
-    ENDFLAG = endflag.to_bytes(1,"big")\r
-    DATA_LEN = (len(data)).to_bytes(4,"big")\r
-    heards_infor = b''\r
-    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data\r
-    return heards_infor\r
-\r
-#AI翻译格式\r
-def protocol_trans_playload_data(data,msgid,endflag):\r
-    MAGIC =  (0xfffe).to_bytes(2,"big")\r
-    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')\r
-    TYPE = (0x0A).to_bytes(1,"big")\r
-    MSGID = msgid.to_bytes(4,"big")\r
-    ENDFLAG = endflag.to_bytes(1,"big")\r
-    DATA_LEN = (len(data)).to_bytes(4,"big")\r
-    heards_infor = b''\r
-    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data\r
-    return heards_infor\r
-#sos紧急电话class\r
-class sosInfoCallManager(Abstract): \r
-    def __init__(self):\r
-        self.num = 0\r
-        self.num=0\r
-        self.num_len = 0\r
-        self.sos_list = []\r
-        self.sos_flag = 0\r
-        self.sos_timer1 = osTimer()\r
-        self.sos_timer2 = osTimer()\r
-        self.save_list = []\r
-        self.call_state = 0\r
-        self.sos_config = {}\r
-        self.phone110112_flag = 0\r
-        self.phone110112 = ""\r
-        if EventMesh.publish("get_storage_config","sos_list") != None:\r
-            self.sos_list = EventMesh.publish("get_storage_config","sos_list")\r
-        if EventMesh.publish("get_storage_config","sos_config") != None:\r
-            self.sos_config= EventMesh.publish("get_storage_config","sos_config")\r
-        \r
-        # self.sos_list=[{"index":0,"phone":"15521469561","name":"dengxueying0"}, {"name":"dengxueying1","index":0,"phone":"15692025051"},{"name":"dengxueying2","index":0,"phone":"15521469561"}]\r
-        EventMesh.subscribe("call_out_cb", self.call_out_cb) \r
-        EventMesh.subscribe("get_sos_flag", self.get_sos_flag) \r
-\r
-    def post_processor_after_instantiation(self):\r
-        self.num = 0\r
-        self.sos_flag = 0\r
-        voiceCall.setCallback(self.voice_callback)\r
-        EventMesh.subscribe("set_sos_info_call", self.set_sos_info_call)\r
-        EventMesh.subscribe("set_sos_list_info",self.set_sos_list_info)\r
-        EventMesh.subscribe("stop_sos_call",self.stop_sos_call)\r
-        EventMesh.subscribe("set_sos_config",self.set_sos_config)\r
-    def set_sos_config(self,event,msg):\r
-        # sos_config:{'enable110': '1', 'pressDuration': 3, 'enableSos': '0'}\r
-        print("set_sos_config:{}".format(msg))\r
-        self.sos_config = msg\r
-        # self.start_phone110112()\r
-        pass\r
-    def get_sos_flag(self,event,msg):\r
-        if self.call_state != 0:\r
-            return 1\r
-        else:\r
-            return 0\r
-        # import EventMesh\r
-        # EventMesh.publish("set_sos_info_call")\r
-    def close_call_timer(self):\r
-        self.sos_timer2.stop()\r
-        pass\r
-    def stop_sos_call(self,event,msg): #停止sos拨打\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.stop_sos_call_cb})\r
-    def stop_sos_call_cb(self):\r
-        if self.call_state == 0:\r
-            return\r
-        self.phone110112_flag=0\r
-        self.sos_flag = 0\r
-        self.call_state = 0\r
-        self.num=0 #不管有没有拨打sos都清除\r
-        self.num_len = 0\r
-        self.sos_timer2.stop()\r
-        self.sos_timer1.stop()\r
-        voiceCall.callEnd() #挂断\r
-\r
-    def start_phone110112(self):\r
-        print("start_phone110112:{} self.sos_config:{}".format(self.call_state,self.sos_config))\r
-        if self.sos_config != {}:\r
-            if "enableSos" in self.sos_config:\r
-                enableSos = self.sos_config["enableSos"]\r
-                if enableSos == "1":\r
-                    if "enable110" in self.sos_config:\r
-                        p110 = self.sos_config["enable110"]\r
-                        if p110 == "0":\r
-                            self.phone110112 = "110"\r
-                            tts_str = "正在呼叫{}".format(self.phone110112)\r
-                            EventMesh.publish("stop_voice_record") #电话优先级最高\r
-                            EventMesh.publish("stop_AI_record")\r
-                            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-                            self.sos_timer1.stop()\r
-                            self.sos_timer1.start(2000, 0, self.phone110112_cb)\r
-                        elif p110 == "1":\r
-                            self.phone110112 = "112"\r
-                            tts_str = "正在呼叫{}".format(self.phone110112)\r
-                            EventMesh.publish("stop_voice_record") #电话优先级最高\r
-                            EventMesh.publish("stop_AI_record")\r
-                            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-                            self.sos_timer1.stop()\r
-                            self.sos_timer1.start(2000, 0, self.phone110112_cb)\r
-            \r
-        else:\r
-            return 1\r
-\r
-    def sos_call_out_phone(self, msg):#电话拨出去\r
-        if self.call_state == 0:\r
-            ret = voiceCall.callStart(self.sos_phone_num)\r
-            if ret == -1:\r
-                \r
-                tts_str = "请确认卡支持语音拨号及余额充足"\r
-                EventMesh.publish("stop_voice_record") #电话优先级最高\r
-                EventMesh.publish("stop_AI_record")\r
-                EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-                self.call_state = 0\r
-                self.num=0 #不管有没有拨打sos都清除\r
-                self.num_len = 0\r
-                self.sos_flag = 0\r
-                self.phone110112_flag=0\r
-                return -1\r
-            else:\r
-                EventMesh.publish(constants.EVENT.START_LOCATION,WEB_CMD_ENUM.POST_SOS_S_CMD) #开始AL定位\r
-                self.sos_timer2.stop()\r
-                self.sos_timer2.start(30000, 0, self.sos_time_out_phone)\r
-        return 1\r
-            \r
-    def phone110112_cb(self,*argc):\r
-        if self.call_state == 0:\r
-            ret = voiceCall.callStart(self.phone110112)\r
-            if ret != -1:\r
-                self.phone110112_flag=1\r
-            else:\r
-                self.phone110112_flag=0\r
-            # print("ret:{} {}".format(ret,self.phone110112))\r
-            self.phone110112 = ""\r
-\r
-    def set_sos_list_info(self,event,msg):\r
-        self.sos_list = msg\r
-\r
-        \r
-    def call_out_cb(self,event,phonenumber):\r
-        \r
-        if self.call_state == 0:\r
-            self.sos_phone_num = phonenumber\r
-            EventMesh.publish("stop_voice_record") #电话优先级最高\r
-            EventMesh.publish("stop_AI_record")\r
-            \r
-            tts_str = "正在呼叫号码{}".format(self.sos_phone_num)\r
-            EventMesh.publish("stop_voice_record") #电话优先级最高\r
-            EventMesh.publish("stop_AI_record")\r
-            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-            self.sos_timer1.stop()\r
-            self.sos_timer1.start(2000, 0, self.sos_call_out_phone)\r
-            \r
-            \r
-      \r
-    def sos_time_out_phone(self, msg):#30s超时未打通或者信号不好则重新拨打\r
-        \r
-        if self.sos_flag == 1:\r
-            if self.call_state == 0:\r
-                voiceCall.callEnd() #挂断\r
-                EventMesh.publish("set_sos_info_call") #电话优先级最高\r
-            pass #再次拨打\r
-\r
-    def set_sos_info_call(self,event,msg):\r
-        if EventMesh.publish("get_yuyinaudio_flag") == 1 or EventMesh.publish("get_voice_record_mode") != 0: #语言留言模式不允许拨打电话\r
-            self.call_state = 0\r
-            self.num=0 #不管有没有拨打sos都清除\r
-            self.num_len = 0\r
-            self.sos_flag = 0\r
-            self.phone110112_flag=0\r
-            return\r
-        if sim.getStatus() != 1:#网络正常\r
-            tts_str = "无SIM卡"\r
-            EventMesh.publish("stop_voice_record") #电话优先级最高\r
-            EventMesh.publish("stop_AI_record")\r
-            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-            self.call_state = 0\r
-            self.num=0 #不管有没有拨打sos都清除\r
-            self.num_len = 0\r
-            self.sos_flag = 0\r
-            self.phone110112_flag=0\r
-            return\r
-        if self.call_state != 0:\r
-            return\r
-        if len(self.sos_list) > 0:\r
-            self.call_state = 0\r
-            self.call_sos_cb(1)\r
-        else:\r
-            tts_str = "未设置号码"\r
-            EventMesh.publish("stop_voice_record") #电话优先级最高\r
-            EventMesh.publish("stop_AI_record")\r
-            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-            self.call_state = 0\r
-            self.num=0 #不管有没有拨打sos都清除\r
-            self.num_len = 0\r
-            self.sos_flag = 0\r
-            self.phone110112_flag=0\r
-\r
-    def call_sos_cb(self,*argc):\r
-        self.phone110112_flag=0\r
-        if self.sos_flag == 0:\r
-            self.sos_flag = 1\r
-        sos_flag = self.sos_flag\r
-        EventMesh.debug_log('voicecall sos_flag',sos_flag)\r
-        if sos_flag == 0 :\r
-            self.num = 0\r
-        else:\r
-            if len(self.sos_list) > 0:\r
-                # for list_t in self.sos_list:\r
-                #     tmp = list_t["phone"]\r
-                #     self.save_list.append(tmp)\r
-                    \r
-                self.num +=1\r
-                if len(self.sos_list) == 1:\r
-                    self.num_len = len(self.sos_list)*3#三次 1\r
-                elif len(self.sos_list) == 2:\r
-                    self.num_len = len(self.sos_list)*3 #1+2+1+2\r
-                elif len(self.sos_list) == 3:\r
-                    self.num_len = len(self.sos_list)*3 #1+3+5\r
-                # EventMesh.debug_log("number:{} number_len :{} num:{} num_len:{}".format(number,len(number),num,num_len))\r
-                if self.num > self.num_len :\r
-                    \r
-                    self.start_phone110112()\r
-                    EventMesh.debug_log("call phone!!!!!!!")\r
-                    self.sos_flag = 0\r
-                    self.num = 0\r
-                    self.num_len = 0\r
-                else:\r
-                    self.phone110112_flag=0\r
-                    EventMesh.debug_log("self.num:{} self.num_len :{}".format(self.num,self.num_len))\r
-\r
-                    if len(self.sos_list) == 1:\r
-                        if len(self.sos_list[0]["phone"]) != 0:\r
-                            EventMesh.publish("call_out_cb", self.sos_list[0]["phone"]) \r
-                            # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["pnone"])\r
-                            EventMesh.debug_log("phone 1 {}".format(self.sos_list[0]["phone"]))\r
-                    elif len(self.sos_list) == 2:\r
-                        # 1 2 \r
-                        # 3 4 \r
-                        # 5 6\r
-                        if self.num == 1 or self.num == 3 or self.num == 5: #1 2 3 4 5 \r
-                            if len(self.sos_list[0]) != 0:\r
-                                EventMesh.debug_log("phone 2 {}".format(self.sos_list[0]["phone"]))\r
-                                EventMesh.publish("call_out_cb", self.sos_list[0]["phone"]) \r
-                                # EventMesh.publish("set_callnumer_cb",self.sos_list[1]["pnone"])\r
-                        elif self.num == 2 or self.num == 4 or self.num == 6:\r
-                            if len(self.sos_list[1]) != 0:\r
-                                EventMesh.debug_log("phone 3 {}".format(self.sos_list[1]["phone"]))\r
-                                EventMesh.publish("call_out_cb", self.sos_list[1]["phone"]) \r
-                                # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["pnone"])\r
-                    elif len(self.sos_list) == 3:\r
-                        # 1 2 3 \r
-                        # 4 5 6 \r
-                        # 7 8 9 \r
-                        if self.num == 1 or self.num == 4 or self.num == 7:\r
-                            if len(self.sos_list[0]) != 0:\r
-                                EventMesh.publish("call_out_cb", self.sos_list[0]["phone"]) \r
-                                # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["phone"])\r
-                                EventMesh.debug_log("phone 4 {}".format(self.sos_list[0]["phone"]))\r
-                        elif self.num == 2 or self.num == 5 or self.num == 8:\r
-                            if len(self.sos_list[1]) != 0:\r
-                                EventMesh.debug_log("phone 5 {}".format(self.sos_list[1]["phone"]))\r
-                                EventMesh.publish("call_out_cb", self.sos_list[1]["phone"]) \r
-                                # EventMesh.publish("set_callnumer_cb",self.sos_list[2]["phone"])\r
-                        elif self.num == 3 or self.num == 6 or self.num == 9:\r
-                            if len(self.sos_list[2]) != 0:\r
-                                EventMesh.debug_log("phone 6 {}".format(self.sos_list[2]["phone"]))\r
-                                EventMesh.publish("call_out_cb", self.sos_list[2]["phone"]) \r
-                                # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["phone"])\r
-            else: #清除表示\r
-                tts_str = "未设置号码"\r
-                EventMesh.publish("stop_voice_record") #电话优先级最高\r
-                EventMesh.publish("stop_AI_record")\r
-                EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容\r
-                self.sos_flag = 0\r
-                self.num = 0\r
-                self.call_state = 0\r
-                self.num=0 #不管有没有拨打sos都清除\r
-                self.num_len = 0\r
-                self.phone110112_flag=0\r
-                #未设置号码\r
-            \r
-\r
-    def voice_callback(self,args):\r
-        EventMesh.debug_log('111test voicecall iinfo:{}'.format(args))\r
-        if args[0] == 10: #电话拨进来\r
-            self.sos_flag = 0\r
-            self.call_state = 1 \r
-            self.phone110112_flag=0\r
-        elif args[0] == 11: #已接听\r
-            self.call_state = 2\r
-            self.num=0 #不管有没有拨打sos都清除\r
-            self.num_len = 0\r
-            self.sos_flag = 0\r
-            self.phone110112_flag=0\r
-            self.close_call_timer()\r
-\r
-            EventMesh.debug_log('voicecall connected, PhoneNO.: ', args[6])\r
-\r
-        elif args[0] == 12: #电话挂断\r
-            self.phone110112_flag=0\r
-            if self.call_state == 2 or self.call_state == 1: #电话拨出去没有接听 就挂断\r
-                pass\r
-            else:\r
-               \r
-                EventMesh.debug_log('voicecall disconnect end !!!')\r
-                self.close_call_timer()\r
-                self.call_state = 0\r
-                if self.sos_flag == 1:\r
-                    EventMesh.publish("set_sos_info_call")\r
-                pass\r
-            self.call_state = 0\r
-                #再次拨打下一个电话号码\r
-            EventMesh.debug_log('voicecall disconnect end')\r
-                \r
-\r
-        elif args[0] == 13: #\r
-            self.call_state = 7\r
-            EventMesh.debug_log('voicecall is waiting, PhoneNO.: ', args[6])\r
-        elif args[0] == 14: #电话拨出去\r
-            EventMesh.debug_log('voicecall dialing, PhoneNO.: ', args[6])\r
-            if self.sos_flag == 1:\r
-                self.call_state = 3 #当前是拨打sos号码标识\r
-            else:\r
-                self.call_state = 4\r
-                \r
-        elif args[0] == 15:\r
-            self.call_state = 5\r
-            EventMesh.debug_log('voicecall alerting, PhoneNO.: ', args[6])\r
-                    \r
-                \r
-        elif args[0] == 16:\r
-            self.call_state = 6\r
-            EventMesh.debug_log('voicecall holding, PhoneNO.: ', args[6])\r
-\r
-\r
-\r
-        \r
-try:   \r
-    import usr.ai_alarm as _schedule\r
-except Exception as e:\r
-    usys.print_exception(e)\r
-    import ai_alarm as _schedule\r
-DEFIEN_HR_REPORT_TIME = 5*60\r
-DEFINE_TIME_COUNT = 10\r
-DEFIEN_LOCATION_REPORT_TIME = DEFINE_TIME_COUNT\r
-WEB_CONNECT_MAX_COUNT = 10 #默认重连次数\r
-class SockMangager(Abstract):\r
-    def __init__(self):\r
-        self.thread_buf_size = 16*1024\r
-        self.iot_web_send_queue = Queue(100)\r
-        self.iot_web_protoclo_recv_id=0\r
-        self.iot_protocol_client = None\r
-        self.connect_web_timer = osTimer() \r
-        self.connect_web_timer_count = 0\r
-        self.hr_report_timer = osTimer()\r
-        self.resolver = None\r
-        self.timerHr = 0\r
-        self.timerLocation = 0\r
-        self.send_device_time = 0\r
-        self.vbat_buf = []\r
-        self.network_state = 0\r
-        self.curr_battery_level = 0\r
-        self.iot_web_send_queue_id = 0\r
-        self.bat_count = 0\r
-        self.minute_now_old = 0\r
-        self.old_minute = 0\r
-        self.is_alarm = 0\r
-        self.reconnect_count = 0 #连续重连次数 默认十次 不过就不重连了 等下一个心跳重连\r
-        self.schedule = _schedule.schedule()\r
-        self._1S_timer = osTimer()\r
-        self._5S_timer = osTimer()\r
-        self.timer_id = 0\r
-        self.start_voice_flag = 0\r
-        self.init_send_default=0\r
-        self.play_new_voice_state = 0\r
-        self.stop_play = 0\r
-        self.get_voice_new_msginit = 0\r
-        self.voice_id = "0"\r
-        self.bt_connect_list = []\r
-        self.new_voice_list = []\r
-        self.location_info= {}\r
-        self.locate_duration = 0\r
-        self.locate_duration_state = 0\r
-        self.location_info = EventMesh.publish("get_storage_config","locate_duration_time") #获取定位频率信息\r
-\r
-        self.temp = 0\r
-        if self.location_info == None:\r
-            self.location_info = {}\r
-        else:\r
-            # location_time["duration"]=locate_duration_time\r
-            # location_time["state"]=locate_duration_state\r
-            if "duration" in self.location_info:\r
-                self.locate_duration = self.location_info["duration"] #定位频率\r
-            if "state" in self.location_info:\r
-                self.locate_duration_state = self.location_info["state"] #是否开启频率定位\r
-            pass\r
-        EventMesh.subscribe("REPORT_ALARM_TO_IOT",self.report_alarm_to_iot)\r
-        EventMesh.subscribe("REPORT_SCHEDULE_TO_IOT",self.report_schedule_to_iot)\r
-\r
-    def report_alarm_to_iot(self,event,msg):\r
-        EventMesh.publish(WEB_CMD_ENUM.POST_ALARM_S_CMD,msg)\r
-        pass\r
-    def report_schedule_to_iot(self,event,msg):\r
-        EventMesh.publish(WEB_CMD_ENUM.POST_SCHEDULE_S_CMD,msg)\r
-        pass\r
-    def usb_event_cb(self, state):\r
-        status = state\r
-    def post_processor_after_instantiation(self):\r
-        self.minute_now_old=0\r
-        EventMesh.subscribe("set_curr_battery_level",self.set_curr_battery_level)\r
-        EventMesh.subscribe("get_curr_battery_level",self.get_curr_battery_level)\r
-        EventMesh.subscribe("screen_get_battery",self.get_battery)\r
-        EventMesh.subscribe("CHANNEL_IOT_WEB_SEND",self.user_send)\r
-        EventMesh.subscribe("set_temp",self.set_temp)\r
-        EventMesh.subscribe("GET_IOT_PROTOCOL_CILENT",self.get_iot_protoclo_client)\r
-        EventMesh.subscribe("SEND_JINMOAUDIO_TO_IOT",self.send_jm_audio_to_iot)\r
-        EventMesh.subscribe("SEND_YUYINAUDIO_TO_IOT",self.send_yuyin_audio_to_iot)\r
-        EventMesh.subscribe("SEND_MEETAUDIO_TO_IOT",self.send_meet_audio_to_iot)\r
-        EventMesh.subscribe("SEND_TRANSAUDIO_TO_IOT",self.send_trans_audio_to_iot)\r
-        EventMesh.subscribe("open_websocket_timer",self.open_websocket_timer)\r
-        EventMesh.subscribe("close_websocket_timer",self.close_websocket_timer)\r
-        EventMesh.subscribe("connect_iot_websocket_cb",self.connect_iot_websocket_cb)\r
-        EventMesh.subscribe("set_play_new_voice_list",self.set_play_new_voice_list)\r
-        EventMesh.subscribe("get_play_new_voice_list",self.get_play_new_voice_list)\r
-\r
-        EventMesh.subscribe("send_meet_open_flag_to_iot",self.send_meet_open_flag_to_iot)#会议创建\r
-        EventMesh.subscribe("send_meet_end_flag_to_iot",self.send_meet_end_flag_to_iot)#会议结束\r
-\r
-        EventMesh.subscribe("send_trans_come_flag_to_iot",self.send_trans_come_flag_to_iot)#进入翻译\r
-        EventMesh.subscribe("send_trans_ok_flag_to_iot",self.send_trans_ok_flag_to_iot)#向APP发送进入翻译成功\r
-        EventMesh.subscribe("send_trans_end_flag_to_iot",self.send_trans_end_flag_to_iot)#退出翻译\r
-        # import EventMesh\r
-        # EventMesh.publish("play_new_voice_list")\r
-        EventMesh.subscribe("play_new_voice_list",self.play_new_voice_list) #播放语音未读信息\r
-        EventMesh.subscribe("update_play_new_voice_info",self.update_play_new_voice_info) #更新语音未读信息状态上报IOT\r
-        EventMesh.subscribe("send_bt_scan_list",self.send_bt_scan_list) #上报扫描列表\r
-        EventMesh.subscribe("save_bt_connect_list",self.save_bt_connect_list) #保存耳机连接状态列表\r
-        EventMesh.subscribe("send_bt_connect_list",self.send_bt_connect_list) #发送耳机连接状态列表\r
-        EventMesh.subscribe("set_play_new_voice_state",self.set_play_new_voice_state) #设置播放语音未读状态1正在播放语音未读消息\r
-        EventMesh.subscribe("get_play_new_voice_state",self.get_play_new_voice_state) #获取播放语音未读状态1正在播放语音未读消息\r
-        EventMesh.subscribe("set_stop_play",self.set_stop_play) #外部控制url音频播放列表暂停\r
-        EventMesh.subscribe("get_stop_play",self.get_stop_play) #获取外部控制url音频播放列表暂停情况\r
-        EventMesh.subscribe("send_bt_connect_state_to_iot",self.send_bt_connect_state_to_iot) #发耳机;链接状态->IOT\r
-        EventMesh.subscribe("send_unbind_dev_to_iot",self.send_unbind_dev_to_iot) #发送解绑指令到IOT平台\r
-        EventMesh.subscribe("set_location_info",self.set_location_info)  #设置定位信息\r
-        EventMesh.subscribe("post_device_state",self.post_device_state) #上报设备状态信息-》IOT\r
-        EventMesh.subscribe("send_power_init_data",self.send_power_init_data) #上电初始化上报数据集合\r
-        # count = 0\r
-        # while True:\r
-        #     count += 1\r
-        #     if count > 20 :\r
-        #         break\r
-        #     data = dataCall.getInfo(1, 0)\r
-        #     print("data call INFO {}".format(data))\r
-        #     if data[2][0]:\r
-        #         self.network_state = 1\r
-        #         break\r
-        #     print("web等待网络 {}...".format(count))\r
-\r
-        #     utime.sleep_ms(500)\r
-        self.timerHr = 0\r
-        self.reconnect_count = 0\r
-        self.voice_id = "0"\r
-        self.deviceReadState = ""\r
-        self.get_voice_new_msginit = 0\r
-        self.bt_connect_list = []\r
-        self.new_voice_list = []\r
-        self.connect_web_timer.start(2000,0,self.iot_web_connect_timerCb) #开机2s之后连接iot\r
-    def connect_iot_websocket(self):\r
-        if EventMesh.publish("get_voice_record_flag") == 1:\r
-            EventMesh.publish("stop_voice_record")\r
-        print("connect_iot_websocket !!!!")\r
-        self.iot_close_web_therad()\r
-        \r
-        if sim.getStatus() != 1 or self.start_voice_flag == 1:#网络正常\r
-            return\r
-\r
-        if self.reconnect_count >= WEB_CONNECT_MAX_COUNT: #重连次数大于10次不在进重连只有下发心跳才重新连接\r
-            return\r
-        token = EventMesh.publish("get_dev_token")\r
-\r
-        print("token:{}".format(token))\r
-        # token = "957119d6b67b89dc1bc533d2e0c058d559"\r
-        # token = "951b74498eabe241e8a8c644a4f07531ef"\r
-\r
-        try:\r
-            if token != None:\r
-                self.connect_web_timer_count = 0\r
-                url_t = "{}?bz-dt={}".format(constants.IOT_WS_URL, token)\r
-                try:\r
-                    self.iot_protocol_client=uwebsocket.Client.connect(url_t, debug=True)\r
-                    self.reconnect_count=0\r
-                except Exception as e:\r
-                    print("再次连接,iot连接报错是")\r
-                    print(e)\r
-                    self.reconnect_count+=1\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) #self.connect_iot_websocket()\r
-                    return \r
-\r
-                if self.iot_protocol_client != None:\r
-                    self.reconnect_count = 0\r
-                    _thread.stack_size(self.thread_buf_size+8*1024) #_thread.stack_size(16*1024)\r
-                    self.iot_web_send_queue_id = _thread.start_new_thread(self.websocket_send_queue_thread, ())\r
-                    _thread.stack_size(8*1024)\r
-                \r
-                    _thread.stack_size(self.thread_buf_size+8*1024)\r
-                    self.iot_web_protoclo_recv_id = _thread.start_new_thread(self.websocket_recv_queue_thread, ())\r
-                    _thread.stack_size(8*1024)\r
-                    \r
-                else:\r
-                    print("self.iot_protocol_client init!!!!!!!!!")\r
-                    self.reconnect_count+=1\r
-                    # EventMesh.publish("connect_iot_websocket_cb")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) #self.connect_iot_websocket()\r
-                    return \r
-\r
-                if self.iot_protocol_client != None:\r
-                    self.start_voice_flag = 1\r
-                else:\r
-                    self.start_voice_flag = 0\r
-                    \r
-                print("self.iot_protocol_client %d self.start_voice_flag:%d",self.iot_protocol_client,self.start_voice_flag)\r
-                if self.iot_protocol_client != None:\r
-                    if self.timer_id == 0:\r
-                        # self.hr_report_timer.start(DEFINE_TIME_COUNT*1000, 1, self.public_report_event)\r
-                        self.timer_id=1\r
-            else:\r
-                print("没有token!!!!!!!!!!")\r
-                self.connect_web_timer_count+=1\r
-                if self.connect_web_timer_count <= 10:\r
-                    self.connect_web_timer.start(5000,0,self.iot_web_connect_timerCb) #没有token 5s之后再重连\r
-                pass\r
-        except Exception as e:   \r
-            self.iot_protocol_client = None\r
-            print("connect_iot_websocket error")\r
-    \r
-    def post_device_state(self,event,msg): #上报设备状态\r
-        device_state=EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD)\r
-        print("device_state{}".format(device_state))\r
-        EventMesh.publish(WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD,device_state) #上报设备状态\r
-    def set_location_info(self,event,msg):\r
-        if "duration" in msg:\r
-            self.locate_duration=msg["duration"]\r
-        if "state" in msg:\r
-            self.locate_duration_state=msg["state"]\r
-\r
-\r
-    def save_bt_connect_list(self,event,msg):\r
-        print("msg:{}".format(msg))\r
-        # msg = {"event":"bt_connect_single","data":{"name":"yeiwyei","mac":"89:89:78:78:98:89","state":"connected"}}\r
-        try:\r
-            data = msg["data"]\r
-            if "None" in data: #开机没有连接过蓝牙耳机就为kong\r
-                return\r
-            if isinstance(data,dict) == True:\r
-                mac = data["mac"]\r
-                state = data["state"]\r
-                name = data["name"]\r
-                need_save = 0\r
-                for i,_alarm in enumerate(self.bt_connect_list):\r
-                    mac_tmp = _alarm["mac"]\r
-                    if mac_tmp == mac:\r
-                        need_save = 1\r
-                        self.bt_connect_list[i]["state"] = state\r
-                        self.bt_connect_list[i]["name"] = name\r
-                        break\r
-                    self.bt_connect_list[i]["state"] ="disconnected"\r
-\r
-                if need_save == 0: #列表里面没有才需要保存\r
-                    self.bt_connect_list.append(data)\r
-                print("self.bt_connect_list:{}".format(self.bt_connect_list))\r
-        except Exception as e:\r
-            print("json数据不完整:self.recv_bt_connect_record_list")\r
-            usys.print_exception(e)\r
-        pass\r
-    def send_bt_connect_list(self,event,msg):\r
-        \r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        bt_connect_record_list = {\r
-            "cmd":"bt_connect_record_list",\r
-            "ts":time_tmp,\r
-            "msg":"上报蓝牙连接列表",\r
-            "data":self.bt_connect_list\r
-                \r
-        }\r
-        print("bt_connect_record_list:{}".format(bt_connect_record_list))\r
-        EventMesh.publish(WEB_CMD_ENUM.BT_CONNECT_RECORD_LIST_S,bt_connect_record_list)\r
-        pass\r
-\r
-    def set_play_new_voice_list(self,event,msg):\r
-        self.new_voice_list = msg\r
-        pass\r
-\r
-    def get_play_new_voice_list(self,event,msg):\r
-        return self.new_voice_list\r
-\r
-    def set_play_new_voice_state(self,event,msg):\r
-        self.play_new_voice_state = msg\r
-        pass\r
-\r
-    def get_play_new_voice_state(self,event,msg):\r
-        return self.play_new_voice_state\r
-\r
-    def get_stop_play(self,event,msg):\r
-        return self.stop_play\r
-\r
-    def set_stop_play(self,event,msg):\r
-        self.stop_play = msg\r
-        pass\r
-\r
-    def _1S_timer_cb(self,*argc):\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self._1S_timer_cb_}) \r
-\r
-    def _1S_timer_cb_(self): \r
-        #self.iot_web_send_queue.put("ping") \r
-        get_VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")\r
-        if get_VC_satrt_flag == None:\r
-            get_VC_satrt_flag = 0\r
-        if get_VC_satrt_flag == 0:\r
-            self.play_new_voice_list_timer_cb()\r
-            self._1S_timer.stop()\r
-\r
-    def play_new_voice_list(self,event,msg):\r
-        if len(self.new_voice_list) != 0:\r
-            self._1S_timer.start(1000, 1, self._1S_timer_cb)\r
-\r
-    def play_new_voice_list_timer_cb(self):\r
-        print("play_new_voice_list:{}".format(len(self.new_voice_list)))\r
-        voice_list = self.new_voice_list\r
-        if len(self.new_voice_list) != 0:\r
-            \r
-            for i,data in enumerate(self.new_voice_list):\r
-                if EventMesh.publish("get_history_voice_flag") == 1:\r
-                    data = voice_list[len(self.new_voice_list)-1-i]\r
-                print("data:{}".format(data))\r
-                self.voice_index = i\r
-                self.voice_id = str(data["id"])\r
-                url = str(data["url"])\r
-                # url = "http://qipaiai-oss1.tos-cn-beijing.volces.com/qi_pai_ringtone/2025_08_11/742659759219544064test.mp3"\r
-                self.play_new_voice_state = 1\r
-                if self.stop_play == 1:\r
-                    print("结束播放")\r
-                    self.stop_play = 0\r
-                    break\r
-                if EventMesh.publish("get_storage_config","is_noread_voice") == 1:#还有未读的(开机)\r
-                    if EventMesh.publish("get_open_device") == 1:#刚开机\r
-                        EventMesh.publish("play_wait_voice",[url,1])\r
-                        EventMesh.publish("set_open_device",0)\r
-                if EventMesh.publish("get_history_voice_flag") == 1:#播放历史留言 or 播放当前留言\r
-                    if EventMesh.publish("get_wait_voice_flag") == 1:#等待留言\r
-                        EventMesh.publish("play_wait_voice",[url,1])\r
-                    else:\r
-                        time = str(data["createTimeStr"])\r
-                        EventMesh.publish("play_his_voice",[url,1,time])\r
-                    if len(EventMesh.publish("get_play_new_voice_list")) == 0:#列表为空才清零\r
-                        EventMesh.publish("set_history_voice_flag",0)#修改标志位\r
-                else:\r
-                    EventMesh.publish("play_new_voice",[url,1])\r
-                break\r
-            pass\r
-    def update_play_new_voice_info(self,event,msg): #更新播放语音留言信息\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        put_voice_msg_state = {\r
-            "cmd":"put_voice_msg_state",\r
-            "ts":time_tmp,\r
-            "msg":"修改语音消息阅读状态",\r
-            "data":self.voice_id\r
-        }\r
-        if len(self.new_voice_list) != 0:\r
-            self.new_voice_list.pop(self.voice_index)\r
-        EventMesh.publish(WEB_CMD_ENUM.PUT_VOICE_MSG_STATE_S,put_voice_msg_state)\r
-        pass\r
-    \r
-    def send_bt_scan_list(self,event,msg):\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        try:\r
-            # print("type:{}".format(msg))\r
-            data = msg["data"]\r
-            bt_scan_rest ={\r
-                "cmd":"bt_scan_rest",\r
-                "ts":time_tmp,\r
-                "msg":"上报扫描列表",\r
-                "data":data\r
-            }\r
-            EventMesh.publish(WEB_CMD_ENUM.BT_SCAN_REST_S_CMD,bt_scan_rest)\r
-        except Exception as e:\r
-            usys.print_exception(e)\r
-        pass\r
-    def send_unbind_dev_to_iot(self,event,msg):\r
-        self._5S_timer.start(5000,0,self._5S_timer_cb)\r
-    def _5S_timer_cb(self,*argc):\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.send_unbind_dev_to_iot_cb})\r
-        pass\r
-    def send_unbind_dev_to_iot_cb(self):\r
-        EventMesh.publish(WEB_CMD_ENUM.UNBIND_DEV_S)\r
-        pass\r
-    def send_bt_connect_state_to_iot(self,event,msg):\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        \r
-        try:\r
-            # {\r
-            #     "cmd":"post_bt_connect_rest",\r
-            #     "ts":1738283829,\r
-            #     "msg":"上报蓝牙连接结果",\r
-            #     "data":{\r
-            #         "mac":"mac地址",\r
-            #         "state":"connected/disconnected/connecting/failed"\r
-            #     }\r
-            # }\r
-            event = msg["event"]\r
-            mac = msg["data"]["mac"]\r
-            state = msg["data"]["state"]\r
-            json_data = \\r
-            {\r
-                \r
-                "cmd": event,\r
-                "ts":time_tmp,\r
-                "msg":"上报蓝牙连接结果",\r
-                "data":{\r
-                    "mac":mac,\r
-                    "state":state\r
-                }   \r
-            \r
-            }\r
-            EventMesh.publish(WEB_CMD_ENUM.POST_BT_CONNECT_REST_S,json_data)\r
-        except Exception as e:\r
-            usys.print_exception(e)\r
-        pass\r
-    \r
-    def send_meet_open_flag_to_iot(self,event,msg): #创建会议纪要上报到IOT平台\r
-        time_tmp = utime.mktime(date_tmp)\r
-        meet_open = {\r
-                "msg": "开始会议",\r
-                "data": None,\r
-                "cmd": "meet_begin",\r
-                "ts": time_tmp\r
-            }\r
-        send_data = protocol_send_total_data(meet_open)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_meet_end_flag_to_iot(self,event,msg): #结束会议纪要上报到IOT平台\r
-        time_tmp = utime.mktime(date_tmp)\r
-        meet_end = {\r
-                "msg": "结束会议",\r
-                "data": None,\r
-                "cmd": "meet_end",\r
-                "ts": time_tmp\r
-            }\r
-        send_data = protocol_send_total_data(meet_end)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_trans_come_flag_to_iot(self,event,msg): #进入翻译上报到IOT平台\r
-        time_tmp = utime.mktime(date_tmp)\r
-        trans_open = {\r
-            "msg": "进入翻译模式",\r
-            "data": None,\r
-            "cmd": "translate_device_enter",\r
-            "ts": time_tmp\r
-        }\r
-        send_data = protocol_send_total_data(trans_open)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_trans_ok_flag_to_iot(self,event,msg): #成功进入翻译发给APP\r
-        time_tmp = utime.mktime(date_tmp)\r
-        trans_ok = {\r
-            "msg": "进入成功",\r
-            "data": None,\r
-            "cmd": "translate_device_complete",\r
-            "ts": time_tmp\r
-        }\r
-        send_data = protocol_send_total_data(trans_ok)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-    \r
-    def send_trans_end_flag_to_iot(self,event,msg): #退出翻译上报到IOT平台\r
-        time_tmp = utime.mktime(date_tmp)\r
-        trans_open = {\r
-            "msg": "退出翻译",\r
-            "data": None,\r
-            "cmd": "translate_device_exit",\r
-            "ts": time_tmp\r
-        }\r
-        send_data = protocol_send_total_data(trans_open)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_jm_audio_to_iot(self,event,msg): #静默录音音频数据上报到IOT平台\r
-        send_data = b''\r
-        send_data = protocol_audio_ji_playload_data(msg[0],msg[1],msg[2])\r
-        # print("jm:{}".format(msg[2]))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_yuyin_audio_to_iot(self,event,msg): #上报语音留言音频数据到IOT平台\r
-        send_data = b''\r
-        send_data = protocol_yuyin_voice_playload_data(msg[0],msg[1],msg[2])\r
-        # print("yuyin:{}".format(msg[2]))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_meet_audio_to_iot(self,event,msg): #上报会议纪要音频数据到IOT平台\r
-        send_data = b''\r
-        send_data = protocol_meet_playload_data(msg[0],msg[1],msg[2])\r
-        # print("yuyin:{}".format(msg[2]))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-\r
-    def send_trans_audio_to_iot(self,event,msg): #上报AI翻译音频数据到IOT平台\r
-        send_data = b''\r
-        send_data = protocol_trans_playload_data(msg[0],msg[1],msg[2])\r
-        # print("yuyin:{}".format(msg[2]))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        pass\r
-    \r
-\r
-    #获取电池电量\r
-    def get_battery(self, event=None, msg=None):\r
-        """\r
-        获取电池事件,暂时没支持\r
-        :param event:\r
-        :param msg:\r
-        :return:\r
-        """\r
-\r
-        charge_state = '0'\r
-        usb_state = EventMesh.publish("GET_USB_STATE")\r
-        if usb_state == None:\r
-            usb_state = 0\r
-\r
-        if  usb_state == 1:\r
-            charge_state = '1'\r
-        else:\r
-            charge_state = '0'\r
-\r
-        vbat = EventMesh.publish("get_battery")\r
-        if vbat is None:\r
-            vbat = 100\r
-        # EventMesh.debug_log("vbat",vbat)\r
-        # if temp != None :\r
-        #     if temp <= 3505:\r
-        #         vbat = 0 \r
-        #     elif temp <= 3699:\r
-        #         vbat = 20 #"E:/vbat_20.jpg"\r
-        #     elif temp <= 3772:\r
-        #         vbat = 40 #"E:/vbat_40.jpg"\r
-        #     elif temp <= 3882:\r
-        #         vbat = 60 #"E:/vbat_60.jpg"\r
-        #     elif temp <= 4070:\r
-        #         vbat = 80 #"E:/vbat_80.jpg"\r
-        #     elif temp <= 4200:\r
-        #         vbat = 90 #\r
-        #     else:\r
-        #         vbat = 100 #"E:/vbat_100.jpg"\r
-        # else:\r
-        #     vbat = 100\r
-\r
-        result = [charge_state, vbat]\r
-\r
-        return result\r
-    def set_curr_battery_level(self, event=None, msg=None):\r
-        self.curr_battery_level = msg\r
-        \r
-    def get_curr_battery_level(self, event=None, msg=None):\r
-        return self.curr_battery_level\r
-    \r
-    def initialization(self, *args):\r
-        pass\r
-        #EventMesh.debug_log("ProtoCol:{}".format(self.TYPE + "_SEND"))\r
-    def connect_iot_websocket_cb(self,event,msg):\r
-        self.connect_iot_websocket()\r
-    def add_support_resolver(self, resolver):\r
-        self.resolver = resolver\r
-    def public_report_event(self, *args, **kwargs):\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.public_report_event_cb}) #self.connect_iot_websocket()\r
-    def send_power_init_data(self,event=None,msg=None):\r
-        self.public_report_event_cb()\r
-    #上报心跳 上报设备状态 检查闹钟 日程\r
-    def public_report_event_cb(self):\r
-        """电池平均值:与 vc_hw._BatteryCfg 共用 10 点滑动平均,避免重复 getVbatt。"""\r
-        bat_total_value = 0\r
-        try:\r
-            from vc_hw import _BatteryCfg\r
-            bcfg = _BatteryCfg.instance()\r
-            try:\r
-                usb_st = EventMesh.publish("GET_USB_STATE")\r
-            except Exception:\r
-                usb_st = 0\r
-            bcfg.push_vbatt_sample(usb_status=usb_st)\r
-            bat_total_value = bcfg.soc_now()\r
-        except Exception:\r
-            bat_total_value = 0\r
-        # EventMesh.publish("AI_schedule_open")#读取闹钟数组\r
-        # print("AI_schedule_open!!!")\r
-        if EventMesh.publish("get_sos_flag") != None:\r
-            if EventMesh.publish("get_sos_flag") == 1:\r
-                print("get_sos_flag!!!!!!")\r
-                EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
-                print("get_sos_flag1!!!!!!")\r
-\r
-        EventMesh.publish("AI_alarm_start") #检查是否有闹钟需要提醒\r
-        EventMesh.publish("AI_schedule_open")#检查日程是否需要提醒\r
-        # self.alarm_test()\r
-        EventMesh.publish("set_curr_battery_level",bat_total_value)\r
-        # EventMesh.publish("publish_AI_btnInfo",{"func":self.hr_report_publish_event,"data":0})  \r
-        self.timerHr+=DEFINE_TIME_COUNT\r
-        if self.timerHr >= DEFIEN_HR_REPORT_TIME:\r
-            self.timerHr= 0\r
-            self.reconnect_count = 0\r
-            self.connect_web_timer_count=0\r
-            EventMesh.publish("publish_AI_btnInfo",{"func":self.hr_report_publish_event,"data":0}) \r
-        # print("self.locate_duration_state:{} self.locate_duration:{}".format(self.locate_duration_state,self.locate_duration))\r
-        if self.locate_duration_state == 1:\r
-            if self.locate_duration == None:\r
-                self.timerLocation = 0\r
-            else:\r
-                if self.locate_duration != 0:\r
-                    self.timerLocation+=DEFINE_TIME_COUNT\r
-                    if self.timerLocation >= self.locate_duration:\r
-                        self.timerLocation= 0\r
-                        EventMesh.publish(constants.EVENT.START_LOCATION,WEB_CMD_ENUM.POST_LOCATION_S_CMD) #定位上报\r
-                else:\r
-                    self.timerLocation= 0\r
-        else:\r
-            self.timerLocation= 0\r
-\r
-       \r
-        if self.send_device_time < DEFIEN_LOCATION_REPORT_TIME : #上电初始化上报一次设备状态信息\r
-            self.send_device_time+=DEFINE_TIME_COUNT\r
-        else:\r
-            if self.get_voice_new_msginit == 0: \r
-                self.get_voice_new_msginit=1\r
-                EventMesh.publish("publish_AI_btnInfo",{"func":self.hr_report_publish_event,"data":0}) \r
-                device_state=EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD)\r
-                print("device_state{}".format(device_state))\r
-                EventMesh.publish(WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD,device_state) #上报设备状态\r
-                # if dev_type == "7PIN_1_5":#二代开机获取留言\r
-                #     EventMesh.publish("set_open_device",1)\r
-                #     EventMesh.publish("set_history_voice_flag",1)\r
-                #     EventMesh.publish("set_wait_voice_flag",1)\r
-                #     EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)#向IOT获取语音未读消息\r
-            \r
-        \r
-        # http://220.160.104.34:30800/ws-bus/test/voice-record/876226211472149?state=1,\r
-        # http://220.160.104.34:30800/ws-bus/test/860132066107046?testType=alarmList\r
-    def hr_report_publish_event(self,data):\r
-        \r
-        HR_DATA = EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.HR_S_CMD)\r
-        print("HR_DATA{}".format(HR_DATA))\r
-        EventMesh.publish(WEB_CMD_ENUM.HR_S_CMD,HR_DATA)\r
-        pass\r
-\r
-    def iot_web_connect_timerCb(self,*argc):\r
-        self.connect_iot_websocket()\r
-\r
-    \r
-    def open_websocket_timer(self,event,msg):\r
-        self.VC_pingping_timerout = 0\r
-        # self._1S_timer.start(500,1,self._1S_timer_cb)\r
-    \r
-    def close_websocket_timer(self,event,msg):\r
-        self._1S_timer.stop()\r
-\r
-    \r
-        \r
-    def websocket_send_queue_thread(self):\r
-        while True:\r
-            data = self.iot_web_send_queue.get()\r
-            \r
-            if data != None:\r
-                # EventMesh.debug_log("websocket:send data1 len:{}".format(len(data)))\r
-                try:\r
-                    # EventMesh.debug_log("websocket:send data1 len:{} {}".format(len(data),self.iot_protocol_client))    \r
-                    if self.iot_protocol_client != None:\r
-                        self.iot_protocol_client.send(data)\r
-                        self.reconnect_count = 0\r
-                    else:\r
-                        # EventMesh.publish("connect_iot_websocket_cb")\r
-                        EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})\r
-                    EventMesh.debug_log("websocket:send data2 len:{}".format(len(data),self.iot_protocol_client))    \r
-                except Exception as e:\r
-                    EventMesh.debug_log("websocket:send error") \r
-                    # EventMesh.publish("connect_iot_websocket_cb")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})\r
-                    break\r
-             \r
-\r
-    def websocket_recv_queue_thread(self):\r
-        while True:\r
-            # 死循环接收数据\r
-            try:\r
-                recv_data = self.iot_protocol_client.recv()\r
-                print("recv_data = {}".format(len(recv_data)))\r
-                if recv_data == "websocket close":\r
-                    # 服务器关闭连接或客户端关闭连接\r
-                    print("对端 服务器强制关闭 cli close")\r
-                    # EventMesh.publish("connect_iot_websocket_cb")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) \r
-                    break\r
-                elif recv_data == "":\r
-                    print("下发的空数据 不做处理")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) \r
-                    break\r
-                else:\r
-                    if len(recv_data) != 0:\r
-                        self.reconnect_count = 0\r
-                        self.resolver.handler(recv_data)\r
-                \r
-            except Exception as e:\r
-                print("cli break")\r
-                # EventMesh.publish("connect_iot_websocket_cb")\r
-                EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) \r
-                break\r
-        self.iot_web_protoclo_recv_id =0\r
-\r
-    def iot_web_send_queue_close(self):\r
-        if self.iot_web_send_queue_id != 0:\r
-            if self.iot_web_send_queue.empty()!=True:\r
-                self.iot_web_send_queue.clear()\r
-            \r
-            try:\r
-                _thread.stop_thread(self.iot_web_send_queue_id)\r
-            except Exception as e:\r
-                print("iot_web_send_queue_close !!!!!!!!")\r
-        print("iot_web_send_queue_close 1111!!!!!!!!")\r
-        self.iot_web_send_queue_id = 0\r
-    \r
-    def iot_web_protoclo_recv_close_therad(self):\r
-        if self.iot_web_protoclo_recv_id != 0:\r
-            try:\r
-                _thread.stop_thread(self.iot_web_protoclo_recv_id)\r
-            except Exception as e:\r
-                print("self.iot_web_protoclo_recv_id !!!!!!!!")\r
-        self.iot_web_protoclo_recv_id = 0\r
-        if self.iot_protocol_client != None:\r
-            try:\r
-                print("关闭websocke")\r
-                self.iot_protocol_client.close()\r
-            except Exception as e:   \r
-                print("关闭websocke失败")\r
-        self.iot_protocol_client = None\r
-        \r
-\r
-        \r
-    def iot_close_web_therad(self):\r
-        try:\r
-            self.iot_web_send_queue_close()\r
-        except Exception as e:\r
-            print("iot_web_send_queue_close error!!!!!!!!")\r
-\r
-        try:\r
-            self.iot_web_protoclo_recv_close_therad()\r
-        except Exception as e:\r
-            print("iot_web_protoclo_recv_close_therad error!!!!!!!!")\r
-        self.start_voice_flag = 0\r
-        print("iot_close_web_therad")\r
-    \r
-    def get_iot_protoclo_client(self,event,msg):\r
-        return self.iot_protocol_client\r
-    def set_temp(self,event,data):\r
-        if self.temp == 0:\r
-            self.temp = 1\r
-        else:\r
-            self.temp = 0\r
-    def user_send(self,event,data):\r
-        # EventMesh.debug_log("TCP:send data:{}".format(msg))\r
-        # EventMesh.debug_log("TCP:send data:{}".format(msg))\r
-        # sim_state = sim.getStatus() \r
-        if sim.getStatus() != 1:#网络不正常\r
-            print("无 sim卡")\r
-            return\r
-        # if self.temp == 1:\r
-        #     print("没信号")\r
-        else :\r
-            try:\r
-                if self.start_voice_flag == 0: #如果关闭了就需要重新再\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) \r
-                    return\r
-                if data != None:\r
-                    if len(data) != 0:\r
-                        self.iot_web_send_queue.put(data)\r
-                    # EventMesh.debug_log("self.socket_send_queue.put:success:{}".format(len(data)))\r
-            except Exception as e:   \r
-                EventMesh.debug_log("self.socket_send_queue.put:ERROR:")\r
-                usys.print_exception(e) \r
-                \r
-        return\r
-        sim_state = sim.getStatus() \r
-        if sim_state != 1 or self.network_state == 0: #没sim,则不发送 ,没联网\r
-            return\r
-        else :\r
-            try:\r
-                if self.iot_protocol_client != None:\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.iot_protocol_send_cb,"data":data})\r
-                else:\r
-                    print("reconnect1 {}".format(data))\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})\r
-\r
-            except Exception as e:   \r
-                print("senc data err reconnect{}".format(data))\r
-                EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})\r
-                \r
-    def iot_protocol_send_cb(self,data):\r
-        try:\r
-            if data != None:\r
-                print("websocket connect success:{} self.iot_protocol_client:{}".format(data,self.iot_protocol_client))\r
-                if self.iot_protocol_client != None:\r
-                    self.iot_protocol_client.send(data)\r
-                    self.reconnect_count = 0\r
-                    print("websocket send success")\r
-                else:\r
-                    print("reconnect2 {}".format(data))\r
-                    # EventMesh.publish("connect_iot_websocket_cb")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})\r
-        except Exception as e:\r
-            print("发送失败 重新连接")\r
-            self.reconnect_count+=1\r
-            # EventMesh.publish("connect_iot_websocket_cb")\r
-            EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})\r
-   \r
-\r
-\r
-\r
-class ProtoCol(Abstract):\r
-    TYPE = ""\r
-    HEADER = "#@{}@#;"\r
-    SEPARATOR = ";"\r
-    END = ";" + chr(0x01)\r
-    #PID = "SZKW"\r
-    #PID = "qAELPTKmFanR4RHe"\r
-    md5_key = ''\r
-    PID = ''\r
-    btstate = 0\r
-    blestate = 0\r
-\r
-    def __init__(self):\r
-        pass\r
-\r
-    def initialization(self,*args):\r
-        EventMesh.subscribe(self.TYPE+"_S", self.handler)\r
-        EventMesh.subscribe(self.TYPE+"_R", self.handler)\r
-        pass\r
-\r
-    def handler(self, event, msg):\r
-        print("handler {}".format(event))\r
-        if event.endswith("_S"):\r
-            self.send(event, msg)\r
-        else:\r
-            self.recv(event, msg)\r
-\r
-    def send(self, event, msg):\r
-       \r
-        pass\r
-\r
-    def recv(self, event, msg):\r
-        pass\r
-#上报蓝牙连接状态\r
-class IOTWeb_post_bt_connect_restProtoCol(ProtoCol):\r
-    """post_bt_connect_rest"""\r
-    TYPE = "post_bt_connect_rest"\r
-\r
-    def send(self, event, msg):\r
-        # print("send post_bt_connect_rest{}".format(msg))\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print("send post_bt_connect_rest{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event: 上报蓝牙耳机连接状态\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv post_bt_connect_rest{}".format(msg))\r
-#电子围栏触发SOS警告\r
-class IOTWeb_fence_sosProtoCol(ProtoCol):\r
-    """fence_sos"""\r
-    TYPE = "fence_sos"\r
-\r
-    def send(self, event, msg):\r
-        # print("send fence_sos{}".format(msg))\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        # 通过websocket发送到服务器端\r
-        fence_sos = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"fence_sos",\r
-                "rest":"success",\r
-                "msg":"收到紧急呼叫指令"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(fence_sos)\r
-        print("send fence_sos{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event: 电子围栏警告\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        EventMesh.publish(WEB_CMD_ENUM.FENCE_SOS_S)\r
-        sos_list = EventMesh.publish("get_storage_config","sos_list")\r
-        if sos_list != None:\r
-            if len(sos_list) > 0:\r
-                get_sos_flag = EventMesh.publish("get_sos_flag") \r
-                if get_sos_flag == 0:\r
-                    EventMesh.publish("set_sos_info_call")\r
-                pass\r
-        \r
-        print("recv fence_sos{}".format(msg))\r
-#上报心跳\r
-class IOTWeb_HRProtoCol(ProtoCol):\r
-    """post_heart_beat"""\r
-    TYPE = "post_heart_beat"\r
-    \r
-    def send(self, event, msg):\r
-        print("send hr{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event: 心跳成功\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"post_rest",\r
-        #     "ts":1738283829,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"post_heart_beat",\r
-        #         "rest":"bind/unbind",\r
-        #         "msg":"上报成功/格式错误"\r
-        #     }\r
-        # }\r
-        print("recv hr{}".format(msg))\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            rest = data["rest"]\r
-            if rest == "unbind": #解绑需要开启蓝牙广播\r
-                self.btstate = 0\r
-                if self.blestate == 0:\r
-                    EventMesh.publish("set_storage_config",["switch_ble_state","1"])\r
-                    EventMesh.publish("send_ble_switch_toble","1")\r
-\r
-                    EventMesh.publish("set_storage_config",["switch_bt_state","1"])\r
-                    EventMesh.publish("send_bt_switch_toble","1")\r
-                    self.blestate=1\r
-\r
-            elif rest == "bind": #绑定需要关闭蓝牙广播\r
-                self.blestate =0\r
-                if self.btstate == 0:\r
-                    EventMesh.publish("set_storage_config",["switch_ble_state","0"])\r
-                    EventMesh.publish("send_ble_switch_toble","0")\r
-                    self.btstate=1\r
-\r
-#定位周期\r
-class IOTWeb_locatedurationProtoCol(ProtoCol):\r
-    """locate_duration"""\r
-    TYPE = "locate_duration"\r
-\r
-    def send(self, event, msg):\r
-        print("send locate_duration{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        locatio = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":int(time_tmp),\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"locate_duration",\r
-                "rest":"success",\r
-                "msg":"收到定位周期指令"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(locatio)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:定位周期\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # locate_now = {\r
-        #     "cmd":"locate_duration",\r
-        #     "ts":1738283829,\r
-        #     "msg":"下发定位周期",\r
-        #     "data":300,0表示关\r
-        # }\r
-        # {"cmd":"locate_duration","msg":"下发定位周期","data":{"duration":300,"state":"1"},"ts":1756089226052,"targetDeviceId":"860132064633266"}\r
-        #location_time\r
-        location_time = {}\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            if "duration" in data:\r
-                locate_duration_time = int(data["duration"])\r
-                locate_duration_state = int(data["state"])\r
-                location_time["duration"]=locate_duration_time\r
-                location_time["state"]=locate_duration_state\r
-\r
-                EventMesh.publish("set_storage_config",["locate_duration_time",location_time])\r
-                EventMesh.publish("set_location_info",location_time)\r
-            locate_duration_c = EventMesh.publish("get_storage_config","locate_duration_time")\r
-            print("locate_duration_c:{}".format(locate_duration_c))\r
-        \r
-#上报SOS告警信息\r
-class IOTWeb_post_sosProtoCol(ProtoCol):\r
-    """post_sos"""\r
-    TYPE = "post_sos"\r
-\r
-    def send(self, event, msg):\r
-        print("send post_sos{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:msg\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # locate_now = {\r
-        #     "cmd":"locate_duration",\r
-        #     "ts":1738283829,\r
-        #     "msg":"下发定位周期",\r
-        #     "data":300,0表示关\r
-        # }\r
-        print("recv post_sos{}".format(msg))\r
-\r
-#上报设备状态    \r
-class IOTWeb_post_device_stateProtoCol(ProtoCol):\r
-    """post_device_state"""\r
-    TYPE = "post_device_state"\r
-\r
-    def send(self, event, msg):\r
-        print("send post_device_state{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:mpost_device_state\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # locate_now = {\r
-        #     "cmd":"locate_duration",\r
-        #     "ts":1738283829,\r
-        #     "msg":"下发定位周期",\r
-        #     "data":300,0表示关\r
-        # }\r
-        print("send post_device_state{}".format(msg))\r
-       \r
-#上报闹钟列表     \r
-class IOTWeb_post_alarmProtoCol(ProtoCol):\r
-    """post_alarm"""\r
-    TYPE = "post_alarm"\r
-\r
-    def send(self, event, msg):\r
-        print("send post_alarm{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print("send send_data{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:post_alarm\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv post_alarm{}".format(msg))\r
-       \r
-#查找设备\r
-class IOTWeb_FindDeviceProtoCol(ProtoCol):\r
-    """find_device"""\r
-    TYPE = "find_device"\r
-\r
-    def send(self, event, msg):\r
-        # print("send locafind_devicetion{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        find_device = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"find_device",\r
-                "rest":"success",\r
-                "msg":"收到查找设备指令"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(find_device)\r
-        print("send_data find_device{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:定位信息下发\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        alarm_TTS_str="我在这呢"\r
-        base_sentence = "{}".format(alarm_TTS_str)\r
-        repeat_sentence = (base_sentence + ",") * 3  # 重复5次并加逗号分隔\r
-        final_sentence = repeat_sentence.rstrip(",")\r
-        \r
-        EventMesh.publish("play_tts",[final_sentence,1])  #播放tts闹钟文本内容\r
-        print("recv find_device{}".format(msg))\r
-        EventMesh.publish(WEB_CMD_ENUM.FIND_DEVCE_S_CMD)\r
-\r
-#立即定位\r
-class IOTWeb_LocateNowProtoCol(ProtoCol):\r
-    """locate_now"""\r
-    TYPE = "locate_now"\r
-    def send(self, event, msg):\r
-        print("send locate_now{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        locate_now = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"locate_now",\r
-                "rest":"success",\r
-                "msg":"收到立即定位指令"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(locate_now)\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:立即定位\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        \r
-        EventMesh.publish(WEB_CMD_ENUM.LOCATION_NOW_S_CMD)\r
-        print("recv locate_now{}".format(msg))\r
-        EventMesh.publish(constants.EVENT.START_LOCATION,WEB_CMD_ENUM.POST_LOCATION_S_CMD) #开始定位 定位完之后上报定位数据\r
-\r
-\r
-#上报定位信息 \r
-class IOTWeb_post_locationProtoCol(ProtoCol):\r
-    """post_location"""\r
-    TYPE = "post_location"\r
-\r
-    def send(self, event, msg):\r
-        # 通过websocket发送到服务器端\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print("send_data post_location{}".format(msg))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:定位信息下发\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv post_location{}".format(msg))\r
-#闹钟列表\r
-class IOTWeb_alarm_listProtoCol(ProtoCol):\r
-    """alarm_list"""\r
-    TYPE = "alarm_list"\r
-\r
-    def send(self, event, msg):\r
-        print("send alarm_list {}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        rest = "error"\r
-        if msg == 1:\r
-            rest = "success"\r
-        alarm_list ={\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"alarm_list",\r
-                "rest":rest,\r
-                "msg":"闹钟配置成功."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(alarm_list)\r
-        print("send_data alarm_list {}".format(msg))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发闹钟配置\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # 定义要添加的新元素\r
-        # new_item = {'time': '11:30','index': 1}\r
-        # data = [{"index":1,"name":"hihihi1"},{"index":2,"name":"hihihi2"},{"index":0,"name":"hihihi0"}]\r
-        # len_t = len(data)\r
-        # j = 0\r
-        # for i in range(len_t):\r
-        #     if data[j]['index'] == 1:\r
-        #         removed_item = data.pop(j)\r
-        #         print(data)\r
-        #         j=0\r
-        #         continue\r
-        #     j+=1\r
-        #     len_t = len(data)\r
-        #     print(len_t)\r
-        # for i in range(len(data)):\r
-        #     data[i]["index"] = i\r
-   \r
-        # url = http://220.160.104.34:30800/ws-bus/test/voice-record/876226211472149?state=1\r
-            # url = http://220.160.104.34:30800/ws-bus/test/876226211472149?testType = alarmList\r
-\r
-        alarm_list=[]\r
-        ret = 1\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            if isinstance(data,list) == True:\r
-                for i in range(len(data)):\r
-                    data_list = data[i]\r
-                    data_list["index"] = i\r
-                    print("data_list:{}".format(data_list))\r
-                    alarm_list.append(data_list)\r
-                alarm_list.sort(key=lambda x: x['index'])\r
-                EventMesh.publish("set_storage_config",["alarm_list",alarm_list])\r
-                EventMesh.publish("set_alarm_info",alarm_list)\r
-                alarm_list = EventMesh.publish("get_storage_config","alarm_list")\r
-                print("alarm_list:{}".format(alarm_list))\r
-            else:\r
-                ret = 0\r
-            \r
-        EventMesh.publish(WEB_CMD_ENUM.ALARM_LIST_S_CMD,ret)\r
-\r
-\r
-#闹钟配置下发\r
-\r
-class IOTWeb_alarm_configProtoCol(ProtoCol):\r
-    """alarm_config"""\r
-    TYPE = "alarm_config"\r
-\r
-    def send(self, event, msg):\r
-        # print("send alarm_config{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        alarm_config ={\r
-            "cmd":"dispatch_rest",\r
-            "ts":int(time_tmp),\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"alarm_config",\r
-                "rest":"success",\r
-                "msg":"闹钟配置成功."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(alarm_config)\r
-        print("send_data alarm_config{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发闹钟配置\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv alarm_config{}".format(msg))\r
-        EventMesh.publish(WEB_CMD_ENUM.ALARM_CONFIG_S_CMD)\r
-\r
-#SOS联系人下发\r
-class IOTWeb_sos_listProtoCol(ProtoCol):\r
-    """sos_list"""\r
-    TYPE = "sos_list"\r
-\r
-    def send(self, event, msg):\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        sos_list ={\r
-            "cmd":"dispatch_rest",\r
-            "ts":int(time_tmp),\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"sos_list",\r
-                "rest":"success",\r
-                "msg":"收到SOS联系人列表."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(sos_list)\r
-        print("send_data sos_list{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发sos列表\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #{'msg': 'SOS', 'data': [{'name': '0', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678900', 'id': '1754031896067', 'deviceId': '876226211472149'}, \r
-        # {'name': '1', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678901', 'id': '1754031896067', 'deviceId': '876226211472149'},\r
-        #  {'name': '2', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678902', 'id': '1754031896067', 'deviceId': '876226211472149'},\r
-        #  {'name': '3', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678903', 'id': '1754031896067', 'deviceId': '876226211472149'},\r
-        #  {'name': '4', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678904', 'id': '1754031896067', 'deviceId': '876226211472149'}],\r
-        #  'ts': 1754031896067, 'cmd': 'sos_list'}\r
-        print("recv sos_list{}".format(msg))\r
-        sos_list = []\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            if isinstance(data,list) == True:\r
-                sos_list_len = len(data)\r
-                if sos_list_len >= 3:\r
-                    sos_list_len = 3\r
-                for i in range(sos_list_len):\r
-                    save_data = {}\r
-                    # EventMesh.publish("set_storage_config",["SOS_NUMBER{}".format(i+1),data[i+1]])\r
-                    # sos_list.append(str(data[i+1]))\r
-                    save_data["index"] = i\r
-                    save_data["name"] = data[i]["name"]\r
-                    save_data["phone"] = data[i]["phone"]\r
-                    sos_list.append(save_data)\r
-                    # EventMesh.publish("set_storage_config",["sos_list",sos_list])\r
-            sos_list.sort(key=lambda x: x['index'])\r
-            EventMesh.publish("set_storage_config",["sos_list",sos_list])\r
-            EventMesh.publish("set_sos_list_info",sos_list)\r
-            read_soslist = EventMesh.publish("get_storage_config","sos_list")\r
-            print(read_soslist)\r
-            EventMesh.publish(WEB_CMD_ENUM.SOS_LIST_S_CMD)\r
-#NLP指令处理\r
-def ai_cmd_handle(cmd):\r
-    # print("")\r
-    if "查询" in cmd:#当指令中有查询时,截取查询操作\r
-        EventMesh.publish("find_cmd",cmd)\r
-    elif "讲话" in cmd:#当指令中有讲话时,截取语速操作\r
-        voice_start_index = cmd.index("讲话") + len("讲话")#动态判断截取部分\r
-        find = cmd[voice_start_index:]#查询截取\r
-        if  "快" in find:\r
-            EventMesh.publish("rate_move",1)\r
-            volume = "变快"\r
-        elif "慢" in find:\r
-            EventMesh.publish("rate_move",0)\r
-            volume = "变慢"\r
-        tts_play_str = "语速已经" + volume\r
-        EventMesh.publish("play_tts",[tts_play_str,0])\r
-    elif "声音改为" in cmd:#当指令中有声音改为时,进入音色修改\r
-        voice_start_index = cmd.index("声音改为") + len("声音改为")#动态判断截取部分\r
-        find = cmd[voice_start_index:]#查询截取\r
-        EventMesh.publish("change_voice",find)\r
-    elif "结束对话" in cmd:#当指令中有结束对话时,结束当前对话\r
-        EventMesh.publish("Close_VC")\r
-    elif "音量" in cmd:#当指令中有音量改为时,音量改为具体数值\r
-        if "调节" in cmd :\r
-            voice_start_index = cmd.index("音量调节:") + len("音量调节:")#动态判断截取部分\r
-            find = cmd[voice_start_index:]#查询截取\r
-            print("find = {}".format(find))\r
-            volume = int(find)\r
-            volume += 100#设置音量以100-200为参数\r
-            result = EventMesh.publish("change_volume",volume)\r
-            if result == 1:\r
-                tts_play_str = "音量已经改为" + find\r
-                EventMesh.publish("play_tts",[tts_play_str,0])\r
-            else:\r
-                tts_play_str = "设置失败,请重新设置0~100的音量大小"\r
-                EventMesh.publish("play_tts",[tts_play_str,0])\r
-        if "加大" in cmd:\r
-            result = EventMesh.publish("change_volume",1)\r
-            if result == 1:\r
-                tts_play_str = "音量已经加大"\r
-                EventMesh.publish("play_tts",[tts_play_str,0])\r
-            else:\r
-                tts_play_str = "当前音量已经是最大了"\r
-                EventMesh.publish("play_tts",[tts_play_str,0])\r
-        elif "减小" in cmd:\r
-            result = EventMesh.publish("change_volume",0)\r
-            if result == 1:\r
-                tts_play_str = "音量已经减小"\r
-                EventMesh.publish("play_tts",[tts_play_str,0])\r
-            else:\r
-                tts_play_str = "当前音量已经是最小了"\r
-                EventMesh.publish("play_tts",[tts_play_str,0])\r
-            \r
-    elif "语速倍速设置为:" in cmd:#修改对话模式\r
-        voice_start_index = cmd.index("语速倍速设置为:") + len("语速倍速设置为:")#动态判断截取部分\r
-        find = cmd[voice_start_index:]#查询截取\r
-        print("find = {}".format(find))\r
-        if float(find) > 2.0 or float(find) < 0.5:\r
-            tts_play_str = "抱歉,语速仅支持0.5-2倍,请重新设置"\r
-            EventMesh.publish("play_tts",[tts_play_str,0])\r
-            return\r
-        EventMesh.publish("SET_SPEECH_RATE",float(find))\r
-        tts_play_str = "语速已经改为" + find\r
-        EventMesh.publish("play_tts",[tts_play_str,0])\r
-    elif "聆听" in cmd:#聆听快/慢一些\r
-        if "快" in cmd:\r
-            tts_play_str = EventMesh.publish("NLP_set_vad_silence",1)\r
-        elif "慢" in cmd:\r
-            tts_play_str = EventMesh.publish("NLP_set_vad_silence",0)\r
-        EventMesh.publish("play_tts",[tts_play_str,0])\r
-    elif "这个不支持哦,请换一个" in cmd:\r
-        tts_play_str = "抱歉呀,暂不支持该方言,请换一个"\r
-        EventMesh.publish("play_tts",[tts_play_str,0])\r
-    elif "不支持的操作" in cmd:\r
-        tts_play_str = "抱歉呀,暂不支持该命令"\r
-        EventMesh.publish("play_tts",[tts_play_str,0])        \r
-\r
-    if not _NLP_EXTENDED_CMDS:\r
-        return\r
-    if "会议纪要" in cmd:\r
-        print("cmd:已进入会议")\r
-        EventMesh.publish("come_meet_mode")\r
-    elif "翻译" in cmd:\r
-        print("cmd:已进入翻译")\r
-        EventMesh.publish("come_trans_mode")\r
-    elif "查看历史留言" in cmd:\r
-        EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)\r
-        EventMesh.publish("set_history_voice_flag", 1)\r
-        EventMesh.publish("clean_unread_vocie")\r
-        EventMesh.publish("set_storage_config", ["is_noread_voice", 0])\r
-\r
-#NLP指令\r
-class IOTWeb_Nlp_ConfigProtoCol(ProtoCol):\r
-    """ai_cmd"""\r
-    TYPE = "ai_cmd"\r
-\r
-    def send(self, event, msg):\r
-        # print("ai_cmd{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        ai_cmd = {\r
-            "cmd":"ai_cmd",\r
-            "ts":time_tmp,\r
-            "msg":"AI指令",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(ai_cmd)\r
-        print("send_data ai_cmd{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发push_music\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #提取指令名称\r
-        cmdName = msg["data"]\r
-        print("提取到的AI指令为{}".format(cmdName))\r
-        ai_cmd_handle(cmdName)\r
-#设备进入翻译,小程序返回在线情况\r
-class IOTWeb_translate_device_enterProtoCol(ProtoCol):\r
-    """translate_device_enter"""\r
-    TYPE = "translate_device_enter"\r
-\r
-    def send(self, event, msg):\r
-        # print("translate_device_enter{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        print("设备进入翻译,小程序返回在线情况")\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发translate_device_enter\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("设备进入翻译获取状态:{}".format(msg["data"]["msg"]))\r
-        status = msg["data"]["rest"]\r
-        \r
-        if status == "success":\r
-           EventMesh.publish("set_trans_flag",1)#能进入AI翻译\r
-           exit_tts = "已进入实时翻译模式"\r
-           EventMesh.publish("play_tts",[exit_tts,1])\r
-        elif status == "error":\r
-           EventMesh.publish("set_trans_flag",0)#不能进入AI翻译\r
-           exit_tts = msg["data"]["msg"]\r
-           EventMesh.publish("play_tts",[exit_tts,0])\r
-        print("recv translate_device_enter")\r
-\r
-#接收小程序控制进入翻译\r
-class IOTWeb_come_transProtoCol(ProtoCol):\r
-    """translate_app_enter"""\r
-    TYPE = "translate_app_enter"\r
-\r
-    def send(self, event, msg):\r
-        # print("translate_app_enter{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        come_trans = {\r
-            "cmd":"translate_device_complete",\r
-            "ts":time_tmp,\r
-            "msg":"进入成功",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(come_trans)\r
-        print("send_data translate_app_enter{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发translate_app_enter\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        EventMesh.publish("come_trans_mode")#进入AI翻译\r
-        print("recv translate_app_enter{}".format(msg))\r
-\r
-#接收小程序控制退出翻译\r
-class IOTWeb_exit_transProtoCol(ProtoCol):\r
-    """translate_app_exit"""\r
-    TYPE = "translate_app_exit"\r
-\r
-    def send(self, event, msg):\r
-        # print("translate_app_enter{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        come_trans = {\r
-            "msg": "退出翻译",\r
-            "data": None,\r
-            "cmd": "translate_device_exit",\r
-            "ts": time_tmp\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(come_trans)\r
-        print("send_data translate_app_enter{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发translate_app_enter\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        EventMesh.publish("exit_trans_mode")#退出AI翻译\r
-        print("recv translate_app_exit{}".format(msg))\r
-\r
-#接收小程序控制进入会议\r
-class IOTWeb_appcontrol_meetProtoCol(ProtoCol):\r
-    """meet_control"""\r
-    TYPE = "meet_control"\r
-    def send(self, event, msg):\r
-        # print("translate_app_enter{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        come_trans = {\r
-            "msg": "开始会议",\r
-            "data": None,\r
-            "cmd": "meet_begin",\r
-            "ts": 1761121597\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(come_trans)\r
-        print("send_data translate_app_enter{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发meeting_app_enter\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        data = msg["data"]#判断是开启还是关闭\r
-        if data == "begin":\r
-            EventMesh.publish("come_meet_mode")#进入会议纪要\r
-        elif data == "end":\r
-            EventMesh.publish("exit_meet_mode")#退出会议纪要\r
-        print("APP Meeting Control")\r
-\r
-#翻译播放TTS\r
-class IOTWeb_play_trans_TTSProtoCol(ProtoCol):\r
-    """tts_audio_url"""\r
-    TYPE = "tts_audio_url"\r
-\r
-    def send(self, event, msg):\r
-        # print("tts_audio_url{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        tts_audio_url = {\r
-            "cmd":"tts_audio_url_complete",\r
-            "ts":time_tmp,\r
-            "msg":"返回状态",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(tts_audio_url)\r
-        print("send_data tts_audio_url{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发push_music\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #提取录音播放名称\r
-        voiceUrl = msg["data"]\r
-        print("提取到的翻译路径为{}".format(voiceUrl))\r
-        EventMesh.publish("play_trans_tts",voiceUrl)\r
-        print("recv tts_audio_url{}".format(msg))\r
-\r
-#音乐播放\r
-class IOTWeb_play_MusicProtoCol(ProtoCol):\r
-    """push_music"""\r
-    TYPE = "push_music"\r
-\r
-    def send(self, event, msg):\r
-        # print("push_music{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        push_music = {\r
-            "cmd":"push_music",\r
-            "ts":time_tmp,\r
-            "msg":"音乐播放",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(push_music)\r
-        print("send_data push_music{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发push_music\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #提取音乐播放名称,地址\r
-        print("recv push_music{}".format(msg))\r
-        musicName = msg["data"]["musicName"]\r
-        musicUrl = msg["data"]["musicUrl"]\r
-        print("提取到的音乐名称为{},路径为{}".format(musicName,musicUrl))\r
-        EventMesh.publish("play_music",[musicName,musicUrl,1])\r
-        \r
-\r
-#音量下发\r
-class IOTWeb_volumeProtoCol(ProtoCol):\r
-    """volume"""\r
-    TYPE = "volume"\r
-\r
-    def send(self, event, msg):\r
-        print("send volume{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        volume = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"volume",\r
-                "rest":"success",\r
-                "msg":"收到音量设置."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(volume)\r
-        print("send_data volume{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发volume\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        if "data" in msg:\r
-            r_volume = msg["data"]\r
-            print("recv volume{} type:{}".format(r_volume,type(msg)))\r
-            \r
-            EventMesh.publish("SET_TTS_VOLUME",int(r_volume))\r
-            EventMesh.publish("adjust_volume_toBle",r_volume)\r
-        EventMesh.publish(WEB_CMD_ENUM.VOLUME_S_CMD)\r
-#下发SOS配置\r
-class IOTWeb_sos_configProtoCol(ProtoCol):\r
-    """sos_config"""\r
-    TYPE = "sos_config"\r
-\r
-    def send(self, event, msg):\r
-        print("send sos_config{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        sos_config = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"sos_config",\r
-                "rest":"success",\r
-                "msg":"SOS配置成功."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(sos_config)\r
-        print("send_data sos_config{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发sos_config\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv sos_config{}".format(msg))\r
-        if "data" in msg:\r
-            sos_config = msg["data"]\r
-            print("recv sos_config{} type:{}".format(sos_config,type(msg)))\r
-            EventMesh.publish("set_storage_config",["sos_config",sos_config])\r
-            EventMesh.publish("set_sos_config",sos_config)\r
-        sos_config = EventMesh.publish("get_storage_config","sos_config")\r
-        # print("sos_config:{} type:{}".format(sos_config,sos_config["enable110"]))\r
-        EventMesh.publish(WEB_CMD_ENUM.SOS_CONFIG_S_CMD)\r
-#闹钟铃声获取\r
-\r
-class IOTWeb_get_ringtoneProtoCol(ProtoCol):\r
-    """get_ringtone"""\r
-    TYPE = "get_ringtone"\r
-\r
-    def send(self, event, msg):\r
-        print("send get_ringtone{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        get_ringtone ={\r
-            "cmd":"get_ringtone",\r
-            "ts":time_tmp,\r
-            "msg":"请求下发铃声列表",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(get_ringtone)\r
-        print("send_data get_ringtone{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发get_ringtone\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv get_ringtone{}".format(msg))\r
-\r
-#闹钟铃声列表\r
-class IOTWeb_ringtone_listProtoCol(ProtoCol):\r
-    """ringtone_list"""\r
-    TYPE = "ringtone_list"\r
-\r
-    def send(self, event, msg):\r
-        print("send ringtone_list{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        ringtone_list ={\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"ringtone_list",\r
-                "rest":"success",\r
-                "msg":"收到闹钟铃声列表."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(ringtone_list)\r
-        print("send_data ringtone_list{}".format(msg))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发ringtone_list\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv ringtone_list{}".format(msg))\r
-\r
-\r
-#下发静默录音\r
-class IOTWeb_voice_recordProtoCol(ProtoCol):\r
-    """voice_record"""\r
-    TYPE = "voice_record"\r
-    start_ai = 0\r
-    def send(self, event, msg):\r
-        print("send voice_record{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        s_rest = "0"\r
-        s_msg = "停止录音."\r
-        if msg == "1":\r
-            print("设备允许录音")\r
-            s_rest = "1"\r
-            s_msg = "开始录音."\r
-        \r
-        voice_record = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"voice_record",\r
-                "rest":s_rest,\r
-                "msg":s_msg\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(voice_record)\r
-        print("send_data voice_record{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发voice_record\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv voice_record{}".format(msg))\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            s_rest = "0"\r
-            if data == "0":\r
-                s_rest = "0"\r
-                \r
-                record_mode = EventMesh.publish("get_voice_record_mode")\r
-                if record_mode == None:\r
-                    record_mode = 0\r
-                if record_mode != 2:#语音留言不允许打断\r
-                    EventMesh.publish("stop_voice_record")\r
-                    print("停止录音")\r
-                EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)\r
-                \r
-            elif data == "1":\r
-                \r
-                get_sos_flag = EventMesh.publish("get_sos_flag") \r
-                if get_sos_flag != 0:\r
-                    s_rest = "0"\r
-                    print("正在电话")\r
-                    EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)\r
-                    return\r
-                record_mode = EventMesh.publish("get_voice_record_mode")\r
-                if record_mode == None:\r
-                    record_mode = 0\r
-                get_VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")\r
-                if get_VC_satrt_flag == None:\r
-                    get_VC_satrt_flag = 0\r
-                \r
-                if record_mode == 2 or get_VC_satrt_flag == 1:# 如果在AI对话 或者语音留言不允许静默录音\r
-                    s_rest = "0"\r
-                    EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)\r
-                    EventMesh.publish("stop_voice_record")\r
-                else:\r
-                    s_rest = "1"\r
-                    EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)\r
-                    EventMesh.publish("start_voice_record",1)\r
-                print("开始录音")\r
-        \r
-#设备解绑\r
-class IOTWeb_unbind_devProtoCol(ProtoCol):\r
-    """unbind_dev"""\r
-    TYPE = "unbind_dev"\r
-    unbind_devtimer = osTimer()\r
-    init_flag = 0\r
-    def send(self, event, msg):\r
-        print("send unbind_dev{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        unbind_dev={\r
-            "cmd":"unbind_dev",\r
-            "ts":time_tmp,\r
-            "msg":"主动解绑设备",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(unbind_dev)\r
-        print("unbind_dev{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-        self.init_flag=1\r
-        self.unbind_devtimer.start(2000,0,self.unbind_devtimer_cb)\r
-    def unbind_devtimer_cb(self,*argc):\r
-        if self.init_flag == 1:\r
-            play_str = "设备解绑失败"\r
-            EventMesh.publish("play_tts",[play_str,1])\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发unbind_dev\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"post_rest",\r
-        #     "ts":1738283829,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"unbind_dev",\r
-        #         "rest":"success|error",\r
-        #         "msg":"收到主动解绑设备指令"\r
-        #     }\r
-        # }\r
-        self.btstate = 0\r
-        self.blestate = 0\r
-        self.unbind_devtimer.stop()\r
-        self.init_flag=0\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            rest = data["rest"]\r
-            if rest == "success":\r
-                play_str = "设备解绑成功"\r
-                EventMesh.publish("set_storage_config",["switch_ble_state","1"])\r
-                EventMesh.publish("send_ble_switch_toble","1")\r
-\r
-  \r
-                EventMesh.publish("set_storage_config",["switch_bt_state","1"])\r
-                EventMesh.publish("send_bt_switch_toble","1")\r
-                EventMesh.publish("play_tts",[play_str,1])\r
-                # ret = 0\r
-                # ret = ql_fs.path_exists("/usr/static/storage_file")#配置文件\r
-                # if ret == True:\r
-                #     ql_fs.rmdirs("/usr/static/storage_file")\r
-                #     ql_fs.mkdirs("/usr/static/storage_file")\r
-                # utime.sleep_ms(2000)\r
-                # Power.powerRestart()\r
-                pass\r
-            else:\r
-                play_str = "设备解绑失败"\r
-                EventMesh.publish("play_tts",[play_str,1])\r
-                pass\r
-        print("recv unbind_dev{}".format(msg))        \r
-#用户解绑\r
-class IOTWeb_user_unbind_devProtoCol(ProtoCol):\r
-    """user_unbind_dev"""\r
-    TYPE = "user_unbind_dev"\r
-    def send(self, event, msg):\r
-        print("send user_unbind_dev{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        user_unbind_dev={\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"user_unbind_dev",\r
-                "rest":"success",\r
-                "msg":"收到用户解绑设备指令"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(user_unbind_dev)\r
-        print("user_unbind_dev{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发user_unbind_dev\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"post_rest",\r
-        #     "ts":1738283829,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"user_unbind_dev",\r
-        #         "rest":"success|error",\r
-        #         "msg":"收到主动解绑设备指令"\r
-        #     }\r
-        # }\r
-        self.btstate = 0\r
-        self.blestate = 0\r
-        EventMesh.publish("set_storage_config",["switch_ble_state","1"]) #用户解绑设备需要开启广播 开启经典蓝牙\r
-        EventMesh.publish("send_ble_switch_toble","1")\r
-\r
-        EventMesh.publish("set_storage_config",["switch_bt_state","1"])\r
-        EventMesh.publish("send_bt_switch_toble","1")\r
-        EventMesh.publish(WEB_CMD_ENUM.USER_UNBIND_DEV_S)\r
-        tts_play_str = "设备已解绑"\r
-        EventMesh.publish("play_tts",[tts_play_str,1])\r
-        # ret = 0\r
-        # ret = ql_fs.path_exists("/usr/static/storage_file")#配置文件\r
-        # if ret == True:\r
-        #     ql_fs.rmdirs("/usr/static/storage_file")\r
-        #     ql_fs.mkdirs("/usr/static/storage_file")\r
-        # utime.sleep_ms(100)\r
-        # Power.powerRestart()\r
-        print("recv user_unbind_dev{}".format(msg))    \r
-#下发位子留言\r
-class IOTWeb_text_msgProtoCol(ProtoCol):\r
-    """text_msg"""\r
-    TYPE = "text_msg"\r
-\r
-    def send(self, event, msg):\r
-        print("send text_msg{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-\r
-        rest = "error"\r
-        if msg == 1:\r
-            rest = "success"\r
-        text_msg = {\r
-            "cmd":"post_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"text_msg",\r
-                "rest":rest,\r
-                "msg":"收到文本消息."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(text_msg)\r
-        print("send_data text_msg{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发voice_record\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv time_tmp{}".format(msg))\r
-        recv_state = 0\r
-        if "data" in msg:\r
-            play_tts_text = msg["data"]\r
-            try:\r
-                EventMesh.publish("play_tts",[play_tts_text,1])\r
-                recv_state = 1\r
-            except Exception as e:\r
-                print("下发文字留言出错了")\r
-        EventMesh.publish(WEB_CMD_ENUM.TEXT_MSG_S_CMD,recv_state)\r
-        \r
-#AI对话音色配置\r
-class IOTWeb_chat_configProtoCol(ProtoCol):\r
-    """chat_config"""\r
-    TYPE = "chat_config"\r
-\r
-    def send(self, event, msg):\r
-        # print("send chat_config{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        chat_config = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"chat_config",\r
-                "rest":"success",\r
-                "msg":"收到AI对话配置"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(chat_config)\r
-        print("send_data chat_config{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发chat_config\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv chat_config{}".format(msg))\r
-        # {\r
-        #     "cmd":"chat_config",\r
-        #     "ts":1738283829,\r
-        #     "msg":"下发AI对话配置",\r
-        #     "data":{\r
-        #         "speed":1.5,\r
-        #         "vadDuration":600,\r
-        #         "voiceId":"音色ID,火山音色或者科大讯飞音色ID"\r
-        #     }\r
-        # }\r
-        # {'msg': '测试聊天配置.', 'data': {'botName': '柒乐乐', 'vadDuration': 600, 'speed': '1.0', 'provider': 'iflytek', 'voiceId': 'x3_linlin'}, \r
-        #  'ts': 1755600807553, 'cmd': 'chat_config'}\r
-\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            speed = None\r
-            try:\r
-                speed = float(data["speed"])\r
-            except Exception as e:\r
-                usys.print_exception(e)\r
-            vadDuration = data["vadDuration"]\r
-            voiceId = data["voiceId"]\r
-            provider = data["provider"]\r
-            botName = data["botName"]\r
-            asr =  data["asr"]\r
-            if speed !=  None:\r
-                    EventMesh.publish("SET_SPEECH_RATE",speed)\r
-            if vadDuration != None:\r
-                #if len(vadDuration) != 0:\r
-                    EventMesh.publish("set_vad_silence",vadDuration)\r
-            tts_type = {}\r
-            if voiceId != None and len(voiceId) != 0:\r
-                tts_type["voice"] = voiceId\r
-            if provider != None and len(provider) != 0:\r
-                tts_type["outbound_attributess_provider"] = provider\r
-            if botName != None and len(botName) != 0:\r
-                tts_type["bot_name"] = botName\r
-            if voiceId != None and len(voiceId) != 0:\r
-                tts_type["voice"] = voiceId\r
-            if asr != None and len(asr) != 0:\r
-                tts_type["inbound_attributess_provider"] = asr\r
-            if len(tts_type)>0:\r
-                print("tts_type:{}".format(tts_type))\r
-                EventMesh.publish("set_VC_TTS_voice_type",tts_type)\r
-        EventMesh.publish(WEB_CMD_ENUM.CHAT_CONFIG_S)\r
-#获取AI对话音色配置\r
-class IOTWeb_get_chat_configProtoCol(ProtoCol):\r
-    """get_chat_config"""\r
-    TYPE = "get_chat_config"\r
-\r
-    def send(self, event, msg):\r
-        # print("send chat_config{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        get_chat_config = {\r
-            "cmd":"get_chat_config",\r
-            "ts":time_tmp,\r
-            "msg":"获取AI对话配置",\r
-            "data":None\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(get_chat_config)\r
-        print("send_data chat_config{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发get_chat_config\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv get_chat_config{}".format(msg))\r
-\r
-#上报日程列表     \r
-class IOTWeb_post_scheduleProtoCol(ProtoCol):\r
-    """post_schedule"""\r
-    TYPE = "post_schedule"\r
-\r
-    def send(self, event, msg):\r
-        print("send post_schedule{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print("send send_data{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:post_schedule\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv post_schedule{}".format(msg))\r
-\r
-\r
-#下发日程    \r
-class IOTWeb_scheduleProtoCol(ProtoCol):\r
-    """schedule"""\r
-    TYPE = "schedule"\r
-\r
-    def send(self, event, msg):\r
-        print("send schedule{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        schedule = {\r
-            "cmd":"post_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"schedule",\r
-                "rest":"success",\r
-                "msg":"收到日程列表."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(schedule)\r
-        print("send_data schedule{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发schedule\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #         {\r
-        #     "cmd":"text_msg",\r
-        #     "ts":1738283829,\r
-        #     "msg":"下发文本消息",\r
-        #     "data":"文本内容"\r
-        # }\r
-        print("recv time_tmp{}".format(msg))\r
-        # {\r
-        #     "cmd":"schedule",\r
-        #     "ts":1738283829,\r
-        #     "msg":"下发日程列表",\r
-        #     "data":[\r
-        #         "title":"日程名称",\r
-        #         "description":"提醒内容",\r
-        #         "repeat":1~4播报重复次数,\r
-        #         "date":"日期,yyyy-MM-dd",\r
-        #         "时间":"HH:mm"\r
-        #     ]\r
-        # }\r
-        schedule = []\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            for i,sh in enumerate(data):\r
-                sh["index"] = i\r
-                schedule.append(sh)\r
-            schedule.sort(key=lambda x: x['index'])\r
-            EventMesh.publish("set_storage_config",["schedule",schedule])\r
-            \r
-            schedule = EventMesh.publish("get_storage_config","schedule")\r
-            EventMesh.publish("set_schedule_info",schedule)#\r
-            print(schedule)\r
-        EventMesh.publish(WEB_CMD_ENUM.SCHEDULE_S_CMD)\r
-#下发新的语音未读留言 \r
-class IOTWeb_new_voice_msgProtoCol(ProtoCol):\r
-    """new_voice_msg"""\r
-    TYPE = "new_voice_msg"\r
-\r
-    def send(self, event, msg):\r
-        print("send new_voice_msg{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        new_voice_msg = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"new_voice_msg",\r
-                "rest":"success",\r
-                "msg":"收到新语音消息指令."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(new_voice_msg)\r
-        print("send send_data{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-        EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:new_voice_msg\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        EventMesh.publish(WEB_CMD_ENUM.NEW_VOICE_MSG_S)\r
-\r
-        print("recv new_voice_msg{}".format(msg))\r
-\r
-#设备主动查询未读的语音消息列表\r
-class IOTWeb_find_new_voice_msgProtoCol(ProtoCol):\r
-    """find_voice_msg_list"""\r
-    TYPE = "find_voice_msg_list"\r
-    def __init__(self):\r
-        super().__init__()\r
-        self.history_board_flag = 0#第一次进去要广播\r
-        self.open_device = 0#开机播报未读留言\r
-        self.has_processed_history_voice = 0 #播完列表不许再拨“历史未读留言为空”\r
-        EventMesh.subscribe("set_history_board_flag",self.set_history_board_flag)\r
-        EventMesh.subscribe("set_open_device",self.set_open_device)\r
-        EventMesh.subscribe("get_open_device",self.get_open_device)\r
-    def send(self, event, msg):\r
-        print("send find_voice_msg_list")\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:find_voice_msg_list\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv find_voice_msg_list{}".format(msg))\r
-\r
-        new_voice_list = []#字典列表\r
-        voice_list = msg["data"]\r
-        print("msg[\"data\"] = {}".format(voice_list))\r
-        if EventMesh.publish("get_history_voice_flag") == 1:\r
-            if len(voice_list) == 0:#为空\r
-                if EventMesh.publish("get_wait_voice_flag") == 0:#未听历史留言\r
-                    tts_play_str = "已无更多历史留言"\r
-                    EventMesh.publish("play_tts",[tts_play_str,0])\r
-                    self.history_board_flag = 0\r
-                    self.has_processed_wait_voice = 0\r
-                    self.wait_first_flag = 0\r
-                    # if self.has_processed_history_voice == 0:\r
-                    #     tts_play_str = "已无更多历史留言"\r
-                    #     EventMesh.publish("play_tts",[tts_play_str,0])\r
-                    # else:\r
-                    #     self.has_processed_history_voice = 0#读完了置为0\r
-                    #     self.history_board_flag = 0#广播置为0\r
-                    # self.history_board_flag = 0\r
-                    # self.has_processed_wait_voice = 0\r
-                    # self.wait_first_flag = 0\r
-                else:#等待历史留言\r
-                    self.has_processed_wait_voice = 0\r
-                    self.wait_first_flag = 0\r
-                EventMesh.publish("set_recv_voice",0) #设置留言播放状态\r
-                EventMesh.publish("set_history_voice_flag",0)\r
-                EventMesh.publish("set_wait_voice_flag",0)\r
-            else:#有历史留言\r
-                if EventMesh.publish("get_wait_voice_flag") == 0:#历史留言\r
-                    print("历史广播状态!!!!! = {}".format(self.history_board_flag))\r
-                    if self.history_board_flag == 0:\r
-                        tts_play_str = "您共有" + str(len(voice_list)) + "条历史留言,正在为您播放"\r
-                        EventMesh.publish("play_tts",[tts_play_str,1])\r
-                        EventMesh.publish("set_pwk_notice_flag",1)\r
-                        EventMesh.publish("set_pwk_cnt_voice",len(voice_list))#设置多,防止进入留言播完判断\r
-                        self.history_board_flag = 1\r
-                        self.has_processed_history_voice = 1\r
-                    for da in voice_list:\r
-                        new_voice_list.append(da)\r
-                    if len(new_voice_list) > 0:\r
-                        pass\r
-                else:#未读留言\r
-                    if self.open_device == 1 or EventMesh.publish("get_60S_voice_flag") == 1:\r
-                        EventMesh.publish("set_pwk_cnt_voice",len(voice_list))#设置未读次数,由于是开机一起查看的\r
-                        EventMesh.publish("set_60S_voice_flag",0)\r
-                    for da in voice_list:\r
-                        new_voice_list.append(da)\r
-                    if len(new_voice_list) > 0:\r
-                        pass\r
-                EventMesh.publish("set_play_new_voice_list",new_voice_list)\r
-                EventMesh.publish("play_new_voice_list")\r
-        else:#正常留言\r
-            if EventMesh.publish("get_1M_flag") == 0:\r
-                if EventMesh.publish("get_pwk_cnt_voice") >= 0:\r
-                    for da in voice_list:\r
-                        new_voice_list.append(da)\r
-                    if len(new_voice_list) > 0:\r
-                        pass\r
-                    EventMesh.publish("set_play_new_voice_list",new_voice_list)\r
-                    EventMesh.publish("play_new_voice_list")\r
-    def set_history_board_flag(self,event,msg):\r
-        self.history_board_flag = msg             \r
-    def set_open_device(self,event,msg):\r
-        self.open_device = msg\r
-    def get_open_device(self,event,msg):\r
-        return self.open_device\r
-#请求未读的语音消息列表\r
-class IOTWeb_get_new_voice_msgProtoCol(ProtoCol):\r
-    """get_new_voice_msg"""\r
-    TYPE = "get_new_voice_msg"\r
-\r
-    def send(self, event, msg):\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        # {\r
-        #     "cmd":"get_new_voice_msg",\r
-        #     "ts":1738283829,\r
-        #     "msg":"请求未读的语音消息列表",\r
-        #     "data":null\r
-        # }\r
-        get_new_voice_msg = {\r
-            "cmd":"get_new_voice_msg",\r
-            "ts":time_tmp,\r
-            "msg":"请求未读的语音消息列表",\r
-            "data":None\r
-        }\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(get_new_voice_msg)\r
-        print("send get_new_voice_msg{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:get_new_voice_msg\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #  get_new_voice_msg = {\r
-        #     "cmd":"post_rest",\r
-        #     "ts":time_tmp,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"get_new_voice_msg",\r
-        #         "rest":"success",\r
-        #         "msg":"收到请求新语音列表."\r
-        #     }\r
-        # }\r
-        print("recv get_new_voice_msg{}".format(msg))\r
-# 1播放 0暂停 2上一首 3下一首\r
-class IOTWeb_player_controlProtoCol(ProtoCol):\r
-    """player_control"""\r
-    TYPE = "player_control"\r
-\r
-    def send(self, event, msg):\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        paly_str = "停止"\r
-        paly_data = "stop"\r
-        if msg == 1:\r
-            paly_str = "播放"\r
-            paly_data = "play"\r
-            pass\r
-        elif msg == 0:\r
-            paly_str = "停止"\r
-            paly_data = "stop"\r
-            pass\r
-        elif msg == 2:\r
-            paly_str = "上一首"\r
-            paly_data = "pre"\r
-            pass\r
-        elif msg == 3:\r
-            paly_str = "下一首"\r
-            paly_data = "next"\r
-            pass\r
-        player_control = {\r
-            "cmd":"player_control",\r
-            "ts":time_tmp,\r
-            "msg":paly_str,\r
-            "data":paly_data\r
-        }\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(player_control)\r
-        print("send player_control{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",msg)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:get_new_voice_msg\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"post_rest",\r
-        #     "ts":1738283829,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"player_control",\r
-        #         "rest":"success|error",\r
-        #         "msg":"收到播放/停止/暂停指令/下一首/上一首"\r
-        #     }\r
-        # }\r
-        paly_state = 0\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            msg_str = data["msg"]\r
-            if msg_str.find("播放") != -1:\r
-                paly_state=1\r
-                pass\r
-            elif msg_str.find("停止") != -1:\r
-                paly_state=0\r
-                pass\r
-            elif msg_str.find("下一首") != -1:\r
-                paly_state = 3\r
-                pass\r
-            elif msg_str.find("上一首") != -1:\r
-                paly_state = 2\r
-                pass\r
-\r
-        EventMesh.publish(WEB_CMD_ENUM.PLAY_CONTROL_S,paly_state)\r
-        print("recv send_data{}".format(msg))\r
-class IOTWeb_new_voice_msg_listProtoCol(ProtoCol):\r
-    """new_voice_msg_list"""\r
-    TYPE = "new_voice_msg_list"\r
-    def __init__(self):\r
-        super().__init__()\r
-        self._1M_flag = 0#判断一分钟定时器是否开启中\r
-        self._1M_nowplay_flag = 0#判断当前留言播放是否结束去影响新留言播放的标志位\r
-        self.app_new_voice_flag = 0#app是否下发留言\r
-        self._1M_timer = osTimer()#多发留言间隙时间60S\r
-        self._60S_voice_flag = 0\r
-        self.is_listen_wait = 0#是否收听未听留言\r
-        self.close_timer_cnt = 0 #若两次未听定时器关闭\r
-        EventMesh.subscribe("get_app_new_voice_flag",self.get_app_new_voice_flag)\r
-        EventMesh.subscribe("set_app_new_voice_flag",self.set_app_new_voice_flag)\r
-        EventMesh.subscribe("get_1M_flag",self.get_1M_flag)\r
-        EventMesh.subscribe("set_1M_nowplay_flag",self.set_1M_nowplay_flag)\r
-        EventMesh.subscribe("set_is_listen_wait",self.set_is_listen_wait)\r
-        EventMesh.subscribe("get_60S_voice_flag",self.get_60S_voice_flag)\r
-        EventMesh.subscribe("set_60S_voice_flag",self.set_60S_voice_flag)\r
-        \r
-    def send(self, event, msg):\r
-        print("send new_voice_msg_list{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        new_voice_msg_list = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"new_voice_msg_list",\r
-                "rest":"success",\r
-                "msg":"收到新语音列表."\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(new_voice_msg_list)\r
-        print("send send_data{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:new_voice_msg_list\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        self.app_new_voice_flag = 1#标志位为1\r
-        new_voice_list = []\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            for da in data:\r
-                new_voice_list.append(da)\r
-        if len(new_voice_list) > 0:\r
-            pass\r
-        EventMesh.publish("set_play_new_voice_list",new_voice_list)\r
-        print("是否收到留言:{},self._1M_flag = {}".format(EventMesh.publish("get_recv_voice"),self._1M_flag))\r
-        if EventMesh.publish("get_recv_voice") == 0 and self._1M_flag == 0 and self._1M_nowplay_flag == 0:#当没有音频在播放时,再播放新的,否则保存下来不播放,等60S后一起播放\r
-            EventMesh.publish("set_recv_voice",1) #设置留言播放状态\r
-            EventMesh.publish("play_new_voice_list")\r
-            self._1M_nowplay_flag == 1\r
-        else:#当有音频在播放时,存下来开始倒计时\r
-            pass\r
-            if self._1M_flag == 0 :\r
-                print("等待留言60S倒计时开启")\r
-                self._1M_timer.start(60000, 0, self._1M_timer_cb)\r
-                self._1M_flag = 1\r
-        EventMesh.publish(WEB_CMD_ENUM.NEW_VOICE_MSG_LIST_S)\r
-        print("recv new_voice_msg_list{}".format(msg))\r
-    def _1M_timer_cb(self,*argc):\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self._1M_timer_cb_}) \r
-\r
-    def _1M_timer_cb_(self): \r
-        #self.iot_web_send_queue.put("ping") \r
-        self._1M_flag = 0\r
-        self._60S_voice_flag = 1\r
-        get_VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")\r
-        if get_VC_satrt_flag == None:\r
-            get_VC_satrt_flag = 0\r
-        if get_VC_satrt_flag == 0:\r
-            print("收到多条信息,统一播放")\r
-            EventMesh.publish("set_history_voice_flag",1)#修改历史留言标志位\r
-            EventMesh.publish("set_wait_voice_flag",1)#修改60S标志位\r
-            EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)\r
-\r
-    def get_app_new_voice_flag(self,event,msg):\r
-        return self.app_new_voice_flag\r
-    def set_app_new_voice_flag(self,event,msg):\r
-        self.app_new_voice_flag = msg\r
-    def get_1M_flag(self,event,msg):\r
-        return self._1M_flag  \r
-    def get_60S_voice_flag(self,event,msg):\r
-        return self._60S_voice_flag\r
-    def set_60S_voice_flag(self,event,msg):\r
-        self._60S_voice_flag = msg\r
-    def set_1M_nowplay_flag(self,event,msg):\r
-        self._1M_nowplay_flag = msg\r
-    def set_is_listen_wait(self,event,msg):\r
-        self.is_listen_wait = msg\r
-class IOTWeb_put_voice_msg_stateProtoCol(ProtoCol):\r
-    """put_voice_msg_state"""\r
-    TYPE = "put_voice_msg_state"\r
-\r
-    def send(self, event, msg):\r
-        print("send put_voice_msg_state{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-       \r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print("send send_data{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:put_voice_msg_state\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv put_voice_msg_state{}".format(msg))\r
-#收到蓝牙连接记录获取请求\r
-class IOTWeb_get_bt_connect_record_listProtoCol(ProtoCol):\r
-    """get_bt_connect_record_list"""\r
-    TYPE = "get_bt_connect_record_list"\r
-\r
-    def send(self, event, msg):\r
-        print("send get_bt_connect_record_list{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        get_bt_connect_record_list = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"get_bt_connect_record_list",\r
-                "rest":"success",\r
-                "msg":"收到蓝牙连接记录获取请求"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(get_bt_connect_record_list)\r
-        print("send send_data{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:get_bt_connect_record_list\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"get_bt_connect_record_list",\r
-        #     "ts":1738283829,\r
-        #     "msg":"上报蓝牙连接列表",\r
-        #     "data": None\r
-        # }\r
-        EventMesh.publish("send_get_connect_bt_state")\r
-        EventMesh.publish(WEB_CMD_ENUM.GET_BT_CONNECT_RECORD_LIST_S)\r
-        print("recv get_bt_connect_record_list{}".format(msg))\r
-\r
-#蓝牙开关\r
-class IOTWeb_switch_bt_stateProtoCol(ProtoCol):\r
-    """switch_bt_state"""\r
-    TYPE = "switch_bt_state"\r
-\r
-    def send(self, event, msg):\r
-        # print("send switch_bt_state{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        #         {\r
-        #     "cmd":"dispatch_rest",\r
-        #     "ts":1738283829,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"switch_bt_state",\r
-        #         "rest":"success|error",\r
-        #         "msg":"收到蓝牙已打开/蓝牙已关闭指令"\r
-        #     }\r
-        # }\r
-        s_msg = "蓝牙已关闭指令"\r
-        if msg == "1":\r
-            s_msg = "收到蓝牙已打开"\r
-        elif msg == "0":\r
-            s_msg = "蓝牙已关闭指令"\r
-        switch_bt_state = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"switch_bt_state",\r
-                "rest":"success",\r
-                "msg":s_msg\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(switch_bt_state)\r
-        print("send_data switch_bt_state{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发switch_bt_state\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        s_state = "1"\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            if data == "0":\r
-                s_state = "0"\r
-                EventMesh.publish("set_storage_config",["switch_bt_state","0"])\r
-                EventMesh.publish("send_bt_switch_toble","0")\r
-                pass\r
-            elif data == "1":\r
-                s_state = "1"\r
-                EventMesh.publish("set_storage_config",["switch_bt_state","1"])\r
-                EventMesh.publish("send_bt_switch_toble","1")\r
-                pass\r
-        print("recv switch_bt_state{}".format(msg))\r
-        \r
-        EventMesh.publish(WEB_CMD_ENUM.SWITCH_BT_STATE_S_CMD,s_state)\r
-\r
-#蓝牙扫描\r
-class IOTWeb_bt_scanProtoCol(ProtoCol):\r
-    """bt_scan"""\r
-    TYPE = "bt_scan"\r
-\r
-    def send(self, event, msg):\r
-        print("send schedule{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        s_msg = ""\r
-        if msg == "1":\r
-            s_msg = "开始扫描附近设备"\r
-        elif msg == "0":\r
-            s_msg = "停止扫描附近设备"\r
-        bt_scan = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"bt_scan",\r
-                "rest":"success",\r
-                "msg":s_msg\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(bt_scan)\r
-        print("send_data bt_scan{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发bt_scan\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"bt_scan",\r
-        #     "ts":1738283829,\r
-        #     "msg":"开始扫描附近设备/停止扫描附近设备",\r
-        #     "data":"0/1"\r
-        # }\r
-        s_ret = "1"\r
-        if "data" in msg:\r
-            data = msg["data"]\r
-            if data == "1":\r
-                s_ret = "1"\r
-                EventMesh.publish("send_ble_scan","1")\r
-            elif data == "0":\r
-                s_ret = "0"\r
-                EventMesh.publish("send_ble_scan","0")\r
-                \r
-            pass\r
-            \r
-        print("recv bt_scan{}".format(msg))\r
-        EventMesh.publish(WEB_CMD_ENUM.BT_SCAN_S_CMD,s_ret)\r
-\r
-#蓝牙当前状态\r
-class IOTWeb_get_bt_stateProtoCol(ProtoCol):\r
-    """get_bt_state"""\r
-    TYPE = "get_bt_state"\r
-\r
-    def send(self, event, msg):\r
-        print("send schedule{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        bt_state ={\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"get_bt_state",\r
-                "rest":msg,\r
-                "msg":"上报蓝牙开关状态"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(bt_state)\r
-        print("send_data bt_state{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发bt_state\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv bt_state{}".format(msg))\r
-        switch_bt_state = EventMesh.publish("get_storage_config","switch_bt_state")\r
-        if switch_bt_state == None:\r
-            switch_bt_state = "1"\r
-        \r
-        EventMesh.publish(WEB_CMD_ENUM.GET_BT_STATE_S,str(switch_bt_state))\r
-\r
-#蓝牙扫描列表\r
-class IOTWeb_bt_scan_restProtoCol(ProtoCol):\r
-    """bt_scan_rest"""\r
-    TYPE = "bt_scan_rest"\r
-\r
-    def send(self, event, msg):\r
-        print("send bt_scan_rest{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print(" bt_scan_rest{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发bt_scan_rest\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        print("recv bt_scan_rest{}".format(msg))\r
-        \r
-\r
-#连接目标蓝牙设备\r
-class IOTWeb_connect_btProtoCol(ProtoCol):\r
-    """connect_bt"""\r
-    TYPE = "connect_bt"\r
-\r
-    def send(self, event, msg):\r
-        print("send connect_bt{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        s_ret = "success"\r
-        if msg == 0:\r
-            s_ret = "error"\r
-        connect_bt = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"connect_bt",\r
-                "rest":s_ret,\r
-                "msg":"收到蓝牙连接请求"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(connect_bt)\r
-        print("send_data bt_state{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发connect_bt\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"connect_bt",\r
-        #     "ts":1738283829,\r
-        #     "msg":"连接蓝牙设备",\r
-        #     "data":"目标设备mac地址"\r
-        # }\r
-        ok = 1\r
-        if "data" in msg:\r
-            mac = msg["data"]\r
-            # mac = "36:72:cc:e6:dd:e8"#deep \r
-            # mac = "61:E8:6D:CD:7F:17" #Q36\r
-            # mac = "75:E8:8C:62:9A:DF" Fineblue Clip\r
-            if len(mac) == 17:\r
-                ok = 1\r
-                EventMesh.publish("send_connect_bt_mac",mac)\r
-            else:\r
-                ok = 0\r
-        EventMesh.publish(WEB_CMD_ENUM.CONNECT_BT_S,ok)\r
-        print("recv connect_bt{}".format(msg))\r
-#上报连接记录\r
-class IOTWeb_bt_connect_record_listProtoCol(ProtoCol):\r
-    """bt_connect_record_list"""\r
-    TYPE = "bt_connect_record_list"\r
-\r
-    def send(self, event, msg):\r
-        print("send bt_connect_record_list{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(msg)\r
-        print("send_data bt_state{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发bt_connect_record_list\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        # {\r
-        #     "cmd":"post_rest",\r
-        #     "ts":1738283829,\r
-        #     "msg":"消息收到响应",\r
-        #     "data":{\r
-        #         "cmd":"bt_connect_record_list",\r
-        #         "rest":"success|error",\r
-        #         "msg":"收到蓝牙连接记录列表"\r
-        #     }\r
-        # }\r
-        print("recv bt_connect_record_list{}".format(msg))    \r
-#断开设备蓝牙连接\r
-class IOTWeb_disconnect_btProtoCol(ProtoCol):\r
-    """disconnect_bt"""\r
-    TYPE = "disconnect_bt"\r
-\r
-    def send(self, event, msg):\r
-        print("send disconnect_bt{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        disconnect_bt = {\r
-            "cmd":"dispatch_rest",\r
-            "ts":time_tmp,\r
-            "msg":"消息收到响应",\r
-            "data":{\r
-                "cmd":"disconnect_bt",\r
-                "rest":"success",\r
-                "msg":"断开蓝牙设备连接请求"\r
-            }\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(disconnect_bt)\r
-        print("send_data bt_state{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-\r
-        @param event:上行下发disconnect_bt\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        #{\r
-        # 'msg': '断开设备蓝牙', \r
-        # 'data': 'AB:12:34:56:78', \r
-        # 'ts': 1754555005787, \r
-        # 'cmd': 'disconnect_bt'}\r
-        if "data" in msg:\r
-            mac = msg["data"]\r
-            EventMesh.publish("send_disconnect_bt_mac",mac)\r
-        print("recv disconnect_bt{}".format(msg))\r
-        EventMesh.publish(WEB_CMD_ENUM.DISCONNECT_BT_S)\r
-class IOTWeb_find_deviceProtoCol(ProtoCol):\r
-    """find_device"""\r
-    TYPE = "find_device"\r
-\r
-    def send(self, event, msg):\r
-        # print("c{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        come_trans = {\r
-            "msg": "查找设备",\r
-            "data": None,\r
-            "cmd": "find_device",\r
-            "ts": time_tmp\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(come_trans)\r
-        print("send_data find_device{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:上行下发translate_app_enter\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        tts_str = "我在这呀,我在这呀,我在这呀 "\r
-        EventMesh.publish("play_tts",[tts_str,1])\r
-\r
-#接收小程序控制上传设备状态\r
-class IOTWeb_get_device_stateProtoCol(ProtoCol):\r
-    """get_device_state"""\r
-    TYPE = "get_device_state"\r
-\r
-    def send(self, event, msg):\r
-        battery_state = (EventMesh.publish("screen_get_battery"))[0]\r
-        battery = (EventMesh.publish("screen_get_battery"))[1]\r
-        if battery >= 100 or battery == None:\r
-            battery = 100\r
-        VC_TTS_volume =  EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置\r
-        if VC_TTS_volume == None:\r
-            VC_TTS_volume =100\r
-        Timeversion = version_util.get_unified_version()\r
-        ts = utime.mktime(utime.localtime())\r
-        come_trans = {\r
-            "data":{\r
-                    "version":Timeversion,#版本号\r
-                    "charge":battery_state,#0表示未充电,1表示充电中\r
-                    "currentRoleName":"",#当前音色的name\r
-                    "volume":str(VC_TTS_volume),#百分比数值\r
-                    "wifiRssi":str(net.csqQueryPoll()//6),#wifi强弱:0,1,2,3,4,5。转成这几个梯度\r
-                    "battery":str(battery),#电量\r
-                    "bargein": "0",#不能手动设置打断模式\r
-                    "deviceId":modem.getDevImei(),#设备号\r
-                },\r
-            "cmd":"device_info",\r
-            "msg": "上报设备信息",\r
-            "ts": ts\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(come_trans)\r
-        print("send_data get_device_state{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:get_device_state\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        EventMesh.publish(WEB_CMD_ENUM.GET_DEVICE_STATE_S)\r
-\r
-#接收小程序控制设备音量\r
-class IOTWeb_switch_volumeProtoCol(ProtoCol):\r
-    """switch_volume"""\r
-    TYPE = "switch_volume"\r
-\r
-    def send(self, event, msg):\r
-        # print("c{}".format(msg))\r
-        # 通过websocket发送到服务器端\r
-        date_tmp = utime.localtime()\r
-        time_tmp = utime.mktime(date_tmp)\r
-        come_trans = {\r
-            "msg": "设置音量",\r
-            "data": None,\r
-            "cmd": "switch_volume",\r
-            "ts": time_tmp\r
-        }\r
-        send_data = b''\r
-        send_data = protocol_send_total_data(come_trans)\r
-        print("send_data switch_volume{}".format(send_data))\r
-        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)\r
-\r
-    def recv(self, event, msg):\r
-        """\r
-        @param event:switch_volume\r
-        @param msg: 接收到的下行数据\r
-        """\r
-        volume = int(msg["data"]["target"]) + 100\r
-        print("音量修改为:{}".format(volume-100))\r
-        EventMesh.publish("change_volume",volume)\r
-\r
-\r
-class ProtoColResolver(object):\r
-    def __init__(self):\r
-        self.protocol_list = []\r
-\r
-    def add_support(self, prot):\r
-        self.protocol_list.append(prot)\r
-        prot.initialization()\r
-\r
-    def handler(self, data):\r
-        # 这里接收数据\r
-        \r
-        EventMesh.debug_log("handler -> {}".format(len(data)))\r
-        # if data.startswith('Init;'):\r
-        #     data = data[5:]\r
-        #     EventMesh.publish(constants.EVENT.SET_KEY, {"value": ujson.loads(data)['KEY']})\r
-            \r
-        # elif data.startswith("Update;"):\r
-        #     data = data[7:]\r
-        #     EventMesh.publish(constants.PROTOCOL.H04_RECV, data)\r
-        try:\r
-            if len(data) > 0: \r
-                playpolad_data(data)\r
-                # EventMesh.publish(ProtoCol_type, data) #返回协议read\r
-        except Exception as e:\r
-            EventMesh.debug_log("handler:ERROR:")\r
-            usys.print_exception(e)\r
-            EventMesh.debug_log("handler:error data:{}".format(len(data)))\r
+try:
+    from qp_log import qp_print as print
+except Exception:
+    pass
+import ujson
+import utime
+import EventMesh
+import constants
+from common import Abstract
+import uhashlib
+import ubinascii
+from misc import Power
+import audio
+from machine import Pin
+import ujson
+import ql_fs
+import dataCall
+import net
+import usocket
+import osTimer
+
+import machine
+import urandom
+import _thread
+import usys
+import request
+import umqtt 
+import modem
+SHA256 = uhashlib.sha256
+import versions
+import version_util
+try:
+    import vc_config as _vc_cfg
+    _NLP_EXTENDED_CMDS = _vc_cfg.get_bool("NLP_EXTENDED_CMDS", False)
+except Exception:
+    _NLP_EXTENDED_CMDS = False
+
+from queue import Queue
+def unicode_to_utf8(src):
+    ref = 0xe08080
+    result = ref
+    
+    #取最后六位
+    a = src & 0x3f
+    
+    #将最后六位放在ref最后六位的空位置
+    result = result | a
+    src = src >> 6
+    
+    #取接下来的六位
+    a = src & 0x3f
+    
+    #放在ref对应的六个位置
+    result = result | (a << 8)
+
+    #保留最后四位
+    src = src >> 6
+
+    #放在ref对应的四个位置    
+    result = result | (src << 16)
+    return result
+
+def UnicodeHexStr_To_Utf8Str(str):
+    odata = 0
+
+    result = bytes() 
+    su =str.upper()
+
+    i = 0
+    for c in su:
+        tmp=ord(c)
+        if tmp <= ord('9') :
+            odata = odata << 4
+            odata += tmp - ord('0')
+
+        elif ord('A') <= tmp <= ord('F'):
+            odata = odata << 4
+            odata += tmp - ord('A') + 10
+        if(i%4==3): #每2字节获取一个int,拿到转换后的unicode int
+            #print(odata.to_bytes(2, 'big'))
+            #result.append(odata)
+            if odata > 255:
+                tmp = unicode_to_utf8(odata)#拿到转换后的utf8 int
+                tmp2 = tmp.to_bytes(3, 'big') #转bytes
+            else :
+                tmp2 = odata.to_bytes(1, 'big') 
+            #print("tmp2:{}".format(tmp2))
+            result = result + tmp2
+            odata = 0 
+        i+=1
+    return result.decode('utf-8') #返回utf8的str
+
+def HMAC(k, m):
+    SHA1_BLOCK_SIZE = 64
+    KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k)))
+    KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)
+    KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)
+    inner_message = KEY_INNER + m
+    outer_message = KEY_OUTER + SHA256(inner_message).digest()
+    return SHA256(outer_message)
+
+
+def get_hmac_sha256(k, m):
+    data = HMAC(k, m).digest()
+    # print("HMAC(k, m).digest():", data)
+    # print("HMAC(k, m).digest():", ubinascii.hexlify(data))
+    hex_data = ubinascii.hexlify(data)
+    hex_data = hex_data.decode('ascii')
+    return hex_data
+
+######################IOT######################
+import sms
+import voiceCall
+import quecgnss
+import wifiScan
+from misc import Power,USB
+def wifiScan_cb(args):
+    print("wifi_data:{}".format(args))
+    EventMesh.publish(constants.EVENT.WIFIINFO, args)
+    wifiScan.control(0)
+#定位thered
+class IOTWebLocationManager(Abstract):
+
+    def __init__(self):
+        self.gps = None
+        #self.match = GPSMatch()
+        self.timer_read_GPS_info = osTimer()
+        self.timer_sos_report_info = osTimer()
+        self.wifi_data = []
+        self.cell_data = []
+        self.location_timer_count = 0
+        self.location_check_data_timer_count = 0
+        self.GGA_count = 0
+        self.RMC_A_count = 0
+        self.location_type = 0 #定位类型,0:正常定位,有gps数据或60秒关闭;1:测试模式,只有发CLOSE_LOCATION消息才关闭
+        self.location_flag = 0
+        self.start_location_flag = 0
+        self.location_thread_id  = 0
+        self.location_queue = Queue(100)
+        self.lpm_fd = None
+        self.start_agps = 0
+        self.sos_report_flag=0
+        quecgnss.gnssEnable(0)
+
+    def post_processor_after_instantiation(self):
+        EventMesh.subscribe(constants.EVENT.START_LOCATION, self.start_location)
+        EventMesh.subscribe(constants.EVENT.WIFIINFO, self.WIFI_INFO)
+        EventMesh.subscribe(constants.EVENT.WIFISCANOPEN,self.WIFI_scan_open)
+        EventMesh.subscribe(constants.EVENT.CLOSE_LOCATION,self.close_location)
+        EventMesh.subscribe("get_location_flag",self.get_location_flag)
+        #EventMesh.subscribe(constants.EVENT.GPSCLOSE, self.gps_close)
+    def get_location_flag(self, event, msg):
+        return self.location_flag
+    
+    def sos_report_info(self):
+        EventMesh.debug_log("sos_report_info:")
+        if self.sos_report_flag == 1:
+            return
+        self.soswifi_data = []
+        self.soscell_data = []
+        self.sos_report_flag = 1
+        self.timer_sos_report_info.start(3000, 1, self.sos_timer)
+    
+    def close_sos(self):
+        self.soswifi_data = []
+        self.soscell_data = []
+        self.sos_report_flag=0
+        self.timer_sos_report_info.stop()
+
+    
+    def sos_timer(self, *args, **kwargs):
+        if self.sos_report_flag == 1:
+            try :   
+                #检查wifi数据
+                if self.soswifi_data == [] or (self.soswifi_data != [] and self.soswifi_data[0] == 0):#and self.wifi_data[0] <= 0:
+                    EventMesh.publish(constants.EVENT.WIFISCANOPEN)
+                    
+                #检查基站数据
+                if self.soscell_data== []:   
+                    data_tmp = net.getCellInfo()
+                    if(len(data_tmp)>=2):
+                        for cell in data_tmp:
+                            if cell and cell[0][0] == 0 and len(cell[0]) > 1:
+                                self.soscell_data = cell
+                                break
+                if self.soscell_data != [] and self.soswifi_data != []:
+                    self.close_sos()
+                    self.Location_send([WEB_CMD_ENUM.SOS_REPORT_S_CMD,self.soswifi_data,self.soscell_data])
+
+            except Exception as e:
+                usys.print_exception(e)
+
+    def start_location(self, event, msg):
+        self.location_queue.put(msg)#测试类型入队
+        self.wifi_data = []
+        self.cell_data = []
+        print("start_location:{} self.location_flag:{}".format(msg,self.location_flag))
+        if( self.location_flag == 1):
+            return
+        self.location_flag = 1
+        _thread.stack_size((9+5)*1024)
+        self.location_thread_id  = _thread.start_new_thread(self.location_thread, ())
+        _thread.stack_size(8*1024)
+        # self.timer_read_GPS_info.start(1000, 1, self.location_timer)
+        # if msg == WEB_CMD_ENUM.SOS_REPORT_S_CMD:
+        #     self.sos_report_info()
+        #     pass
+        # elif msg == WEB_CMD_ENUM.GPS_REPORT_S_CMD:
+        #     self.gps_report_init()
+            # pass
+    def gps_report_init(self):
+
+        EventMesh.debug_log("start_location:")
+        if self.location_flag == 1:
+            return
+        self.wifi_data = []
+        self.cell_data = []
+        self.location_timer_count = 0
+        self.location_flag = 1
+        self.timer_read_GPS_info.start(3000, 1, self.location_timer)
+
+
+    def close_location(self,event,msg):
+        self.location_flag = 0
+        if  wifiScan.getState() == True:
+            wifiScan.control(0)
+        # self.timer_read_GPS_info.stop()
+        if self.location_queue.empty() == False:
+            self.location_queue.clear()
+        if self.location_thread_id != 0:
+            try:
+                _thread.stop_thread(self.location_thread_id)
+            except Exception as e:
+                pass
+        self.location_thread_id=0
+        print("close_location:!!!!!!!!!")
+
+        
+
+    def WIFI_INFO(self, event, msg):
+        self.wifi_data = msg
+
+    def WIFI_scan_open(self, event, msg):
+
+        if wifiScan.getState() == False :
+            self.wifi_data = []
+            wifiScan.control(1)
+            wifiScan.setCfgParam(6, 2, 5,  0)  #设置WiFi参数,10秒上层,两轮,最多6个,6秒底层,PS优先
+            wifiScan.setCallback(wifiScan_cb) #注册回调函数
+            wifiScan.asyncStart()
+            #EventMesh.debug_log("wifiScan.asyncStart")
+        
+    def location_timer(self, *args, **kwargs):
+        if self.location_flag == 1:
+            try :   
+                #检查wifi数据
+                if self.wifi_data == [] or (self.wifi_data != [] and self.wifi_data[0] == 0):#and self.wifi_data[0] <= 0:
+                    EventMesh.publish(constants.EVENT.WIFISCANOPEN)
+                    
+                #检查基站数据
+                if self.cell_data == []:   
+                    data_tmp = net.getCellInfo()
+                    if(len(data_tmp)>=2):
+                        for cell in data_tmp:
+                            if cell and cell[0][0] == 0 and len(cell[0]) > 1:
+                                self.cell_data = cell
+                                break
+                if self.cell_data != [] and self.wifi_data != []:
+                    self.close_location(0,0)
+                    self.Location_send([WEB_CMD_ENUM.POST_LOCATION_S_CMD,self.wifi_data,self.cell_data])
+
+            except Exception as e:
+                usys.print_exception(e)
+       
+    def Location_send(self, msg) :
+        EventMesh.debug_log("Location_send1")
+        if self.location_queue.empty() == True:
+            return
+        EventMesh.debug_log("Location_send2")
+        print("Location_send1")
+        while True:
+            if self.location_queue.empty() == False:
+                TYPE = self.location_queue.get()
+                # (5, [('70:2A:D7:63:BD:B4', -38), ('C0:94:AD:80:21:5F', -61), ('D8:38:0D:05:1F:E3', -65), ('08:9E:84:E7:A6:38', -73), ('B0:44:14:BC:13:B0', -73)])
+                wifi_tmp = []
+                wifi_num = msg[0][0]
+                print("wifi_num:{}".format(wifi_num))
+                if  wifi_num == 0:
+                    wifi_tmp = []
+                else: #([], [], [(0, 69483137, 1120, 1, 385, 30504, 100, -75, -8), (3, 111950593, 1120, 1, 58, 30504, 1650, -98, -19), (3, 219097617, 1120, 17, 76, 30504, 2452, -106, -23), (3, 102345712, 1120, 1, 58, 30504, 3740, -96, -18)])
+                    
+                    wifi_tmp_data = msg[0][1]
+                    print(wifi_tmp_data)
+                    for i in range(wifi_num):
+                        list_data = {}
+                        list_data["mac"] = str(wifi_tmp_data[i][0])
+                        list_data["rsi"] = str(wifi_tmp_data[i][1])
+                        # list_data = "{},{}".format(wifi_tmp_data[i][0],wifi_tmp_data[i][1])
+
+                        wifi_tmp.append(list_data)
+                    pass
+                # ([(flag, cid, mcc, mnc, lac, arfcn, bsic, rssi)], [(flag, cid, licd, mcc, mnc, lac, uarfcn, psc, rssi)], [(flag, cid, mcc, mnc, pci, tac, earfcn, rssi, rsrq),...])
+                date_tmp = utime.localtime()
+                time_tmp = utime.mktime(date_tmp)
+                celList = []
+                if msg[1] != []:
+                    for CELL in msg[1] :
+                        print("CELL:{}".format(CELL))
+                        CELL_data = {}
+                        CELL_data["mcc"] = str(CELL[2])
+                        CELL_data["mnc"] = str(CELL[3])
+                        CELL_data["lac"] = str(CELL[4])
+                        CELL_data["cid"] = str(CELL[1])
+                        CELL_data["rsi"] = str(CELL[7])
+
+
+                        # CELL_data += str(CELL[2]) +"," #MCC
+                        # CELL_data += str(CELL[3]) +"," #MNC
+                        # CELL_data += str(CELL[4]) +"," #LAC
+                        # CELL_data += str(CELL[7]) +"," #RSSI
+                        celList.append(CELL_data)
+                s_msg=""
+                s_cmd = ""
+                if TYPE == WEB_CMD_ENUM.POST_LOCATION_S_CMD:
+                    s_msg = "上报经纬度"
+                    s_cmd = "post_location"
+                elif TYPE == WEB_CMD_ENUM.POST_SOS_S_CMD:
+                    s_msg = "上报SOS求救"
+                    s_cmd = "post_sos"
+                send_data = {
+                    "cmd":s_cmd,
+                    "ts":int(time_tmp),
+                    "msg":s_msg,
+                    "data":{
+                        "gps": {
+                            "longitude": "0",
+                            "latitude": "0",
+                            "located": "0",
+                            "speed": "0"
+                        },
+                        "wifiList":wifi_tmp,
+                        "celList":celList
+                        
+                    }
+                }
+                print("TYPE{}".format(TYPE))
+                EventMesh.publish(TYPE,send_data)
+            else:
+                self.location_thread_id=0
+                EventMesh.publish(constants.EVENT.CLOSE_LOCATION)
+                break
+
+
+    def location_thread(self):
+        
+        self.wifi_data = []
+        print("location_thread self.location_flag {}".format(self.location_flag))
+        while self.location_flag == 1:
+            try :   
+
+                #检查wifi数据
+                if self.wifi_data == [] or (self.wifi_data != [] and self.wifi_data[0] == 0):#and self.wifi_data[0] <= 0:
+                    EventMesh.publish(constants.EVENT.WIFISCANOPEN)
+                    
+                #检查基站数据
+                if self.cell_data == []:   
+                    print("location_thread")
+                    data_tmp = net.getCellInfo()
+                    if(len(data_tmp)>=2):
+                        for cell in data_tmp:
+                            if cell and cell[0][0] == 0 and len(cell[0]) > 1:
+                                self.cell_data = cell
+                                break
+
+                if self.cell_data != [] and self.wifi_data != []:
+                    self.Location_send([self.wifi_data,self.cell_data])
+                    
+                    break
+                
+            except Exception as e:
+                usys.print_exception(e)
+                EventMesh.publish(constants.EVENT.CLOSE_LOCATION)#关闭定位 
+            utime.sleep_ms(1000)
+        wifiScan.control(0)
+
+                    
+
+import uwebsocket
+import sim
+# 设备主动上报后台返回cmd的字符串
+DEVICE_REPORT_RETURN_STRING = "post_rest" 
+# 服务器主动下发给设备的cmd的字符串
+IOT_AUTO_REPORT_RETURN_STRING = "dispatch_rest" 
+MAGIC_STR = len("FFFE")
+VERSION_STR =len("03")
+TYPE_STR = len("03")
+PLAYLOAD_STR = len("0000")
+PROTOCLO_LEN = MAGIC_STR+VERSION_STR+TYPE_STR+PLAYLOAD_STR+len('{')+len('}')+len("cmd:")+len("data:")+len("ts:")
+
+class WEB_CMD_ENUM:
+    HR_R_CMD = "post_heart_beat_R"
+    HR_S_CMD = "post_heart_beat_S"
+
+    POST_LOCATION_R_CMD = "post_location_R"
+    POST_LOCATION_S_CMD = "post_location_S"
+
+    POST_SOS_R_CMD = "post_sos_R"
+    POST_SOS_S_CMD = "post_sos_S"
+
+    POST_DEVICE_STATE_R_CMD = "post_device_state_R"
+    POST_DEVICE_STATE_S_CMD = "post_device_state_S"
+
+    POST_ALARM_R_CMD = "post_alarm_R"
+    POST_ALARM_S_CMD = "post_alarm_S"
+
+    POST_SCHEDULE_R_CMD = "post_schedule_R"
+    POST_SCHEDULE_S_CMD = "post_schedule_S"
+
+    FIND_DEVCE_R_CMD = "find_device_R"
+    FIND_DEVCE_S_CMD = "find_device_S"
+
+    LOCATION_NOW_R_CMD = "locate_now_R"
+    LOCATION_NOW_S_CMD = "locate_now_S"
+
+    LOCATION_DURATION_R_CMD = "locate_duration_R"
+    LOCATION_DURATION_S_CMD = "locate_duration_S"
+
+    ALARM_CONFIG_R_CMD = "alarm_config_R"
+    ALARM_CONFIG_S_CMD = "alarm_config_S"
+
+    ALARM_LIST_R_CMD = "alarm_list_R"
+    ALARM_LIST_S_CMD = "alarm_list_S"
+
+    SCHEDULE_LIST_R_CMD = "schedule_list_R"
+    SCHEDULE_LIST_S_CMD = "schedule_list_S"
+    
+    SOS_LIST_R_CMD ="sos_list_R"
+    SOS_LIST_S_CMD ="sos_list_S"
+
+    SOS_CONFIG_R_CMD = "sos_config_R"
+    SOS_CONFIG_S_CMD = "sos_config_S"
+
+    DEVICE_SHAKE_STATE_R_CMD = "device_shake_state_R"
+    DEVICE_SHAKE_STATE_S_CMD = "device_shake_state_S"
+
+    RING_TONE_URL_R_CMD = "ring_tone_url_R"
+    RING_TONE_URL_S_CMD = "ring_tone_url_S"
+
+    VOLUME_R_CMD = "volume_R"
+    VOLUME_S_CMD = "volume_S"
+
+    TEXT_MSG_R_CMD = "text_msg_R"
+    TEXT_MSG_S_CMD = "text_msg_S"
+
+    VOICE_RECORD_R_CMD = "voice_record_R"
+    VOICE_RECORD_S_CMD = "voice_record_S"
+
+    GET_RINGTONE_R_CMD = "get_ringtone_R"
+    GET_RINGTONE_S_CMD = "get_ringtone_S"
+
+    RINGTONE_LIST_R_CMD = "ringtone_list_R"
+    RINGTONE_LIST_S_CMD = "ringtone_list_S"
+
+    SCHEDULE_R_CMD = "schedule_R"
+    SCHEDULE_S_CMD = "schedule_S"
+    
+    SWITCH_BT_STATE_R_CMD="switch_bt_state_R"
+    SWITCH_BT_STATE_S_CMD="switch_bt_state_S"
+
+
+    BT_SCAN_R_CMD="bt_scan_R"
+    BT_SCAN_S_CMD="bt_scan_S"
+
+    BT_STATE_R_CMD="bt_state_R"
+    BT_STATE_S_CMD="bt_state_S"
+
+    BT_SCAN_REST_R_CMD = "bt_scan_rest_R"
+    BT_SCAN_REST_S_CMD = "bt_scan_rest_S"
+
+    CONNECT_BT_R="connect_bt_R"
+    CONNECT_BT_S="connect_bt_S"
+
+    DISCONNECT_BT_R="disconnect_bt_R"
+    DISCONNECT_BT_S="disconnect_bt_S"
+
+    NEW_VOICE_MSG_R = "new_voice_msg_R"
+    NEW_VOICE_MSG_S = "new_voice_msg_S"
+
+    GET_NEW_VOICE_MSG_R="get_new_voice_msg_R"
+    GET_NEW_VOICE_MSG_S="get_new_voice_msg_S"
+
+    NEW_VOICE_MSG_LIST_R="new_voice_msg_list_R"
+    NEW_VOICE_MSG_LIST_S="new_voice_msg_list_S"
+
+    PUT_VOICE_MSG_STATE_R="put_voice_msg_state_R"
+    PUT_VOICE_MSG_STATE_S="put_voice_msg_state_S"
+
+    GET_BT_STATE_R="get_bt_state_R"
+    GET_BT_STATE_S="get_bt_state_S"
+
+    GET_BT_CONNECT_RECORD_LIST_R = "get_bt_connect_record_list_R"
+    GET_BT_CONNECT_RECORD_LIST_S = "get_bt_connect_record_list_S"
+
+    BT_CONNECT_RECORD_LIST_R="bt_connect_record_list_R"
+    BT_CONNECT_RECORD_LIST_S="bt_connect_record_list_S"
+
+    FENCE_SOS_R="fence_sos_R"
+    FENCE_SOS_S="fence_sos_S"
+
+    PLAY_CONTROL_R="player_control_R"
+    PLAY_CONTROL_S="player_control_S"
+
+    GET_CHAT_CONFIG_R="get_chat_config_R"
+    GET_CHAT_CONFIG_S="get_chat_config_S"
+
+    CHAT_CONFIG_R = "chat_config_R"
+    CHAT_CONFIG_S = "chat_config_S"
+
+    POST_BT_CONNECT_REST_R = "post_bt_connect_rest_R"
+    POST_BT_CONNECT_REST_S = "post_bt_connect_rest_S"
+
+    UNBIND_DEV_R="unbind_dev_R"
+    UNBIND_DEV_S="unbind_dev_S"
+
+    USER_UNBIND_DEV_R="user_unbind_dev_R"
+    USER_UNBIND_DEV_S="user_unbind_dev_S"
+
+    MUSIC_PLAY_R="music_play_R"
+    MUSIC_PLAY_S="music_play_S"
+
+    NLP_PLAY_R = "ai_cmd_R"
+    NLP_PLAY_S = "ai_cmd_S"
+
+    #金三
+    GET_DEVICE_STATE_R = "get_device_state_R"
+    GET_DEVICE_STATE_S = "get_device_state_S"
+class IOTSendCmdManager(Abstract):
+    #
+    def __init__(self):
+        self.send_data = ""
+        self.time_tmp = 0
+    def post_processor_after_instantiation(self):
+        EventMesh.subscribe("GET_IOT_WEB_SEND_CMD", self.get_iot_send_cmd)
+        print("IOTSendCmdManager")
+
+    def get_iot_send_cmd(self, event, msg):
+        date_tmp = utime.localtime()
+        self.time_tmp = utime.mktime(date_tmp)
+        if msg == WEB_CMD_ENUM.HR_S_CMD:
+            self.send_data = self.hr_send_data(self.time_tmp)
+            return self.send_data
+        elif msg == WEB_CMD_ENUM.POST_LOCATION_S_CMD:
+            self.send_data = self.gps_send_data()
+            return self.send_data
+        elif msg == WEB_CMD_ENUM.POST_SOS_S_CMD:
+            self.send_data = self.sos_wraing_send_data()
+            return self.send_data
+        elif msg == WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD:
+           self.send_data = self.device_report_state_send_data()
+           return self.send_data
+        elif msg == WEB_CMD_ENUM.POST_ALARM_S_CMD:
+            self.send_data = self.alarm_config_send_data()
+        elif msg == WEB_CMD_ENUM.POST_SCHEDULE_S_CMD:
+            self.send_data = self.schedule_config_send_data()
+        return self.send_data
+    
+    # 心率上报数据格式
+    def hr_send_data(self,time):
+        print("hr_send_data")
+        battery = (EventMesh.publish("screen_get_battery"))[1]
+        if battery >= 100 or battery == None:
+            battery = 100
+        hr_send_data = {
+            "cmd":"post_heart_beat",
+            "ts":time,
+            "msg":"心跳",
+            "data":{
+                "battery": int(battery)
+            }
+        }
+        return hr_send_data
+    
+    # gps上报数据格式
+    def gps_send_data(self):
+        gps_data = {
+            "cmd":"post_location",
+            "ts":self.time_tmp,
+            "data":{
+                "gps":{
+                    "longitude":"W,67",
+                    "latitude":"S,465",
+                    "direction":"FR",
+                    "speed":"45",
+                    "altude":"56",
+                    "count":"3"
+                },
+                "wifiList":[
+                    "89:78:78:67:89:98,-98"
+                ],
+                "celList":[
+                    "MCC,MNC,LAC,CID,RSSI"
+                ],
+                "btList":[
+                    "D4:78:65:67:89:78"
+                ]
+            }
+        }
+        return gps_data
+    
+    # sos警告上报数据格式
+    def sos_wraing_send_data(self):
+        sos_wraing_data ={
+                "cmd":"post_sos",
+                "ts":self.time_tmp,
+                "data":{
+                    "gps":{
+                        "longitude":"W,67",
+                        "latitude":"S,465",
+                        "direction":"FR",
+                        "speed":"45",
+                        "altude":"56",
+                        "count":"3"
+                    },
+                    "wifiList":[
+                        "89:78:78:67:89:98,-98"
+                    ],
+                    "celList":[
+                        "MCC,MNC,LAC,CID,RSSI"
+                    ],
+                    "btList":[
+                        "D4:78:65:67:89:78"
+                    ]
+                }
+            }
+        return sos_wraing_data
+    
+    #设备上报状态上报数据格式
+    def device_report_state_send_data(self):
+        get_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")
+        device_report_data ={
+            "cmd":"post_device_state",
+            "ts":self.time_tmp,
+            "data":{
+                "mediaTitle":"haha",
+                "mediaState":"stop",
+                "cover":"899",
+                "volume":get_volume
+            }
+        }
+        # HR_DATA=EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD)
+        # print("HR_DATA{}".format(HR_DATA))
+        #  EventMesh.publish(WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD,HR_DATA)
+        return device_report_data
+
+    #闹钟配置上报数据格式
+    def alarm_config_send_data(self):
+        alarm_data = {
+            "cmd":"post_alarm",
+            "ts":self.time_tmp,
+            "msg":"上报闹钟",
+            "data":[
+                {
+                    "name":"喝水",
+                    "loop":"0101001",
+                    "state":"0",
+                    "time":"14:34",
+                    "ringTimes":1,
+                    "alarmMode":"ring",
+                    "shakeType":"short"
+                }, 
+                {
+                    "name":"起床",
+                    "loop":"0101001",
+                    "state":"0",
+                    "time":"07:40",
+                    "ringTimes":1,
+                    "alarmMode":"ring",
+                    "shakeType":"short"
+                }
+            ]
+        }
+            
+        return alarm_data
+    #日程配置上报数据格式
+    def schedule_config_send_data(self):
+        schedule_data = {
+            "cmd":"post_schedule",
+            "ts":self.time_tmp,
+            "msg":"上报日程",
+            "data":[
+                {
+                    "name":"喝水",
+                    "loop":"0101001",
+                    "state":"0",
+                    "time":"14:34",
+                    "ringTimes":1,
+                    "alarmMode":"ring",
+                    "shakeType":"short"
+                }, 
+                {
+                    "name":"起床",
+                    "loop":"0101001",
+                    "state":"0",
+                    "time":"07:40",
+                    "ringTimes":1,
+                    "alarmMode":"ring",
+                    "shakeType":"short"
+                }
+            ]
+        }
+            
+        return schedule_data
+#解析收到的数据协议
+def playpolad_data(data):
+    if(isinstance(data, bytes) == 0 or len(data) <= PROTOCLO_LEN):
+        return
+
+    if data[0] != 0xff and data[1] !=0xfe and data[2] != 0x03 and data[3] !=0x03:
+        return
+    
+    # print(type(data))
+    json_bytes = data[8:]
+    # print(data[0],data[1],data[2],data[3],data[4],data[5],data[6],data[7])
+    # 2. 将二进制JSON转换为字符串(使用UTF-8解码)
+    json_str = json_bytes.decode('utf-8')
+    print("解析出的JSON字符串:")
+    print(json_str)
+    print("\n" + "-"*50 + "\n")
+
+    # 3. 将JSON字符串转换为Python字典
+    try:
+        data_dict = ujson.loads(json_str)
+        print("解析后的字典数据:")
+        print(data_dict)
+        
+        # 可以直接访问字典中的字段
+        
+        cmd =  data_dict['cmd']
+        if cmd !=  None:
+            if cmd == DEVICE_REPORT_RETURN_STRING:
+                data_cmd =  data_dict['data']
+                if "cmd" in data_cmd:
+                    cmd =  data_dict['data']["cmd"]
+                # {"cmd ":"post_rest","ts":1753870158426,"data":{"cmd":"post_heart_beat","rest":"success","msg":"."}}
+                    print("设备主动上报后台返回的数据 设备不需要回复")
+                    EventMesh.publish(cmd+"_R",data_dict)
+                else:
+                    print("返回的data里面没有cmd 返回的数据出错了")
+            else:
+                # {"cmd":"locate_now","msg":"","data":"876226211472149","ts":1753870224117}
+                print("后台主动下发给设备 需要设备回复给后台的数据命令") #后台主动下发的数据不为dict类型
+                EventMesh.publish(cmd+"_R",data_dict)
+        else:
+            print("cmd is None") #下发的指令无不解析
+    except Exception as e:
+        print("解析JSON出错:", str(e))
+date_tmp = utime.localtime()
+time_tmp = utime.mktime(date_tmp)
+MAGIC =  (0xfffe).to_bytes(2,"big")
+VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')
+TYPE = (0x03).to_bytes(1,"big")
+DATA_LEN = (0x00000000).to_bytes(4,"big")
+#magic+version+type+playload_len+playload
+#普通协议数据格式根据协议指定
+def protocol_send_total_data(data):
+    MAGIC =  (0xfffe).to_bytes(2,"big")
+    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')
+    TYPE = (0x03).to_bytes(1,"big")
+    HR_JSON = ujson.dumps(data)
+    GPS_JSON_b = HR_JSON.encode('utf-8')
+    DATA_LEN = (len(HR_JSON)).to_bytes(4,"big")
+    heards_infor = b''
+    heards_infor = MAGIC+VERSION+TYPE+DATA_LEN+GPS_JSON_b
+    return heards_infor
+#音频流数据格式
+#magic+version+type+msgid+playload_len+playload
+def protocol_audio_ji_playload_data(data,msgid,endflag):
+    MAGIC =  (0xfffe).to_bytes(2,"big")
+    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')
+    TYPE = (0x05).to_bytes(1,"big")
+    MSGID = msgid.to_bytes(4,"big")
+    ENDFLAG = endflag.to_bytes(1,"big")
+    DATA_LEN = (len(data)).to_bytes(4,"big")
+    heards_infor = b''
+    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data
+    return heards_infor
+
+#语音留言格式
+def protocol_yuyin_voice_playload_data(data,msgid,endflag):
+    MAGIC =  (0xfffe).to_bytes(2,"big")
+    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')
+    TYPE = (0x08).to_bytes(1,"big")
+    MSGID = msgid.to_bytes(4,"big")
+    ENDFLAG = endflag.to_bytes(1,"big")
+    DATA_LEN = (len(data)).to_bytes(4,"big")
+    heards_infor = b''
+    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data
+    return heards_infor
+
+#会议纪要格式
+def protocol_meet_playload_data(data,msgid,endflag):
+    MAGIC =  (0xfffe).to_bytes(2,"big")
+    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')
+    TYPE = (0x09).to_bytes(1,"big")
+    MSGID = msgid.to_bytes(4,"big")
+    ENDFLAG = endflag.to_bytes(1,"big")
+    DATA_LEN = (len(data)).to_bytes(4,"big")
+    heards_infor = b''
+    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data
+    return heards_infor
+
+#AI翻译格式
+def protocol_trans_playload_data(data,msgid,endflag):
+    MAGIC =  (0xfffe).to_bytes(2,"big")
+    VERSION = (0x01).to_bytes(1,"big")#"03".encode('utf-8')
+    TYPE = (0x0A).to_bytes(1,"big")
+    MSGID = msgid.to_bytes(4,"big")
+    ENDFLAG = endflag.to_bytes(1,"big")
+    DATA_LEN = (len(data)).to_bytes(4,"big")
+    heards_infor = b''
+    heards_infor = MAGIC+VERSION+TYPE+MSGID+ENDFLAG+DATA_LEN+data
+    return heards_infor
+#sos紧急电话class
+class sosInfoCallManager(Abstract): 
+    def __init__(self):
+        self.num = 0
+        self.num=0
+        self.num_len = 0
+        self.sos_list = []
+        self.sos_flag = 0
+        self.sos_timer1 = osTimer()
+        self.sos_timer2 = osTimer()
+        self.save_list = []
+        self.call_state = 0
+        self.sos_config = {}
+        self.phone110112_flag = 0
+        self.phone110112 = ""
+        if EventMesh.publish("get_storage_config","sos_list") != None:
+            self.sos_list = EventMesh.publish("get_storage_config","sos_list")
+        if EventMesh.publish("get_storage_config","sos_config") != None:
+            self.sos_config= EventMesh.publish("get_storage_config","sos_config")
+        
+        # self.sos_list=[{"index":0,"phone":"15521469561","name":"dengxueying0"}, {"name":"dengxueying1","index":0,"phone":"15692025051"},{"name":"dengxueying2","index":0,"phone":"15521469561"}]
+        EventMesh.subscribe("call_out_cb", self.call_out_cb) 
+        EventMesh.subscribe("get_sos_flag", self.get_sos_flag) 
+
+    def post_processor_after_instantiation(self):
+        self.num = 0
+        self.sos_flag = 0
+        voiceCall.setCallback(self.voice_callback)
+        EventMesh.subscribe("set_sos_info_call", self.set_sos_info_call)
+        EventMesh.subscribe("set_sos_list_info",self.set_sos_list_info)
+        EventMesh.subscribe("stop_sos_call",self.stop_sos_call)
+        EventMesh.subscribe("set_sos_config",self.set_sos_config)
+    def set_sos_config(self,event,msg):
+        # sos_config:{'enable110': '1', 'pressDuration': 3, 'enableSos': '0'}
+        print("set_sos_config:{}".format(msg))
+        self.sos_config = msg
+        # self.start_phone110112()
+        pass
+    def get_sos_flag(self,event,msg):
+        if self.call_state != 0:
+            return 1
+        else:
+            return 0
+        # import EventMesh
+        # EventMesh.publish("set_sos_info_call")
+    def close_call_timer(self):
+        self.sos_timer2.stop()
+        pass
+    def stop_sos_call(self,event,msg): #停止sos拨打
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.stop_sos_call_cb})
+    def stop_sos_call_cb(self):
+        if self.call_state == 0:
+            return
+        self.phone110112_flag=0
+        self.sos_flag = 0
+        self.call_state = 0
+        self.num=0 #不管有没有拨打sos都清除
+        self.num_len = 0
+        self.sos_timer2.stop()
+        self.sos_timer1.stop()
+        voiceCall.callEnd() #挂断
+
+    def start_phone110112(self):
+        print("start_phone110112:{} self.sos_config:{}".format(self.call_state,self.sos_config))
+        if self.sos_config != {}:
+            if "enableSos" in self.sos_config:
+                enableSos = self.sos_config["enableSos"]
+                if enableSos == "1":
+                    if "enable110" in self.sos_config:
+                        p110 = self.sos_config["enable110"]
+                        if p110 == "0":
+                            self.phone110112 = "110"
+                            tts_str = "正在呼叫{}".format(self.phone110112)
+                            EventMesh.publish("stop_voice_record") #电话优先级最高
+                            EventMesh.publish("stop_AI_record")
+                            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+                            self.sos_timer1.stop()
+                            self.sos_timer1.start(2000, 0, self.phone110112_cb)
+                        elif p110 == "1":
+                            self.phone110112 = "112"
+                            tts_str = "正在呼叫{}".format(self.phone110112)
+                            EventMesh.publish("stop_voice_record") #电话优先级最高
+                            EventMesh.publish("stop_AI_record")
+                            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+                            self.sos_timer1.stop()
+                            self.sos_timer1.start(2000, 0, self.phone110112_cb)
+            
+        else:
+            return 1
+
+    def sos_call_out_phone(self, msg):#电话拨出去
+        if self.call_state == 0:
+            ret = voiceCall.callStart(self.sos_phone_num)
+            if ret == -1:
+                
+                tts_str = "请确认卡支持语音拨号及余额充足"
+                EventMesh.publish("stop_voice_record") #电话优先级最高
+                EventMesh.publish("stop_AI_record")
+                EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+                self.call_state = 0
+                self.num=0 #不管有没有拨打sos都清除
+                self.num_len = 0
+                self.sos_flag = 0
+                self.phone110112_flag=0
+                return -1
+            else:
+                EventMesh.publish(constants.EVENT.START_LOCATION,WEB_CMD_ENUM.POST_SOS_S_CMD) #开始AL定位
+                self.sos_timer2.stop()
+                self.sos_timer2.start(30000, 0, self.sos_time_out_phone)
+        return 1
+            
+    def phone110112_cb(self,*argc):
+        if self.call_state == 0:
+            ret = voiceCall.callStart(self.phone110112)
+            if ret != -1:
+                self.phone110112_flag=1
+            else:
+                self.phone110112_flag=0
+            # print("ret:{} {}".format(ret,self.phone110112))
+            self.phone110112 = ""
+
+    def set_sos_list_info(self,event,msg):
+        self.sos_list = msg
+
+        
+    def call_out_cb(self,event,phonenumber):
+        
+        if self.call_state == 0:
+            self.sos_phone_num = phonenumber
+            EventMesh.publish("stop_voice_record") #电话优先级最高
+            EventMesh.publish("stop_AI_record")
+            
+            tts_str = "正在呼叫号码{}".format(self.sos_phone_num)
+            EventMesh.publish("stop_voice_record") #电话优先级最高
+            EventMesh.publish("stop_AI_record")
+            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+            self.sos_timer1.stop()
+            self.sos_timer1.start(2000, 0, self.sos_call_out_phone)
+            
+            
+      
+    def sos_time_out_phone(self, msg):#30s超时未打通或者信号不好则重新拨打
+        
+        if self.sos_flag == 1:
+            if self.call_state == 0:
+                voiceCall.callEnd() #挂断
+                EventMesh.publish("set_sos_info_call") #电话优先级最高
+            pass #再次拨打
+
+    def set_sos_info_call(self,event,msg):
+        if EventMesh.publish("get_yuyinaudio_flag") == 1 or EventMesh.publish("get_voice_record_mode") != 0: #语言留言模式不允许拨打电话
+            self.call_state = 0
+            self.num=0 #不管有没有拨打sos都清除
+            self.num_len = 0
+            self.sos_flag = 0
+            self.phone110112_flag=0
+            return
+        if sim.getStatus() != 1:#网络正常
+            tts_str = "无SIM卡"
+            EventMesh.publish("stop_voice_record") #电话优先级最高
+            EventMesh.publish("stop_AI_record")
+            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+            self.call_state = 0
+            self.num=0 #不管有没有拨打sos都清除
+            self.num_len = 0
+            self.sos_flag = 0
+            self.phone110112_flag=0
+            return
+        if self.call_state != 0:
+            return
+        if len(self.sos_list) > 0:
+            self.call_state = 0
+            self.call_sos_cb(1)
+        else:
+            tts_str = "未设置号码"
+            EventMesh.publish("stop_voice_record") #电话优先级最高
+            EventMesh.publish("stop_AI_record")
+            EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+            self.call_state = 0
+            self.num=0 #不管有没有拨打sos都清除
+            self.num_len = 0
+            self.sos_flag = 0
+            self.phone110112_flag=0
+
+    def call_sos_cb(self,*argc):
+        self.phone110112_flag=0
+        if self.sos_flag == 0:
+            self.sos_flag = 1
+        sos_flag = self.sos_flag
+        EventMesh.debug_log('voicecall sos_flag',sos_flag)
+        if sos_flag == 0 :
+            self.num = 0
+        else:
+            if len(self.sos_list) > 0:
+                # for list_t in self.sos_list:
+                #     tmp = list_t["phone"]
+                #     self.save_list.append(tmp)
+                    
+                self.num +=1
+                if len(self.sos_list) == 1:
+                    self.num_len = len(self.sos_list)*3#三次 1
+                elif len(self.sos_list) == 2:
+                    self.num_len = len(self.sos_list)*3 #1+2+1+2
+                elif len(self.sos_list) == 3:
+                    self.num_len = len(self.sos_list)*3 #1+3+5
+                # EventMesh.debug_log("number:{} number_len :{} num:{} num_len:{}".format(number,len(number),num,num_len))
+                if self.num > self.num_len :
+                    
+                    self.start_phone110112()
+                    EventMesh.debug_log("call phone!!!!!!!")
+                    self.sos_flag = 0
+                    self.num = 0
+                    self.num_len = 0
+                else:
+                    self.phone110112_flag=0
+                    EventMesh.debug_log("self.num:{} self.num_len :{}".format(self.num,self.num_len))
+
+                    if len(self.sos_list) == 1:
+                        if len(self.sos_list[0]["phone"]) != 0:
+                            EventMesh.publish("call_out_cb", self.sos_list[0]["phone"]) 
+                            # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["pnone"])
+                            EventMesh.debug_log("phone 1 {}".format(self.sos_list[0]["phone"]))
+                    elif len(self.sos_list) == 2:
+                        # 1 2 
+                        # 3 4 
+                        # 5 6
+                        if self.num == 1 or self.num == 3 or self.num == 5: #1 2 3 4 5 
+                            if len(self.sos_list[0]) != 0:
+                                EventMesh.debug_log("phone 2 {}".format(self.sos_list[0]["phone"]))
+                                EventMesh.publish("call_out_cb", self.sos_list[0]["phone"]) 
+                                # EventMesh.publish("set_callnumer_cb",self.sos_list[1]["pnone"])
+                        elif self.num == 2 or self.num == 4 or self.num == 6:
+                            if len(self.sos_list[1]) != 0:
+                                EventMesh.debug_log("phone 3 {}".format(self.sos_list[1]["phone"]))
+                                EventMesh.publish("call_out_cb", self.sos_list[1]["phone"]) 
+                                # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["pnone"])
+                    elif len(self.sos_list) == 3:
+                        # 1 2 3 
+                        # 4 5 6 
+                        # 7 8 9 
+                        if self.num == 1 or self.num == 4 or self.num == 7:
+                            if len(self.sos_list[0]) != 0:
+                                EventMesh.publish("call_out_cb", self.sos_list[0]["phone"]) 
+                                # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["phone"])
+                                EventMesh.debug_log("phone 4 {}".format(self.sos_list[0]["phone"]))
+                        elif self.num == 2 or self.num == 5 or self.num == 8:
+                            if len(self.sos_list[1]) != 0:
+                                EventMesh.debug_log("phone 5 {}".format(self.sos_list[1]["phone"]))
+                                EventMesh.publish("call_out_cb", self.sos_list[1]["phone"]) 
+                                # EventMesh.publish("set_callnumer_cb",self.sos_list[2]["phone"])
+                        elif self.num == 3 or self.num == 6 or self.num == 9:
+                            if len(self.sos_list[2]) != 0:
+                                EventMesh.debug_log("phone 6 {}".format(self.sos_list[2]["phone"]))
+                                EventMesh.publish("call_out_cb", self.sos_list[2]["phone"]) 
+                                # EventMesh.publish("set_callnumer_cb",self.sos_list[0]["phone"])
+            else: #清除表示
+                tts_str = "未设置号码"
+                EventMesh.publish("stop_voice_record") #电话优先级最高
+                EventMesh.publish("stop_AI_record")
+                EventMesh.publish("play_tts",[tts_str,1])  #播放tts闹钟文本内容
+                self.sos_flag = 0
+                self.num = 0
+                self.call_state = 0
+                self.num=0 #不管有没有拨打sos都清除
+                self.num_len = 0
+                self.phone110112_flag=0
+                #未设置号码
+            
+
+    def voice_callback(self,args):
+        EventMesh.debug_log('111test voicecall iinfo:{}'.format(args))
+        if args[0] == 10: #电话拨进来
+            self.sos_flag = 0
+            self.call_state = 1 
+            self.phone110112_flag=0
+        elif args[0] == 11: #已接听
+            self.call_state = 2
+            self.num=0 #不管有没有拨打sos都清除
+            self.num_len = 0
+            self.sos_flag = 0
+            self.phone110112_flag=0
+            self.close_call_timer()
+
+            EventMesh.debug_log('voicecall connected, PhoneNO.: ', args[6])
+
+        elif args[0] == 12: #电话挂断
+            self.phone110112_flag=0
+            if self.call_state == 2 or self.call_state == 1: #电话拨出去没有接听 就挂断
+                pass
+            else:
+               
+                EventMesh.debug_log('voicecall disconnect end !!!')
+                self.close_call_timer()
+                self.call_state = 0
+                if self.sos_flag == 1:
+                    EventMesh.publish("set_sos_info_call")
+                pass
+            self.call_state = 0
+                #再次拨打下一个电话号码
+            EventMesh.debug_log('voicecall disconnect end')
+                
+
+        elif args[0] == 13: #
+            self.call_state = 7
+            EventMesh.debug_log('voicecall is waiting, PhoneNO.: ', args[6])
+        elif args[0] == 14: #电话拨出去
+            EventMesh.debug_log('voicecall dialing, PhoneNO.: ', args[6])
+            if self.sos_flag == 1:
+                self.call_state = 3 #当前是拨打sos号码标识
+            else:
+                self.call_state = 4
+                
+        elif args[0] == 15:
+            self.call_state = 5
+            EventMesh.debug_log('voicecall alerting, PhoneNO.: ', args[6])
+                    
+                
+        elif args[0] == 16:
+            self.call_state = 6
+            EventMesh.debug_log('voicecall holding, PhoneNO.: ', args[6])
+
+
+
+        
+try:   
+    import usr.ai_alarm as _schedule
+except Exception as e:
+    usys.print_exception(e)
+    import ai_alarm as _schedule
+DEFIEN_HR_REPORT_TIME = 5*60
+DEFINE_TIME_COUNT = 10
+DEFIEN_LOCATION_REPORT_TIME = DEFINE_TIME_COUNT
+WEB_CONNECT_MAX_COUNT = 10 #默认重连次数
+class SockMangager(Abstract):
+    def __init__(self):
+        self.thread_buf_size = 16*1024
+        self.iot_web_send_queue = Queue(100)
+        self.iot_web_protoclo_recv_id=0
+        self.iot_protocol_client = None
+        self.connect_web_timer = osTimer() 
+        self.connect_web_timer_count = 0
+        self.hr_report_timer = osTimer()
+        self.resolver = None
+        self.timerHr = 0
+        self.timerLocation = 0
+        self.send_device_time = 0
+        self.vbat_buf = []
+        self.network_state = 0
+        self.curr_battery_level = 0
+        self.iot_web_send_queue_id = 0
+        self.bat_count = 0
+        self.minute_now_old = 0
+        self.old_minute = 0
+        self.is_alarm = 0
+        self.reconnect_count = 0 #连续重连次数 默认十次 不过就不重连了 等下一个心跳重连
+        self.schedule = _schedule.schedule()
+        self._1S_timer = osTimer()
+        self._5S_timer = osTimer()
+        self.timer_id = 0
+        self.start_voice_flag = 0
+        self.init_send_default=0
+        self.play_new_voice_state = 0
+        self.stop_play = 0
+        self.get_voice_new_msginit = 0
+        self.voice_id = "0"
+        self.bt_connect_list = []
+        self.new_voice_list = []
+        self.location_info= {}
+        self.locate_duration = 0
+        self.locate_duration_state = 0
+        self.location_info = EventMesh.publish("get_storage_config","locate_duration_time") #获取定位频率信息
+
+        self.temp = 0
+        if self.location_info == None:
+            self.location_info = {}
+        else:
+            # location_time["duration"]=locate_duration_time
+            # location_time["state"]=locate_duration_state
+            if "duration" in self.location_info:
+                self.locate_duration = self.location_info["duration"] #定位频率
+            if "state" in self.location_info:
+                self.locate_duration_state = self.location_info["state"] #是否开启频率定位
+            pass
+        EventMesh.subscribe("REPORT_ALARM_TO_IOT",self.report_alarm_to_iot)
+        EventMesh.subscribe("REPORT_SCHEDULE_TO_IOT",self.report_schedule_to_iot)
+
+    def report_alarm_to_iot(self,event,msg):
+        EventMesh.publish(WEB_CMD_ENUM.POST_ALARM_S_CMD,msg)
+        pass
+    def report_schedule_to_iot(self,event,msg):
+        EventMesh.publish(WEB_CMD_ENUM.POST_SCHEDULE_S_CMD,msg)
+        pass
+    def usb_event_cb(self, state):
+        status = state
+    def post_processor_after_instantiation(self):
+        self.minute_now_old=0
+        EventMesh.subscribe("set_curr_battery_level",self.set_curr_battery_level)
+        EventMesh.subscribe("get_curr_battery_level",self.get_curr_battery_level)
+        EventMesh.subscribe("screen_get_battery",self.get_battery)
+        EventMesh.subscribe("CHANNEL_IOT_WEB_SEND",self.user_send)
+        EventMesh.subscribe("set_temp",self.set_temp)
+        EventMesh.subscribe("GET_IOT_PROTOCOL_CILENT",self.get_iot_protoclo_client)
+        EventMesh.subscribe("SEND_JINMOAUDIO_TO_IOT",self.send_jm_audio_to_iot)
+        EventMesh.subscribe("SEND_YUYINAUDIO_TO_IOT",self.send_yuyin_audio_to_iot)
+        EventMesh.subscribe("SEND_MEETAUDIO_TO_IOT",self.send_meet_audio_to_iot)
+        EventMesh.subscribe("SEND_TRANSAUDIO_TO_IOT",self.send_trans_audio_to_iot)
+        EventMesh.subscribe("open_websocket_timer",self.open_websocket_timer)
+        EventMesh.subscribe("close_websocket_timer",self.close_websocket_timer)
+        EventMesh.subscribe("connect_iot_websocket_cb",self.connect_iot_websocket_cb)
+        EventMesh.subscribe("set_play_new_voice_list",self.set_play_new_voice_list)
+        EventMesh.subscribe("get_play_new_voice_list",self.get_play_new_voice_list)
+
+        EventMesh.subscribe("send_meet_open_flag_to_iot",self.send_meet_open_flag_to_iot)#会议创建
+        EventMesh.subscribe("send_meet_end_flag_to_iot",self.send_meet_end_flag_to_iot)#会议结束
+
+        EventMesh.subscribe("send_trans_come_flag_to_iot",self.send_trans_come_flag_to_iot)#进入翻译
+        EventMesh.subscribe("send_trans_ok_flag_to_iot",self.send_trans_ok_flag_to_iot)#向APP发送进入翻译成功
+        EventMesh.subscribe("send_trans_end_flag_to_iot",self.send_trans_end_flag_to_iot)#退出翻译
+        # import EventMesh
+        # EventMesh.publish("play_new_voice_list")
+        EventMesh.subscribe("play_new_voice_list",self.play_new_voice_list) #播放语音未读信息
+        EventMesh.subscribe("update_play_new_voice_info",self.update_play_new_voice_info) #更新语音未读信息状态上报IOT
+        EventMesh.subscribe("send_bt_scan_list",self.send_bt_scan_list) #上报扫描列表
+        EventMesh.subscribe("save_bt_connect_list",self.save_bt_connect_list) #保存耳机连接状态列表
+        EventMesh.subscribe("send_bt_connect_list",self.send_bt_connect_list) #发送耳机连接状态列表
+        EventMesh.subscribe("set_play_new_voice_state",self.set_play_new_voice_state) #设置播放语音未读状态1正在播放语音未读消息
+        EventMesh.subscribe("get_play_new_voice_state",self.get_play_new_voice_state) #获取播放语音未读状态1正在播放语音未读消息
+        EventMesh.subscribe("set_stop_play",self.set_stop_play) #外部控制url音频播放列表暂停
+        EventMesh.subscribe("get_stop_play",self.get_stop_play) #获取外部控制url音频播放列表暂停情况
+        EventMesh.subscribe("send_bt_connect_state_to_iot",self.send_bt_connect_state_to_iot) #发耳机;链接状态->IOT
+        EventMesh.subscribe("send_unbind_dev_to_iot",self.send_unbind_dev_to_iot) #发送解绑指令到IOT平台
+        EventMesh.subscribe("set_location_info",self.set_location_info)  #设置定位信息
+        EventMesh.subscribe("post_device_state",self.post_device_state) #上报设备状态信息-》IOT
+        EventMesh.subscribe("send_power_init_data",self.send_power_init_data) #上电初始化上报数据集合
+        # count = 0
+        # while True:
+        #     count += 1
+        #     if count > 20 :
+        #         break
+        #     data = dataCall.getInfo(1, 0)
+        #     print("data call INFO {}".format(data))
+        #     if data[2][0]:
+        #         self.network_state = 1
+        #         break
+        #     print("web等待网络 {}...".format(count))
+
+        #     utime.sleep_ms(500)
+        self.timerHr = 0
+        self.reconnect_count = 0
+        self.voice_id = "0"
+        self.deviceReadState = ""
+        self.get_voice_new_msginit = 0
+        self.bt_connect_list = []
+        self.new_voice_list = []
+        self.connect_web_timer.start(2000,0,self.iot_web_connect_timerCb) #开机2s之后连接iot
+    def connect_iot_websocket(self):
+        if EventMesh.publish("get_voice_record_flag") == 1:
+            EventMesh.publish("stop_voice_record")
+        print("connect_iot_websocket !!!!")
+        self.iot_close_web_therad()
+        
+        if sim.getStatus() != 1 or self.start_voice_flag == 1:#网络正常
+            return
+
+        if self.reconnect_count >= WEB_CONNECT_MAX_COUNT: #重连次数大于10次不在进重连只有下发心跳才重新连接
+            return
+        token = EventMesh.publish("get_dev_token")
+
+        print("token:{}".format(token))
+        # token = "957119d6b67b89dc1bc533d2e0c058d559"
+        # token = "951b74498eabe241e8a8c644a4f07531ef"
+
+        try:
+            if token != None:
+                self.connect_web_timer_count = 0
+                url_t = "{}?bz-dt={}".format(constants.IOT_WS_URL, token)
+                try:
+                    self.iot_protocol_client=uwebsocket.Client.connect(url_t, debug=True)
+                    self.reconnect_count=0
+                except Exception as e:
+                    print("再次连接,iot连接报错是")
+                    print(e)
+                    self.reconnect_count+=1
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) #self.connect_iot_websocket()
+                    return 
+
+                if self.iot_protocol_client != None:
+                    self.reconnect_count = 0
+                    _thread.stack_size(self.thread_buf_size+8*1024) #_thread.stack_size(16*1024)
+                    self.iot_web_send_queue_id = _thread.start_new_thread(self.websocket_send_queue_thread, ())
+                    _thread.stack_size(8*1024)
+                
+                    _thread.stack_size(self.thread_buf_size+8*1024)
+                    self.iot_web_protoclo_recv_id = _thread.start_new_thread(self.websocket_recv_queue_thread, ())
+                    _thread.stack_size(8*1024)
+                    
+                else:
+                    print("self.iot_protocol_client init!!!!!!!!!")
+                    self.reconnect_count+=1
+                    # EventMesh.publish("connect_iot_websocket_cb")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) #self.connect_iot_websocket()
+                    return 
+
+                if self.iot_protocol_client != None:
+                    self.start_voice_flag = 1
+                else:
+                    self.start_voice_flag = 0
+                    
+                print("self.iot_protocol_client %d self.start_voice_flag:%d",self.iot_protocol_client,self.start_voice_flag)
+                if self.iot_protocol_client != None:
+                    if self.timer_id == 0:
+                        # self.hr_report_timer.start(DEFINE_TIME_COUNT*1000, 1, self.public_report_event)
+                        self.timer_id=1
+            else:
+                print("没有token!!!!!!!!!!")
+                self.connect_web_timer_count+=1
+                if self.connect_web_timer_count <= 10:
+                    self.connect_web_timer.start(5000,0,self.iot_web_connect_timerCb) #没有token 5s之后再重连
+                pass
+        except Exception as e:   
+            self.iot_protocol_client = None
+            print("connect_iot_websocket error")
+    
+    def post_device_state(self,event,msg): #上报设备状态
+        device_state=EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD)
+        print("device_state{}".format(device_state))
+        EventMesh.publish(WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD,device_state) #上报设备状态
+    def set_location_info(self,event,msg):
+        if "duration" in msg:
+            self.locate_duration=msg["duration"]
+        if "state" in msg:
+            self.locate_duration_state=msg["state"]
+
+
+    def save_bt_connect_list(self,event,msg):
+        print("msg:{}".format(msg))
+        # msg = {"event":"bt_connect_single","data":{"name":"yeiwyei","mac":"89:89:78:78:98:89","state":"connected"}}
+        try:
+            data = msg["data"]
+            if "None" in data: #开机没有连接过蓝牙耳机就为kong
+                return
+            if isinstance(data,dict) == True:
+                mac = data["mac"]
+                state = data["state"]
+                name = data["name"]
+                need_save = 0
+                for i,_alarm in enumerate(self.bt_connect_list):
+                    mac_tmp = _alarm["mac"]
+                    if mac_tmp == mac:
+                        need_save = 1
+                        self.bt_connect_list[i]["state"] = state
+                        self.bt_connect_list[i]["name"] = name
+                        break
+                    self.bt_connect_list[i]["state"] ="disconnected"
+
+                if need_save == 0: #列表里面没有才需要保存
+                    self.bt_connect_list.append(data)
+                print("self.bt_connect_list:{}".format(self.bt_connect_list))
+        except Exception as e:
+            print("json数据不完整:self.recv_bt_connect_record_list")
+            usys.print_exception(e)
+        pass
+    def send_bt_connect_list(self,event,msg):
+        
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        bt_connect_record_list = {
+            "cmd":"bt_connect_record_list",
+            "ts":time_tmp,
+            "msg":"上报蓝牙连接列表",
+            "data":self.bt_connect_list
+                
+        }
+        print("bt_connect_record_list:{}".format(bt_connect_record_list))
+        EventMesh.publish(WEB_CMD_ENUM.BT_CONNECT_RECORD_LIST_S,bt_connect_record_list)
+        pass
+
+    def set_play_new_voice_list(self,event,msg):
+        self.new_voice_list = msg
+        pass
+
+    def get_play_new_voice_list(self,event,msg):
+        return self.new_voice_list
+
+    def set_play_new_voice_state(self,event,msg):
+        self.play_new_voice_state = msg
+        pass
+
+    def get_play_new_voice_state(self,event,msg):
+        return self.play_new_voice_state
+
+    def get_stop_play(self,event,msg):
+        return self.stop_play
+
+    def set_stop_play(self,event,msg):
+        self.stop_play = msg
+        pass
+
+    def _1S_timer_cb(self,*argc):
+        EventMesh.publish("publish_AI_btnInfo",{"func":self._1S_timer_cb_}) 
+
+    def _1S_timer_cb_(self): 
+        #self.iot_web_send_queue.put("ping") 
+        get_VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")
+        if get_VC_satrt_flag == None:
+            get_VC_satrt_flag = 0
+        if get_VC_satrt_flag == 0:
+            self.play_new_voice_list_timer_cb()
+            self._1S_timer.stop()
+
+    def play_new_voice_list(self,event,msg):
+        if len(self.new_voice_list) != 0:
+            self._1S_timer.start(1000, 1, self._1S_timer_cb)
+
+    def play_new_voice_list_timer_cb(self):
+        print("play_new_voice_list:{}".format(len(self.new_voice_list)))
+        voice_list = self.new_voice_list
+        if len(self.new_voice_list) != 0:
+            
+            for i,data in enumerate(self.new_voice_list):
+                if EventMesh.publish("get_history_voice_flag") == 1:
+                    data = voice_list[len(self.new_voice_list)-1-i]
+                print("data:{}".format(data))
+                self.voice_index = i
+                self.voice_id = str(data["id"])
+                url = str(data["url"])
+                # url = "http://qipaiai-oss1.tos-cn-beijing.volces.com/qi_pai_ringtone/2025_08_11/742659759219544064test.mp3"
+                self.play_new_voice_state = 1
+                if self.stop_play == 1:
+                    print("结束播放")
+                    self.stop_play = 0
+                    break
+                if EventMesh.publish("get_storage_config","is_noread_voice") == 1:#还有未读的(开机)
+                    if EventMesh.publish("get_open_device") == 1:#刚开机
+                        EventMesh.publish("play_wait_voice",[url,1])
+                        EventMesh.publish("set_open_device",0)
+                if EventMesh.publish("get_history_voice_flag") == 1:#播放历史留言 or 播放当前留言
+                    if EventMesh.publish("get_wait_voice_flag") == 1:#等待留言
+                        EventMesh.publish("play_wait_voice",[url,1])
+                    else:
+                        time = str(data["createTimeStr"])
+                        EventMesh.publish("play_his_voice",[url,1,time])
+                    if len(EventMesh.publish("get_play_new_voice_list")) == 0:#列表为空才清零
+                        EventMesh.publish("set_history_voice_flag",0)#修改标志位
+                else:
+                    EventMesh.publish("play_new_voice",[url,1])
+                break
+            pass
+    def update_play_new_voice_info(self,event,msg): #更新播放语音留言信息
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        put_voice_msg_state = {
+            "cmd":"put_voice_msg_state",
+            "ts":time_tmp,
+            "msg":"修改语音消息阅读状态",
+            "data":self.voice_id
+        }
+        if len(self.new_voice_list) != 0:
+            self.new_voice_list.pop(self.voice_index)
+        EventMesh.publish(WEB_CMD_ENUM.PUT_VOICE_MSG_STATE_S,put_voice_msg_state)
+        pass
+    
+    def send_bt_scan_list(self,event,msg):
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        try:
+            # print("type:{}".format(msg))
+            data = msg["data"]
+            bt_scan_rest ={
+                "cmd":"bt_scan_rest",
+                "ts":time_tmp,
+                "msg":"上报扫描列表",
+                "data":data
+            }
+            EventMesh.publish(WEB_CMD_ENUM.BT_SCAN_REST_S_CMD,bt_scan_rest)
+        except Exception as e:
+            usys.print_exception(e)
+        pass
+    def send_unbind_dev_to_iot(self,event,msg):
+        self._5S_timer.start(5000,0,self._5S_timer_cb)
+    def _5S_timer_cb(self,*argc):
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.send_unbind_dev_to_iot_cb})
+        pass
+    def send_unbind_dev_to_iot_cb(self):
+        EventMesh.publish(WEB_CMD_ENUM.UNBIND_DEV_S)
+        pass
+    def send_bt_connect_state_to_iot(self,event,msg):
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        
+        try:
+            # {
+            #     "cmd":"post_bt_connect_rest",
+            #     "ts":1738283829,
+            #     "msg":"上报蓝牙连接结果",
+            #     "data":{
+            #         "mac":"mac地址",
+            #         "state":"connected/disconnected/connecting/failed"
+            #     }
+            # }
+            event = msg["event"]
+            mac = msg["data"]["mac"]
+            state = msg["data"]["state"]
+            json_data = \
+            {
+                
+                "cmd": event,
+                "ts":time_tmp,
+                "msg":"上报蓝牙连接结果",
+                "data":{
+                    "mac":mac,
+                    "state":state
+                }   
+            
+            }
+            EventMesh.publish(WEB_CMD_ENUM.POST_BT_CONNECT_REST_S,json_data)
+        except Exception as e:
+            usys.print_exception(e)
+        pass
+    
+    def send_meet_open_flag_to_iot(self,event,msg): #创建会议纪要上报到IOT平台
+        time_tmp = utime.mktime(date_tmp)
+        meet_open = {
+                "msg": "开始会议",
+                "data": None,
+                "cmd": "meet_begin",
+                "ts": time_tmp
+            }
+        send_data = protocol_send_total_data(meet_open)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_meet_end_flag_to_iot(self,event,msg): #结束会议纪要上报到IOT平台
+        time_tmp = utime.mktime(date_tmp)
+        meet_end = {
+                "msg": "结束会议",
+                "data": None,
+                "cmd": "meet_end",
+                "ts": time_tmp
+            }
+        send_data = protocol_send_total_data(meet_end)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_trans_come_flag_to_iot(self,event,msg): #进入翻译上报到IOT平台
+        time_tmp = utime.mktime(date_tmp)
+        trans_open = {
+            "msg": "进入翻译模式",
+            "data": None,
+            "cmd": "translate_device_enter",
+            "ts": time_tmp
+        }
+        send_data = protocol_send_total_data(trans_open)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_trans_ok_flag_to_iot(self,event,msg): #成功进入翻译发给APP
+        time_tmp = utime.mktime(date_tmp)
+        trans_ok = {
+            "msg": "进入成功",
+            "data": None,
+            "cmd": "translate_device_complete",
+            "ts": time_tmp
+        }
+        send_data = protocol_send_total_data(trans_ok)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+    
+    def send_trans_end_flag_to_iot(self,event,msg): #退出翻译上报到IOT平台
+        time_tmp = utime.mktime(date_tmp)
+        trans_open = {
+            "msg": "退出翻译",
+            "data": None,
+            "cmd": "translate_device_exit",
+            "ts": time_tmp
+        }
+        send_data = protocol_send_total_data(trans_open)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_jm_audio_to_iot(self,event,msg): #静默录音音频数据上报到IOT平台
+        send_data = b''
+        send_data = protocol_audio_ji_playload_data(msg[0],msg[1],msg[2])
+        # print("jm:{}".format(msg[2]))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_yuyin_audio_to_iot(self,event,msg): #上报语音留言音频数据到IOT平台
+        send_data = b''
+        send_data = protocol_yuyin_voice_playload_data(msg[0],msg[1],msg[2])
+        # print("yuyin:{}".format(msg[2]))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_meet_audio_to_iot(self,event,msg): #上报会议纪要音频数据到IOT平台
+        send_data = b''
+        send_data = protocol_meet_playload_data(msg[0],msg[1],msg[2])
+        # print("yuyin:{}".format(msg[2]))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+
+    def send_trans_audio_to_iot(self,event,msg): #上报AI翻译音频数据到IOT平台
+        send_data = b''
+        send_data = protocol_trans_playload_data(msg[0],msg[1],msg[2])
+        # print("yuyin:{}".format(msg[2]))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        pass
+    
+
+    #获取电池电量
+    def get_battery(self, event=None, msg=None):
+        """
+        获取电池事件,暂时没支持
+        :param event:
+        :param msg:
+        :return:
+        """
+
+        charge_state = '0'
+        usb_state = EventMesh.publish("GET_USB_STATE")
+        if usb_state == None:
+            usb_state = 0
+
+        if  usb_state == 1:
+            charge_state = '1'
+        else:
+            charge_state = '0'
+
+        vbat = EventMesh.publish("get_battery")
+        if vbat is None:
+            vbat = 100
+        # EventMesh.debug_log("vbat",vbat)
+        # if temp != None :
+        #     if temp <= 3505:
+        #         vbat = 0 
+        #     elif temp <= 3699:
+        #         vbat = 20 #"E:/vbat_20.jpg"
+        #     elif temp <= 3772:
+        #         vbat = 40 #"E:/vbat_40.jpg"
+        #     elif temp <= 3882:
+        #         vbat = 60 #"E:/vbat_60.jpg"
+        #     elif temp <= 4070:
+        #         vbat = 80 #"E:/vbat_80.jpg"
+        #     elif temp <= 4200:
+        #         vbat = 90 #
+        #     else:
+        #         vbat = 100 #"E:/vbat_100.jpg"
+        # else:
+        #     vbat = 100
+
+        result = [charge_state, vbat]
+
+        return result
+    def set_curr_battery_level(self, event=None, msg=None):
+        self.curr_battery_level = msg
+        
+    def get_curr_battery_level(self, event=None, msg=None):
+        return self.curr_battery_level
+    
+    def initialization(self, *args):
+        pass
+        #EventMesh.debug_log("ProtoCol:{}".format(self.TYPE + "_SEND"))
+    def connect_iot_websocket_cb(self,event,msg):
+        self.connect_iot_websocket()
+    def add_support_resolver(self, resolver):
+        self.resolver = resolver
+    def public_report_event(self, *args, **kwargs):
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.public_report_event_cb}) #self.connect_iot_websocket()
+    def send_power_init_data(self,event=None,msg=None):
+        self.public_report_event_cb()
+    #上报心跳 上报设备状态 检查闹钟 日程
+    def public_report_event_cb(self):
+        """电池平均值:与 vc_hw._BatteryCfg 共用 10 点滑动平均,避免重复 getVbatt。"""
+        bat_total_value = 0
+        try:
+            from vc_hw import _BatteryCfg
+            bcfg = _BatteryCfg.instance()
+            try:
+                usb_st = EventMesh.publish("GET_USB_STATE")
+            except Exception:
+                usb_st = 0
+            bcfg.push_vbatt_sample(usb_status=usb_st)
+            bat_total_value = bcfg.soc_now()
+        except Exception:
+            bat_total_value = 0
+        # EventMesh.publish("AI_schedule_open")#读取闹钟数组
+        # print("AI_schedule_open!!!")
+        if EventMesh.publish("get_sos_flag") != None:
+            if EventMesh.publish("get_sos_flag") == 1:
+                print("get_sos_flag!!!!!!")
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+                print("get_sos_flag1!!!!!!")
+
+        EventMesh.publish("AI_alarm_start") #检查是否有闹钟需要提醒
+        EventMesh.publish("AI_schedule_open")#检查日程是否需要提醒
+        # self.alarm_test()
+        EventMesh.publish("set_curr_battery_level",bat_total_value)
+        # EventMesh.publish("publish_AI_btnInfo",{"func":self.hr_report_publish_event,"data":0})  
+        self.timerHr+=DEFINE_TIME_COUNT
+        if self.timerHr >= DEFIEN_HR_REPORT_TIME:
+            self.timerHr= 0
+            self.reconnect_count = 0
+            self.connect_web_timer_count=0
+            EventMesh.publish("publish_AI_btnInfo",{"func":self.hr_report_publish_event,"data":0}) 
+        # print("self.locate_duration_state:{} self.locate_duration:{}".format(self.locate_duration_state,self.locate_duration))
+        if self.locate_duration_state == 1:
+            if self.locate_duration == None:
+                self.timerLocation = 0
+            else:
+                if self.locate_duration != 0:
+                    self.timerLocation+=DEFINE_TIME_COUNT
+                    if self.timerLocation >= self.locate_duration:
+                        self.timerLocation= 0
+                        EventMesh.publish(constants.EVENT.START_LOCATION,WEB_CMD_ENUM.POST_LOCATION_S_CMD) #定位上报
+                else:
+                    self.timerLocation= 0
+        else:
+            self.timerLocation= 0
+
+       
+        if self.send_device_time < DEFIEN_LOCATION_REPORT_TIME : #上电初始化上报一次设备状态信息
+            self.send_device_time+=DEFINE_TIME_COUNT
+        else:
+            if self.get_voice_new_msginit == 0: 
+                self.get_voice_new_msginit=1
+                EventMesh.publish("publish_AI_btnInfo",{"func":self.hr_report_publish_event,"data":0}) 
+                device_state=EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD)
+                print("device_state{}".format(device_state))
+                EventMesh.publish(WEB_CMD_ENUM.POST_DEVICE_STATE_S_CMD,device_state) #上报设备状态
+                # if dev_type == "7PIN_1_5":#二代开机获取留言
+                #     EventMesh.publish("set_open_device",1)
+                #     EventMesh.publish("set_history_voice_flag",1)
+                #     EventMesh.publish("set_wait_voice_flag",1)
+                #     EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)#向IOT获取语音未读消息
+            
+        
+        # http://220.160.104.34:30800/ws-bus/test/voice-record/876226211472149?state=1,
+        # http://220.160.104.34:30800/ws-bus/test/860132066107046?testType=alarmList
+    def hr_report_publish_event(self,data):
+        
+        HR_DATA = EventMesh.publish("GET_IOT_WEB_SEND_CMD",WEB_CMD_ENUM.HR_S_CMD)
+        print("HR_DATA{}".format(HR_DATA))
+        EventMesh.publish(WEB_CMD_ENUM.HR_S_CMD,HR_DATA)
+        pass
+
+    def iot_web_connect_timerCb(self,*argc):
+        self.connect_iot_websocket()
+
+    
+    def open_websocket_timer(self,event,msg):
+        self.VC_pingping_timerout = 0
+        # self._1S_timer.start(500,1,self._1S_timer_cb)
+    
+    def close_websocket_timer(self,event,msg):
+        self._1S_timer.stop()
+
+    
+        
+    def websocket_send_queue_thread(self):
+        while True:
+            data = self.iot_web_send_queue.get()
+            
+            if data != None:
+                # EventMesh.debug_log("websocket:send data1 len:{}".format(len(data)))
+                try:
+                    # EventMesh.debug_log("websocket:send data1 len:{} {}".format(len(data),self.iot_protocol_client))    
+                    if self.iot_protocol_client != None:
+                        self.iot_protocol_client.send(data)
+                        self.reconnect_count = 0
+                    else:
+                        # EventMesh.publish("connect_iot_websocket_cb")
+                        EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})
+                    EventMesh.debug_log("websocket:send data2 len:{}".format(len(data),self.iot_protocol_client))    
+                except Exception as e:
+                    EventMesh.debug_log("websocket:send error") 
+                    # EventMesh.publish("connect_iot_websocket_cb")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})
+                    break
+             
+
+    def websocket_recv_queue_thread(self):
+        while True:
+            # 死循环接收数据
+            try:
+                recv_data = self.iot_protocol_client.recv()
+                print("recv_data = {}".format(len(recv_data)))
+                if recv_data == "websocket close":
+                    # 服务器关闭连接或客户端关闭连接
+                    print("对端 服务器强制关闭 cli close")
+                    # EventMesh.publish("connect_iot_websocket_cb")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) 
+                    break
+                elif recv_data == "":
+                    print("下发的空数据 不做处理")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) 
+                    break
+                else:
+                    if len(recv_data) != 0:
+                        self.reconnect_count = 0
+                        self.resolver.handler(recv_data)
+                
+            except Exception as e:
+                print("cli break")
+                # EventMesh.publish("connect_iot_websocket_cb")
+                EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) 
+                break
+        self.iot_web_protoclo_recv_id =0
+
+    def iot_web_send_queue_close(self):
+        if self.iot_web_send_queue_id != 0:
+            if self.iot_web_send_queue.empty()!=True:
+                self.iot_web_send_queue.clear()
+            
+            try:
+                _thread.stop_thread(self.iot_web_send_queue_id)
+            except Exception as e:
+                print("iot_web_send_queue_close !!!!!!!!")
+        print("iot_web_send_queue_close 1111!!!!!!!!")
+        self.iot_web_send_queue_id = 0
+    
+    def iot_web_protoclo_recv_close_therad(self):
+        if self.iot_web_protoclo_recv_id != 0:
+            try:
+                _thread.stop_thread(self.iot_web_protoclo_recv_id)
+            except Exception as e:
+                print("self.iot_web_protoclo_recv_id !!!!!!!!")
+        self.iot_web_protoclo_recv_id = 0
+        if self.iot_protocol_client != None:
+            try:
+                print("关闭websocke")
+                self.iot_protocol_client.close()
+            except Exception as e:   
+                print("关闭websocke失败")
+        self.iot_protocol_client = None
+        
+
+        
+    def iot_close_web_therad(self):
+        try:
+            self.iot_web_send_queue_close()
+        except Exception as e:
+            print("iot_web_send_queue_close error!!!!!!!!")
+
+        try:
+            self.iot_web_protoclo_recv_close_therad()
+        except Exception as e:
+            print("iot_web_protoclo_recv_close_therad error!!!!!!!!")
+        self.start_voice_flag = 0
+        print("iot_close_web_therad")
+    
+    def get_iot_protoclo_client(self,event,msg):
+        return self.iot_protocol_client
+    def set_temp(self,event,data):
+        if self.temp == 0:
+            self.temp = 1
+        else:
+            self.temp = 0
+    def user_send(self,event,data):
+        # EventMesh.debug_log("TCP:send data:{}".format(msg))
+        # EventMesh.debug_log("TCP:send data:{}".format(msg))
+        # sim_state = sim.getStatus() 
+        if sim.getStatus() != 1:#网络不正常
+            print("无 sim卡")
+            return
+        # if self.temp == 1:
+        #     print("没信号")
+        else :
+            try:
+                if self.start_voice_flag == 0: #如果关闭了就需要重新再
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket}) 
+                    return
+                if data != None:
+                    if len(data) != 0:
+                        self.iot_web_send_queue.put(data)
+                    # EventMesh.debug_log("self.socket_send_queue.put:success:{}".format(len(data)))
+            except Exception as e:   
+                EventMesh.debug_log("self.socket_send_queue.put:ERROR:")
+                usys.print_exception(e) 
+                
+        return
+        sim_state = sim.getStatus() 
+        if sim_state != 1 or self.network_state == 0: #没sim,则不发送 ,没联网
+            return
+        else :
+            try:
+                if self.iot_protocol_client != None:
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.iot_protocol_send_cb,"data":data})
+                else:
+                    print("reconnect1 {}".format(data))
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})
+
+            except Exception as e:   
+                print("senc data err reconnect{}".format(data))
+                EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})
+                
+    def iot_protocol_send_cb(self,data):
+        try:
+            if data != None:
+                print("websocket connect success:{} self.iot_protocol_client:{}".format(data,self.iot_protocol_client))
+                if self.iot_protocol_client != None:
+                    self.iot_protocol_client.send(data)
+                    self.reconnect_count = 0
+                    print("websocket send success")
+                else:
+                    print("reconnect2 {}".format(data))
+                    # EventMesh.publish("connect_iot_websocket_cb")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})
+        except Exception as e:
+            print("发送失败 重新连接")
+            self.reconnect_count+=1
+            # EventMesh.publish("connect_iot_websocket_cb")
+            EventMesh.publish("publish_AI_btnInfo",{"func":self.connect_iot_websocket})
+   
+
+
+
+class ProtoCol(Abstract):
+    TYPE = ""
+    HEADER = "#@{}@#;"
+    SEPARATOR = ";"
+    END = ";" + chr(0x01)
+    #PID = "SZKW"
+    #PID = "qAELPTKmFanR4RHe"
+    md5_key = ''
+    PID = ''
+    btstate = 0
+    blestate = 0
+
+    def __init__(self):
+        pass
+
+    def initialization(self,*args):
+        EventMesh.subscribe(self.TYPE+"_S", self.handler)
+        EventMesh.subscribe(self.TYPE+"_R", self.handler)
+        pass
+
+    def handler(self, event, msg):
+        print("handler {}".format(event))
+        if event.endswith("_S"):
+            self.send(event, msg)
+        else:
+            self.recv(event, msg)
+
+    def send(self, event, msg):
+       
+        pass
+
+    def recv(self, event, msg):
+        pass
+#上报蓝牙连接状态
+class IOTWeb_post_bt_connect_restProtoCol(ProtoCol):
+    """post_bt_connect_rest"""
+    TYPE = "post_bt_connect_rest"
+
+    def send(self, event, msg):
+        # print("send post_bt_connect_rest{}".format(msg))
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print("send post_bt_connect_rest{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event: 上报蓝牙耳机连接状态
+        @param msg: 接收到的下行数据
+        """
+        print("recv post_bt_connect_rest{}".format(msg))
+#电子围栏触发SOS警告
+class IOTWeb_fence_sosProtoCol(ProtoCol):
+    """fence_sos"""
+    TYPE = "fence_sos"
+
+    def send(self, event, msg):
+        # print("send fence_sos{}".format(msg))
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        # 通过websocket发送到服务器端
+        fence_sos = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"fence_sos",
+                "rest":"success",
+                "msg":"收到紧急呼叫指令"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(fence_sos)
+        print("send fence_sos{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event: 电子围栏警告
+        @param msg: 接收到的下行数据
+        """
+        EventMesh.publish(WEB_CMD_ENUM.FENCE_SOS_S)
+        sos_list = EventMesh.publish("get_storage_config","sos_list")
+        if sos_list != None:
+            if len(sos_list) > 0:
+                get_sos_flag = EventMesh.publish("get_sos_flag") 
+                if get_sos_flag == 0:
+                    EventMesh.publish("set_sos_info_call")
+                pass
+        
+        print("recv fence_sos{}".format(msg))
+#上报心跳
+class IOTWeb_HRProtoCol(ProtoCol):
+    """post_heart_beat"""
+    TYPE = "post_heart_beat"
+    
+    def send(self, event, msg):
+        print("send hr{}".format(msg))
+        # 通过websocket发送到服务器端
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event: 心跳成功
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"post_rest",
+        #     "ts":1738283829,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"post_heart_beat",
+        #         "rest":"bind/unbind",
+        #         "msg":"上报成功/格式错误"
+        #     }
+        # }
+        print("recv hr{}".format(msg))
+        if "data" in msg:
+            data = msg["data"]
+            rest = data["rest"]
+            if rest == "unbind": #解绑需要开启蓝牙广播
+                self.btstate = 0
+                if self.blestate == 0:
+                    EventMesh.publish("set_storage_config",["switch_ble_state","1"])
+                    EventMesh.publish("send_ble_switch_toble","1")
+
+                    EventMesh.publish("set_storage_config",["switch_bt_state","1"])
+                    EventMesh.publish("send_bt_switch_toble","1")
+                    self.blestate=1
+
+            elif rest == "bind": #绑定需要关闭蓝牙广播
+                self.blestate =0
+                if self.btstate == 0:
+                    EventMesh.publish("set_storage_config",["switch_ble_state","0"])
+                    EventMesh.publish("send_ble_switch_toble","0")
+                    self.btstate=1
+
+#定位周期
+class IOTWeb_locatedurationProtoCol(ProtoCol):
+    """locate_duration"""
+    TYPE = "locate_duration"
+
+    def send(self, event, msg):
+        print("send locate_duration{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        locatio = {
+            "cmd":"dispatch_rest",
+            "ts":int(time_tmp),
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"locate_duration",
+                "rest":"success",
+                "msg":"收到定位周期指令"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(locatio)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:定位周期
+        @param msg: 接收到的下行数据
+        """
+        # locate_now = {
+        #     "cmd":"locate_duration",
+        #     "ts":1738283829,
+        #     "msg":"下发定位周期",
+        #     "data":300,0表示关
+        # }
+        # {"cmd":"locate_duration","msg":"下发定位周期","data":{"duration":300,"state":"1"},"ts":1756089226052,"targetDeviceId":"860132064633266"}
+        #location_time
+        location_time = {}
+        if "data" in msg:
+            data = msg["data"]
+            if "duration" in data:
+                locate_duration_time = int(data["duration"])
+                locate_duration_state = int(data["state"])
+                location_time["duration"]=locate_duration_time
+                location_time["state"]=locate_duration_state
+
+                EventMesh.publish("set_storage_config",["locate_duration_time",location_time])
+                EventMesh.publish("set_location_info",location_time)
+            locate_duration_c = EventMesh.publish("get_storage_config","locate_duration_time")
+            print("locate_duration_c:{}".format(locate_duration_c))
+        
+#上报SOS告警信息
+class IOTWeb_post_sosProtoCol(ProtoCol):
+    """post_sos"""
+    TYPE = "post_sos"
+
+    def send(self, event, msg):
+        print("send post_sos{}".format(msg))
+        # 通过websocket发送到服务器端
+       
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:msg
+        @param msg: 接收到的下行数据
+        """
+        # locate_now = {
+        #     "cmd":"locate_duration",
+        #     "ts":1738283829,
+        #     "msg":"下发定位周期",
+        #     "data":300,0表示关
+        # }
+        print("recv post_sos{}".format(msg))
+
+#上报设备状态    
+class IOTWeb_post_device_stateProtoCol(ProtoCol):
+    """post_device_state"""
+    TYPE = "post_device_state"
+
+    def send(self, event, msg):
+        print("send post_device_state{}".format(msg))
+        # 通过websocket发送到服务器端
+       
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:mpost_device_state
+        @param msg: 接收到的下行数据
+        """
+        # locate_now = {
+        #     "cmd":"locate_duration",
+        #     "ts":1738283829,
+        #     "msg":"下发定位周期",
+        #     "data":300,0表示关
+        # }
+        print("send post_device_state{}".format(msg))
+       
+#上报闹钟列表     
+class IOTWeb_post_alarmProtoCol(ProtoCol):
+    """post_alarm"""
+    TYPE = "post_alarm"
+
+    def send(self, event, msg):
+        print("send post_alarm{}".format(msg))
+        # 通过websocket发送到服务器端
+       
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print("send send_data{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:post_alarm
+        @param msg: 接收到的下行数据
+        """
+        print("recv post_alarm{}".format(msg))
+       
+#查找设备
+class IOTWeb_FindDeviceProtoCol(ProtoCol):
+    """find_device"""
+    TYPE = "find_device"
+
+    def send(self, event, msg):
+        # print("send locafind_devicetion{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        find_device = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"find_device",
+                "rest":"success",
+                "msg":"收到查找设备指令"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(find_device)
+        print("send_data find_device{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:定位信息下发
+        @param msg: 接收到的下行数据
+        """
+        alarm_TTS_str="我在这呢"
+        base_sentence = "{}".format(alarm_TTS_str)
+        repeat_sentence = (base_sentence + ",") * 3  # 重复5次并加逗号分隔
+        final_sentence = repeat_sentence.rstrip(",")
+        
+        EventMesh.publish("play_tts", [final_sentence, 1, "find_device"])
+        print("recv find_device{}".format(msg))
+        EventMesh.publish(WEB_CMD_ENUM.FIND_DEVCE_S_CMD)
+
+#立即定位
+class IOTWeb_LocateNowProtoCol(ProtoCol):
+    """locate_now"""
+    TYPE = "locate_now"
+    def send(self, event, msg):
+        print("send locate_now{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        locate_now = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"locate_now",
+                "rest":"success",
+                "msg":"收到立即定位指令"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(locate_now)
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:立即定位
+        @param msg: 接收到的下行数据
+        """
+        
+        EventMesh.publish(WEB_CMD_ENUM.LOCATION_NOW_S_CMD)
+        print("recv locate_now{}".format(msg))
+        EventMesh.publish(constants.EVENT.START_LOCATION,WEB_CMD_ENUM.POST_LOCATION_S_CMD) #开始定位 定位完之后上报定位数据
+
+
+#上报定位信息 
+class IOTWeb_post_locationProtoCol(ProtoCol):
+    """post_location"""
+    TYPE = "post_location"
+
+    def send(self, event, msg):
+        # 通过websocket发送到服务器端
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print("send_data post_location{}".format(msg))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:定位信息下发
+        @param msg: 接收到的下行数据
+        """
+        print("recv post_location{}".format(msg))
+#闹钟列表
+class IOTWeb_alarm_listProtoCol(ProtoCol):
+    """alarm_list"""
+    TYPE = "alarm_list"
+
+    def send(self, event, msg):
+        print("send alarm_list {}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        rest = "error"
+        if msg == 1:
+            rest = "success"
+        alarm_list ={
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"alarm_list",
+                "rest":rest,
+                "msg":"闹钟配置成功."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(alarm_list)
+        print("send_data alarm_list {}".format(msg))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发闹钟配置
+        @param msg: 接收到的下行数据
+        """
+        # 定义要添加的新元素
+        # new_item = {'time': '11:30','index': 1}
+        # data = [{"index":1,"name":"hihihi1"},{"index":2,"name":"hihihi2"},{"index":0,"name":"hihihi0"}]
+        # len_t = len(data)
+        # j = 0
+        # for i in range(len_t):
+        #     if data[j]['index'] == 1:
+        #         removed_item = data.pop(j)
+        #         print(data)
+        #         j=0
+        #         continue
+        #     j+=1
+        #     len_t = len(data)
+        #     print(len_t)
+        # for i in range(len(data)):
+        #     data[i]["index"] = i
+   
+        # url = http://220.160.104.34:30800/ws-bus/test/voice-record/876226211472149?state=1
+            # url = http://220.160.104.34:30800/ws-bus/test/876226211472149?testType = alarmList
+
+        alarm_list=[]
+        ret = 1
+        if "data" in msg:
+            data = msg["data"]
+            if isinstance(data,list) == True:
+                for i in range(len(data)):
+                    data_list = data[i]
+                    data_list["index"] = i
+                    print("data_list:{}".format(data_list))
+                    alarm_list.append(data_list)
+                alarm_list.sort(key=lambda x: x['index'])
+                EventMesh.publish("set_storage_config",["alarm_list",alarm_list])
+                EventMesh.publish("set_alarm_info",alarm_list)
+                alarm_list = EventMesh.publish("get_storage_config","alarm_list")
+                print("alarm_list:{}".format(alarm_list))
+            else:
+                ret = 0
+            
+        EventMesh.publish(WEB_CMD_ENUM.ALARM_LIST_S_CMD,ret)
+
+
+#闹钟配置下发
+
+class IOTWeb_alarm_configProtoCol(ProtoCol):
+    """alarm_config"""
+    TYPE = "alarm_config"
+
+    def send(self, event, msg):
+        # print("send alarm_config{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        alarm_config ={
+            "cmd":"dispatch_rest",
+            "ts":int(time_tmp),
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"alarm_config",
+                "rest":"success",
+                "msg":"闹钟配置成功."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(alarm_config)
+        print("send_data alarm_config{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发闹钟配置
+        @param msg: 接收到的下行数据
+        """
+        print("recv alarm_config{}".format(msg))
+        EventMesh.publish(WEB_CMD_ENUM.ALARM_CONFIG_S_CMD)
+
+#SOS联系人下发
+class IOTWeb_sos_listProtoCol(ProtoCol):
+    """sos_list"""
+    TYPE = "sos_list"
+
+    def send(self, event, msg):
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        sos_list ={
+            "cmd":"dispatch_rest",
+            "ts":int(time_tmp),
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"sos_list",
+                "rest":"success",
+                "msg":"收到SOS联系人列表."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(sos_list)
+        print("send_data sos_list{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发sos列表
+        @param msg: 接收到的下行数据
+        """
+        #{'msg': 'SOS', 'data': [{'name': '0', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678900', 'id': '1754031896067', 'deviceId': '876226211472149'}, 
+        # {'name': '1', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678901', 'id': '1754031896067', 'deviceId': '876226211472149'},
+        #  {'name': '2', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678902', 'id': '1754031896067', 'deviceId': '876226211472149'},
+        #  {'name': '3', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678903', 'id': '1754031896067', 'deviceId': '876226211472149'},
+        #  {'name': '4', 'createTime': 1754031896067, 'updateTime': 1754031896067, 'phone': '12345678904', 'id': '1754031896067', 'deviceId': '876226211472149'}],
+        #  'ts': 1754031896067, 'cmd': 'sos_list'}
+        print("recv sos_list{}".format(msg))
+        sos_list = []
+        if "data" in msg:
+            data = msg["data"]
+            if isinstance(data,list) == True:
+                sos_list_len = len(data)
+                if sos_list_len >= 3:
+                    sos_list_len = 3
+                for i in range(sos_list_len):
+                    save_data = {}
+                    # EventMesh.publish("set_storage_config",["SOS_NUMBER{}".format(i+1),data[i+1]])
+                    # sos_list.append(str(data[i+1]))
+                    save_data["index"] = i
+                    save_data["name"] = data[i]["name"]
+                    save_data["phone"] = data[i]["phone"]
+                    sos_list.append(save_data)
+                    # EventMesh.publish("set_storage_config",["sos_list",sos_list])
+            sos_list.sort(key=lambda x: x['index'])
+            EventMesh.publish("set_storage_config",["sos_list",sos_list])
+            EventMesh.publish("set_sos_list_info",sos_list)
+            read_soslist = EventMesh.publish("get_storage_config","sos_list")
+            print(read_soslist)
+            EventMesh.publish(WEB_CMD_ENUM.SOS_LIST_S_CMD)
+#NLP指令处理
+def ai_cmd_handle(cmd):
+    # print("")
+    if "查询" in cmd:#当指令中有查询时,截取查询操作
+        EventMesh.publish("find_cmd",cmd)
+    elif "讲话" in cmd:#当指令中有讲话时,截取语速操作
+        voice_start_index = cmd.index("讲话") + len("讲话")#动态判断截取部分
+        find = cmd[voice_start_index:]#查询截取
+        if  "快" in find:
+            EventMesh.publish("rate_move",1)
+            volume = "变快"
+        elif "慢" in find:
+            EventMesh.publish("rate_move",0)
+            volume = "变慢"
+        tts_play_str = "语速已经" + volume
+        EventMesh.publish("play_tts",[tts_play_str,0])
+    elif "声音改为" in cmd:#当指令中有声音改为时,进入音色修改
+        voice_start_index = cmd.index("声音改为") + len("声音改为")#动态判断截取部分
+        find = cmd[voice_start_index:]#查询截取
+        EventMesh.publish("change_voice",find)
+    elif "结束对话" in cmd:#当指令中有结束对话时,结束当前对话
+        EventMesh.publish("Close_VC")
+    elif "音量" in cmd:#当指令中有音量改为时,音量改为具体数值
+        if "调节" in cmd :
+            voice_start_index = cmd.index("音量调节:") + len("音量调节:")#动态判断截取部分
+            find = cmd[voice_start_index:]#查询截取
+            print("find = {}".format(find))
+            volume = int(find)
+            volume += 100#设置音量以100-200为参数
+            result = EventMesh.publish("change_volume",volume)
+            if result == 1:
+                tts_play_str = "音量已经改为" + find
+                EventMesh.publish("play_tts",[tts_play_str,0])
+            else:
+                tts_play_str = "设置失败,请重新设置0~100的音量大小"
+                EventMesh.publish("play_tts",[tts_play_str,0])
+        if "加大" in cmd:
+            result = EventMesh.publish("change_volume",1)
+            if result == 1:
+                tts_play_str = "音量已经加大"
+                EventMesh.publish("play_tts",[tts_play_str,0])
+            else:
+                tts_play_str = "当前音量已经是最大了"
+                EventMesh.publish("play_tts",[tts_play_str,0])
+        elif "减小" in cmd:
+            result = EventMesh.publish("change_volume",0)
+            if result == 1:
+                tts_play_str = "音量已经减小"
+                EventMesh.publish("play_tts",[tts_play_str,0])
+            else:
+                tts_play_str = "当前音量已经是最小了"
+                EventMesh.publish("play_tts",[tts_play_str,0])
+            
+    elif "语速倍速设置为:" in cmd:#修改对话模式
+        voice_start_index = cmd.index("语速倍速设置为:") + len("语速倍速设置为:")#动态判断截取部分
+        find = cmd[voice_start_index:]#查询截取
+        print("find = {}".format(find))
+        if float(find) > 2.0 or float(find) < 0.5:
+            tts_play_str = "抱歉,语速仅支持0.5-2倍,请重新设置"
+            EventMesh.publish("play_tts",[tts_play_str,0])
+            return
+        EventMesh.publish("SET_SPEECH_RATE",float(find))
+        tts_play_str = "语速已经改为" + find
+        EventMesh.publish("play_tts",[tts_play_str,0])
+    elif "聆听" in cmd:#聆听快/慢一些
+        if "快" in cmd:
+            tts_play_str = EventMesh.publish("NLP_set_vad_silence",1)
+        elif "慢" in cmd:
+            tts_play_str = EventMesh.publish("NLP_set_vad_silence",0)
+        EventMesh.publish("play_tts",[tts_play_str,0])
+    elif "这个不支持哦,请换一个" in cmd:
+        tts_play_str = "抱歉呀,暂不支持该方言,请换一个"
+        EventMesh.publish("play_tts",[tts_play_str,0])
+    elif "不支持的操作" in cmd:
+        tts_play_str = "抱歉呀,暂不支持该命令"
+        EventMesh.publish("play_tts",[tts_play_str,0])        
+
+    if not _NLP_EXTENDED_CMDS:
+        return
+    if "会议纪要" in cmd:
+        print("cmd:已进入会议")
+        EventMesh.publish("come_meet_mode")
+    elif "翻译" in cmd:
+        print("cmd:已进入翻译")
+        EventMesh.publish("come_trans_mode")
+    elif "查看历史留言" in cmd:
+        EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)
+        EventMesh.publish("set_history_voice_flag", 1)
+        EventMesh.publish("clean_unread_vocie")
+        EventMesh.publish("set_storage_config", ["is_noread_voice", 0])
+
+#NLP指令
+class IOTWeb_Nlp_ConfigProtoCol(ProtoCol):
+    """ai_cmd"""
+    TYPE = "ai_cmd"
+
+    def send(self, event, msg):
+        # print("ai_cmd{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        ai_cmd = {
+            "cmd":"ai_cmd",
+            "ts":time_tmp,
+            "msg":"AI指令",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(ai_cmd)
+        print("send_data ai_cmd{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发push_music
+        @param msg: 接收到的下行数据
+        """
+        #提取指令名称
+        cmdName = msg["data"]
+        print("提取到的AI指令为{}".format(cmdName))
+        ai_cmd_handle(cmdName)
+#设备进入翻译,小程序返回在线情况
+class IOTWeb_translate_device_enterProtoCol(ProtoCol):
+    """translate_device_enter"""
+    TYPE = "translate_device_enter"
+
+    def send(self, event, msg):
+        # print("translate_device_enter{}".format(msg))
+        # 通过websocket发送到服务器端
+        print("设备进入翻译,小程序返回在线情况")
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发translate_device_enter
+        @param msg: 接收到的下行数据
+        """
+        print("设备进入翻译获取状态:{}".format(msg["data"]["msg"]))
+        status = msg["data"]["rest"]
+        
+        if status == "success":
+           EventMesh.publish("set_trans_flag",1)#能进入AI翻译
+           exit_tts = "已进入实时翻译模式"
+           EventMesh.publish("play_tts",[exit_tts,1])
+        elif status == "error":
+           EventMesh.publish("set_trans_flag",0)#不能进入AI翻译
+           exit_tts = msg["data"]["msg"]
+           EventMesh.publish("play_tts",[exit_tts,0])
+        print("recv translate_device_enter")
+
+#接收小程序控制进入翻译
+class IOTWeb_come_transProtoCol(ProtoCol):
+    """translate_app_enter"""
+    TYPE = "translate_app_enter"
+
+    def send(self, event, msg):
+        # print("translate_app_enter{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        come_trans = {
+            "cmd":"translate_device_complete",
+            "ts":time_tmp,
+            "msg":"进入成功",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(come_trans)
+        print("send_data translate_app_enter{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发translate_app_enter
+        @param msg: 接收到的下行数据
+        """
+        EventMesh.publish("come_trans_mode")#进入AI翻译
+        print("recv translate_app_enter{}".format(msg))
+
+#接收小程序控制退出翻译
+class IOTWeb_exit_transProtoCol(ProtoCol):
+    """translate_app_exit"""
+    TYPE = "translate_app_exit"
+
+    def send(self, event, msg):
+        # print("translate_app_enter{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        come_trans = {
+            "msg": "退出翻译",
+            "data": None,
+            "cmd": "translate_device_exit",
+            "ts": time_tmp
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(come_trans)
+        print("send_data translate_app_enter{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发translate_app_enter
+        @param msg: 接收到的下行数据
+        """
+        EventMesh.publish("exit_trans_mode")#退出AI翻译
+        print("recv translate_app_exit{}".format(msg))
+
+#接收小程序控制进入会议
+class IOTWeb_appcontrol_meetProtoCol(ProtoCol):
+    """meet_control"""
+    TYPE = "meet_control"
+    def send(self, event, msg):
+        # print("translate_app_enter{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        come_trans = {
+            "msg": "开始会议",
+            "data": None,
+            "cmd": "meet_begin",
+            "ts": 1761121597
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(come_trans)
+        print("send_data translate_app_enter{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发meeting_app_enter
+        @param msg: 接收到的下行数据
+        """
+        data = msg["data"]#判断是开启还是关闭
+        if data == "begin":
+            EventMesh.publish("come_meet_mode")#进入会议纪要
+        elif data == "end":
+            EventMesh.publish("exit_meet_mode")#退出会议纪要
+        print("APP Meeting Control")
+
+#翻译播放TTS
+class IOTWeb_play_trans_TTSProtoCol(ProtoCol):
+    """tts_audio_url"""
+    TYPE = "tts_audio_url"
+
+    def send(self, event, msg):
+        # print("tts_audio_url{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        tts_audio_url = {
+            "cmd":"tts_audio_url_complete",
+            "ts":time_tmp,
+            "msg":"返回状态",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(tts_audio_url)
+        print("send_data tts_audio_url{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发push_music
+        @param msg: 接收到的下行数据
+        """
+        #提取录音播放名称
+        voiceUrl = msg["data"]
+        print("提取到的翻译路径为{}".format(voiceUrl))
+        EventMesh.publish("play_trans_tts",voiceUrl)
+        print("recv tts_audio_url{}".format(msg))
+
+#音乐播放
+class IOTWeb_play_MusicProtoCol(ProtoCol):
+    """push_music"""
+    TYPE = "push_music"
+
+    def send(self, event, msg):
+        # print("push_music{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        push_music = {
+            "cmd":"push_music",
+            "ts":time_tmp,
+            "msg":"音乐播放",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(push_music)
+        print("send_data push_music{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发push_music
+        @param msg: 接收到的下行数据
+        """
+        #提取音乐播放名称,地址
+        print("recv push_music{}".format(msg))
+        musicName = msg["data"]["musicName"]
+        musicUrl = msg["data"]["musicUrl"]
+        print("提取到的音乐名称为{},路径为{}".format(musicName,musicUrl))
+        EventMesh.publish("play_music",[musicName,musicUrl,1])
+        
+
+#音量下发
+class IOTWeb_volumeProtoCol(ProtoCol):
+    """volume"""
+    TYPE = "volume"
+
+    def send(self, event, msg):
+        print("send volume{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        volume = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"volume",
+                "rest":"success",
+                "msg":"收到音量设置."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(volume)
+        print("send_data volume{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发volume
+        @param msg: 接收到的下行数据
+        """
+        if "data" in msg:
+            r_volume = msg["data"]
+            print("recv volume{} type:{}".format(r_volume,type(msg)))
+            
+            EventMesh.publish("SET_TTS_VOLUME",int(r_volume))
+            EventMesh.publish("adjust_volume_toBle",r_volume)
+        EventMesh.publish(WEB_CMD_ENUM.VOLUME_S_CMD)
+#下发SOS配置
+class IOTWeb_sos_configProtoCol(ProtoCol):
+    """sos_config"""
+    TYPE = "sos_config"
+
+    def send(self, event, msg):
+        print("send sos_config{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        sos_config = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"sos_config",
+                "rest":"success",
+                "msg":"SOS配置成功."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(sos_config)
+        print("send_data sos_config{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发sos_config
+        @param msg: 接收到的下行数据
+        """
+        print("recv sos_config{}".format(msg))
+        if "data" in msg:
+            sos_config = msg["data"]
+            print("recv sos_config{} type:{}".format(sos_config,type(msg)))
+            EventMesh.publish("set_storage_config",["sos_config",sos_config])
+            EventMesh.publish("set_sos_config",sos_config)
+        sos_config = EventMesh.publish("get_storage_config","sos_config")
+        # print("sos_config:{} type:{}".format(sos_config,sos_config["enable110"]))
+        EventMesh.publish(WEB_CMD_ENUM.SOS_CONFIG_S_CMD)
+#闹钟铃声获取
+
+class IOTWeb_get_ringtoneProtoCol(ProtoCol):
+    """get_ringtone"""
+    TYPE = "get_ringtone"
+
+    def send(self, event, msg):
+        print("send get_ringtone{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        get_ringtone ={
+            "cmd":"get_ringtone",
+            "ts":time_tmp,
+            "msg":"请求下发铃声列表",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(get_ringtone)
+        print("send_data get_ringtone{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发get_ringtone
+        @param msg: 接收到的下行数据
+        """
+        print("recv get_ringtone{}".format(msg))
+
+#闹钟铃声列表
+class IOTWeb_ringtone_listProtoCol(ProtoCol):
+    """ringtone_list"""
+    TYPE = "ringtone_list"
+
+    def send(self, event, msg):
+        print("send ringtone_list{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        ringtone_list ={
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"ringtone_list",
+                "rest":"success",
+                "msg":"收到闹钟铃声列表."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(ringtone_list)
+        print("send_data ringtone_list{}".format(msg))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发ringtone_list
+        @param msg: 接收到的下行数据
+        """
+        print("recv ringtone_list{}".format(msg))
+
+
+#下发静默录音
+class IOTWeb_voice_recordProtoCol(ProtoCol):
+    """voice_record"""
+    TYPE = "voice_record"
+    start_ai = 0
+    def send(self, event, msg):
+        print("send voice_record{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        s_rest = "0"
+        s_msg = "停止录音."
+        if msg == "1":
+            print("设备允许录音")
+            s_rest = "1"
+            s_msg = "开始录音."
+        
+        voice_record = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"voice_record",
+                "rest":s_rest,
+                "msg":s_msg
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(voice_record)
+        print("send_data voice_record{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发voice_record
+        @param msg: 接收到的下行数据
+        """
+        print("recv voice_record{}".format(msg))
+        if "data" in msg:
+            data = msg["data"]
+            s_rest = "0"
+            if data == "0":
+                s_rest = "0"
+                
+                record_mode = EventMesh.publish("get_voice_record_mode")
+                if record_mode == None:
+                    record_mode = 0
+                if record_mode != 2:#语音留言不允许打断
+                    EventMesh.publish("stop_voice_record")
+                    print("停止录音")
+                EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)
+                
+            elif data == "1":
+                
+                get_sos_flag = EventMesh.publish("get_sos_flag") 
+                if get_sos_flag != 0:
+                    s_rest = "0"
+                    print("正在电话")
+                    EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)
+                    return
+                record_mode = EventMesh.publish("get_voice_record_mode")
+                if record_mode == None:
+                    record_mode = 0
+                get_VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")
+                if get_VC_satrt_flag == None:
+                    get_VC_satrt_flag = 0
+                
+                if record_mode == 2 or get_VC_satrt_flag == 1:# 如果在AI对话 或者语音留言不允许静默录音
+                    s_rest = "0"
+                    EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)
+                    EventMesh.publish("stop_voice_record")
+                else:
+                    s_rest = "1"
+                    EventMesh.publish(WEB_CMD_ENUM.VOICE_RECORD_S_CMD,s_rest)
+                    EventMesh.publish("start_voice_record",1)
+                print("开始录音")
+        
+#设备解绑
+class IOTWeb_unbind_devProtoCol(ProtoCol):
+    """unbind_dev"""
+    TYPE = "unbind_dev"
+    unbind_devtimer = osTimer()
+    init_flag = 0
+    def send(self, event, msg):
+        print("send unbind_dev{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        unbind_dev={
+            "cmd":"unbind_dev",
+            "ts":time_tmp,
+            "msg":"主动解绑设备",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(unbind_dev)
+        print("unbind_dev{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+        self.init_flag=1
+        self.unbind_devtimer.start(2000,0,self.unbind_devtimer_cb)
+    def unbind_devtimer_cb(self,*argc):
+        if self.init_flag == 1:
+            play_str = "设备解绑失败"
+            EventMesh.publish("play_tts",[play_str,1])
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发unbind_dev
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"post_rest",
+        #     "ts":1738283829,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"unbind_dev",
+        #         "rest":"success|error",
+        #         "msg":"收到主动解绑设备指令"
+        #     }
+        # }
+        self.btstate = 0
+        self.blestate = 0
+        self.unbind_devtimer.stop()
+        self.init_flag=0
+        if "data" in msg:
+            data = msg["data"]
+            rest = data["rest"]
+            if rest == "success":
+                play_str = "设备解绑成功"
+                EventMesh.publish("set_storage_config",["switch_ble_state","1"])
+                EventMesh.publish("send_ble_switch_toble","1")
+
+  
+                EventMesh.publish("set_storage_config",["switch_bt_state","1"])
+                EventMesh.publish("send_bt_switch_toble","1")
+                EventMesh.publish("play_tts",[play_str,1])
+                # ret = 0
+                # ret = ql_fs.path_exists("/usr/static/storage_file")#配置文件
+                # if ret == True:
+                #     ql_fs.rmdirs("/usr/static/storage_file")
+                #     ql_fs.mkdirs("/usr/static/storage_file")
+                # utime.sleep_ms(2000)
+                # Power.powerRestart()
+                pass
+            else:
+                play_str = "设备解绑失败"
+                EventMesh.publish("play_tts",[play_str,1])
+                pass
+        print("recv unbind_dev{}".format(msg))        
+#用户解绑
+class IOTWeb_user_unbind_devProtoCol(ProtoCol):
+    """user_unbind_dev"""
+    TYPE = "user_unbind_dev"
+    def send(self, event, msg):
+        print("send user_unbind_dev{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        user_unbind_dev={
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"user_unbind_dev",
+                "rest":"success",
+                "msg":"收到用户解绑设备指令"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(user_unbind_dev)
+        print("user_unbind_dev{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发user_unbind_dev
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"post_rest",
+        #     "ts":1738283829,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"user_unbind_dev",
+        #         "rest":"success|error",
+        #         "msg":"收到主动解绑设备指令"
+        #     }
+        # }
+        self.btstate = 0
+        self.blestate = 0
+        EventMesh.publish("set_storage_config",["switch_ble_state","1"]) #用户解绑设备需要开启广播 开启经典蓝牙
+        EventMesh.publish("send_ble_switch_toble","1")
+
+        EventMesh.publish("set_storage_config",["switch_bt_state","1"])
+        EventMesh.publish("send_bt_switch_toble","1")
+        EventMesh.publish(WEB_CMD_ENUM.USER_UNBIND_DEV_S)
+        tts_play_str = "设备已解绑"
+        EventMesh.publish("play_tts",[tts_play_str,1])
+        # ret = 0
+        # ret = ql_fs.path_exists("/usr/static/storage_file")#配置文件
+        # if ret == True:
+        #     ql_fs.rmdirs("/usr/static/storage_file")
+        #     ql_fs.mkdirs("/usr/static/storage_file")
+        # utime.sleep_ms(100)
+        # Power.powerRestart()
+        print("recv user_unbind_dev{}".format(msg))    
+#下发位子留言
+class IOTWeb_text_msgProtoCol(ProtoCol):
+    """text_msg"""
+    TYPE = "text_msg"
+
+    def send(self, event, msg):
+        print("send text_msg{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+
+        rest = "error"
+        if msg == 1:
+            rest = "success"
+        text_msg = {
+            "cmd":"post_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"text_msg",
+                "rest":rest,
+                "msg":"收到文本消息."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(text_msg)
+        print("send_data text_msg{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发voice_record
+        @param msg: 接收到的下行数据
+        """
+        print("recv time_tmp{}".format(msg))
+        recv_state = 0
+        if "data" in msg:
+            play_tts_text = msg["data"]
+            try:
+                EventMesh.publish("play_tts",[play_tts_text,1])
+                recv_state = 1
+            except Exception as e:
+                print("下发文字留言出错了")
+        EventMesh.publish(WEB_CMD_ENUM.TEXT_MSG_S_CMD,recv_state)
+        
+#AI对话音色配置
+class IOTWeb_chat_configProtoCol(ProtoCol):
+    """chat_config"""
+    TYPE = "chat_config"
+
+    def send(self, event, msg):
+        # print("send chat_config{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        chat_config = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"chat_config",
+                "rest":"success",
+                "msg":"收到AI对话配置"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(chat_config)
+        print("send_data chat_config{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发chat_config
+        @param msg: 接收到的下行数据
+        """
+        print("recv chat_config{}".format(msg))
+        # {
+        #     "cmd":"chat_config",
+        #     "ts":1738283829,
+        #     "msg":"下发AI对话配置",
+        #     "data":{
+        #         "speed":1.5,
+        #         "vadDuration":600,
+        #         "voiceId":"音色ID,火山音色或者科大讯飞音色ID"
+        #     }
+        # }
+        # {'msg': '测试聊天配置.', 'data': {'botName': '柒乐乐', 'vadDuration': 600, 'speed': '1.0', 'provider': 'iflytek', 'voiceId': 'x3_linlin'}, 
+        #  'ts': 1755600807553, 'cmd': 'chat_config'}
+
+        if "data" in msg:
+            data = msg["data"]
+            speed = None
+            try:
+                speed = float(data["speed"])
+            except Exception as e:
+                usys.print_exception(e)
+            vadDuration = data["vadDuration"]
+            voiceId = data["voiceId"]
+            provider = data["provider"]
+            botName = data["botName"]
+            asr =  data["asr"]
+            if speed !=  None:
+                    EventMesh.publish("SET_SPEECH_RATE",speed)
+            if vadDuration != None:
+                #if len(vadDuration) != 0:
+                    EventMesh.publish("set_vad_silence",vadDuration)
+            tts_type = {}
+            if voiceId != None and len(voiceId) != 0:
+                tts_type["voice"] = voiceId
+            if provider != None and len(provider) != 0:
+                tts_type["outbound_attributess_provider"] = provider
+            if botName != None and len(botName) != 0:
+                tts_type["bot_name"] = botName
+            if voiceId != None and len(voiceId) != 0:
+                tts_type["voice"] = voiceId
+            if asr != None and len(asr) != 0:
+                tts_type["inbound_attributess_provider"] = asr
+            if len(tts_type)>0:
+                print("tts_type:{}".format(tts_type))
+                EventMesh.publish("set_VC_TTS_voice_type",tts_type)
+        EventMesh.publish(WEB_CMD_ENUM.CHAT_CONFIG_S)
+#获取AI对话音色配置
+class IOTWeb_get_chat_configProtoCol(ProtoCol):
+    """get_chat_config"""
+    TYPE = "get_chat_config"
+
+    def send(self, event, msg):
+        # print("send chat_config{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        get_chat_config = {
+            "cmd":"get_chat_config",
+            "ts":time_tmp,
+            "msg":"获取AI对话配置",
+            "data":None
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(get_chat_config)
+        print("send_data chat_config{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发get_chat_config
+        @param msg: 接收到的下行数据
+        """
+        print("recv get_chat_config{}".format(msg))
+
+#上报日程列表     
+class IOTWeb_post_scheduleProtoCol(ProtoCol):
+    """post_schedule"""
+    TYPE = "post_schedule"
+
+    def send(self, event, msg):
+        print("send post_schedule{}".format(msg))
+        # 通过websocket发送到服务器端
+       
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print("send send_data{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:post_schedule
+        @param msg: 接收到的下行数据
+        """
+        print("recv post_schedule{}".format(msg))
+
+
+#下发日程    
+class IOTWeb_scheduleProtoCol(ProtoCol):
+    """schedule"""
+    TYPE = "schedule"
+
+    def send(self, event, msg):
+        print("send schedule{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        schedule = {
+            "cmd":"post_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"schedule",
+                "rest":"success",
+                "msg":"收到日程列表."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(schedule)
+        print("send_data schedule{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发schedule
+        @param msg: 接收到的下行数据
+        """
+        #         {
+        #     "cmd":"text_msg",
+        #     "ts":1738283829,
+        #     "msg":"下发文本消息",
+        #     "data":"文本内容"
+        # }
+        print("recv time_tmp{}".format(msg))
+        # {
+        #     "cmd":"schedule",
+        #     "ts":1738283829,
+        #     "msg":"下发日程列表",
+        #     "data":[
+        #         "title":"日程名称",
+        #         "description":"提醒内容",
+        #         "repeat":1~4播报重复次数,
+        #         "date":"日期,yyyy-MM-dd",
+        #         "时间":"HH:mm"
+        #     ]
+        # }
+        schedule = []
+        if "data" in msg:
+            data = msg["data"]
+            for i,sh in enumerate(data):
+                sh["index"] = i
+                schedule.append(sh)
+            schedule.sort(key=lambda x: x['index'])
+            EventMesh.publish("set_storage_config",["schedule",schedule])
+            
+            schedule = EventMesh.publish("get_storage_config","schedule")
+            EventMesh.publish("set_schedule_info",schedule)#
+            print(schedule)
+        EventMesh.publish(WEB_CMD_ENUM.SCHEDULE_S_CMD)
+#下发新的语音未读留言 
+class IOTWeb_new_voice_msgProtoCol(ProtoCol):
+    """new_voice_msg"""
+    TYPE = "new_voice_msg"
+
+    def send(self, event, msg):
+        print("send new_voice_msg{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        new_voice_msg = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"new_voice_msg",
+                "rest":"success",
+                "msg":"收到新语音消息指令."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(new_voice_msg)
+        print("send send_data{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+        EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)
+    def recv(self, event, msg):
+        """
+
+        @param event:new_voice_msg
+        @param msg: 接收到的下行数据
+        """
+        EventMesh.publish(WEB_CMD_ENUM.NEW_VOICE_MSG_S)
+
+        print("recv new_voice_msg{}".format(msg))
+
+#设备主动查询未读的语音消息列表
+class IOTWeb_find_new_voice_msgProtoCol(ProtoCol):
+    """find_voice_msg_list"""
+    TYPE = "find_voice_msg_list"
+    def __init__(self):
+        super().__init__()
+        self.history_board_flag = 0#第一次进去要广播
+        self.open_device = 0#开机播报未读留言
+        self.has_processed_history_voice = 0 #播完列表不许再拨“历史未读留言为空”
+        EventMesh.subscribe("set_history_board_flag",self.set_history_board_flag)
+        EventMesh.subscribe("set_open_device",self.set_open_device)
+        EventMesh.subscribe("get_open_device",self.get_open_device)
+    def send(self, event, msg):
+        print("send find_voice_msg_list")
+
+    def recv(self, event, msg):
+        """
+
+        @param event:find_voice_msg_list
+        @param msg: 接收到的下行数据
+        """
+        print("recv find_voice_msg_list{}".format(msg))
+
+        new_voice_list = []#字典列表
+        voice_list = msg["data"]
+        print("msg[\"data\"] = {}".format(voice_list))
+        if EventMesh.publish("get_history_voice_flag") == 1:
+            if len(voice_list) == 0:#为空
+                if EventMesh.publish("get_wait_voice_flag") == 0:#未听历史留言
+                    tts_play_str = "已无更多历史留言"
+                    EventMesh.publish("play_tts",[tts_play_str,0])
+                    self.history_board_flag = 0
+                    self.has_processed_wait_voice = 0
+                    self.wait_first_flag = 0
+                    # if self.has_processed_history_voice == 0:
+                    #     tts_play_str = "已无更多历史留言"
+                    #     EventMesh.publish("play_tts",[tts_play_str,0])
+                    # else:
+                    #     self.has_processed_history_voice = 0#读完了置为0
+                    #     self.history_board_flag = 0#广播置为0
+                    # self.history_board_flag = 0
+                    # self.has_processed_wait_voice = 0
+                    # self.wait_first_flag = 0
+                else:#等待历史留言
+                    self.has_processed_wait_voice = 0
+                    self.wait_first_flag = 0
+                EventMesh.publish("set_recv_voice",0) #设置留言播放状态
+                EventMesh.publish("set_history_voice_flag",0)
+                EventMesh.publish("set_wait_voice_flag",0)
+            else:#有历史留言
+                if EventMesh.publish("get_wait_voice_flag") == 0:#历史留言
+                    print("历史广播状态!!!!! = {}".format(self.history_board_flag))
+                    if self.history_board_flag == 0:
+                        tts_play_str = "您共有" + str(len(voice_list)) + "条历史留言,正在为您播放"
+                        EventMesh.publish("play_tts",[tts_play_str,1])
+                        EventMesh.publish("set_pwk_notice_flag",1)
+                        EventMesh.publish("set_pwk_cnt_voice",len(voice_list))#设置多,防止进入留言播完判断
+                        self.history_board_flag = 1
+                        self.has_processed_history_voice = 1
+                    for da in voice_list:
+                        new_voice_list.append(da)
+                    if len(new_voice_list) > 0:
+                        pass
+                else:#未读留言
+                    if self.open_device == 1 or EventMesh.publish("get_60S_voice_flag") == 1:
+                        EventMesh.publish("set_pwk_cnt_voice",len(voice_list))#设置未读次数,由于是开机一起查看的
+                        EventMesh.publish("set_60S_voice_flag",0)
+                    for da in voice_list:
+                        new_voice_list.append(da)
+                    if len(new_voice_list) > 0:
+                        pass
+                EventMesh.publish("set_play_new_voice_list",new_voice_list)
+                EventMesh.publish("play_new_voice_list")
+        else:#正常留言
+            if EventMesh.publish("get_1M_flag") == 0:
+                if EventMesh.publish("get_pwk_cnt_voice") >= 0:
+                    for da in voice_list:
+                        new_voice_list.append(da)
+                    if len(new_voice_list) > 0:
+                        pass
+                    EventMesh.publish("set_play_new_voice_list",new_voice_list)
+                    EventMesh.publish("play_new_voice_list")
+    def set_history_board_flag(self,event,msg):
+        self.history_board_flag = msg             
+    def set_open_device(self,event,msg):
+        self.open_device = msg
+    def get_open_device(self,event,msg):
+        return self.open_device
+#请求未读的语音消息列表
+class IOTWeb_get_new_voice_msgProtoCol(ProtoCol):
+    """get_new_voice_msg"""
+    TYPE = "get_new_voice_msg"
+
+    def send(self, event, msg):
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        # {
+        #     "cmd":"get_new_voice_msg",
+        #     "ts":1738283829,
+        #     "msg":"请求未读的语音消息列表",
+        #     "data":null
+        # }
+        get_new_voice_msg = {
+            "cmd":"get_new_voice_msg",
+            "ts":time_tmp,
+            "msg":"请求未读的语音消息列表",
+            "data":None
+        }
+       
+        send_data = b''
+        send_data = protocol_send_total_data(get_new_voice_msg)
+        print("send get_new_voice_msg{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:get_new_voice_msg
+        @param msg: 接收到的下行数据
+        """
+        #  get_new_voice_msg = {
+        #     "cmd":"post_rest",
+        #     "ts":time_tmp,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"get_new_voice_msg",
+        #         "rest":"success",
+        #         "msg":"收到请求新语音列表."
+        #     }
+        # }
+        print("recv get_new_voice_msg{}".format(msg))
+# 1播放 0暂停 2上一首 3下一首
+class IOTWeb_player_controlProtoCol(ProtoCol):
+    """player_control"""
+    TYPE = "player_control"
+
+    def send(self, event, msg):
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        paly_str = "停止"
+        paly_data = "stop"
+        if msg == 1:
+            paly_str = "播放"
+            paly_data = "play"
+            pass
+        elif msg == 0:
+            paly_str = "停止"
+            paly_data = "stop"
+            pass
+        elif msg == 2:
+            paly_str = "上一首"
+            paly_data = "pre"
+            pass
+        elif msg == 3:
+            paly_str = "下一首"
+            paly_data = "next"
+            pass
+        player_control = {
+            "cmd":"player_control",
+            "ts":time_tmp,
+            "msg":paly_str,
+            "data":paly_data
+        }
+       
+        send_data = b''
+        send_data = protocol_send_total_data(player_control)
+        print("send player_control{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",msg)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:get_new_voice_msg
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"post_rest",
+        #     "ts":1738283829,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"player_control",
+        #         "rest":"success|error",
+        #         "msg":"收到播放/停止/暂停指令/下一首/上一首"
+        #     }
+        # }
+        paly_state = 0
+        if "data" in msg:
+            data = msg["data"]
+            msg_str = data["msg"]
+            if msg_str.find("播放") != -1:
+                paly_state=1
+                pass
+            elif msg_str.find("停止") != -1:
+                paly_state=0
+                pass
+            elif msg_str.find("下一首") != -1:
+                paly_state = 3
+                pass
+            elif msg_str.find("上一首") != -1:
+                paly_state = 2
+                pass
+
+        EventMesh.publish(WEB_CMD_ENUM.PLAY_CONTROL_S,paly_state)
+        print("recv send_data{}".format(msg))
+class IOTWeb_new_voice_msg_listProtoCol(ProtoCol):
+    """new_voice_msg_list"""
+    TYPE = "new_voice_msg_list"
+    def __init__(self):
+        super().__init__()
+        self._1M_flag = 0#判断一分钟定时器是否开启中
+        self._1M_nowplay_flag = 0#判断当前留言播放是否结束去影响新留言播放的标志位
+        self.app_new_voice_flag = 0#app是否下发留言
+        self._1M_timer = osTimer()#多发留言间隙时间60S
+        self._60S_voice_flag = 0
+        self.is_listen_wait = 0#是否收听未听留言
+        self.close_timer_cnt = 0 #若两次未听定时器关闭
+        EventMesh.subscribe("get_app_new_voice_flag",self.get_app_new_voice_flag)
+        EventMesh.subscribe("set_app_new_voice_flag",self.set_app_new_voice_flag)
+        EventMesh.subscribe("get_1M_flag",self.get_1M_flag)
+        EventMesh.subscribe("set_1M_nowplay_flag",self.set_1M_nowplay_flag)
+        EventMesh.subscribe("set_is_listen_wait",self.set_is_listen_wait)
+        EventMesh.subscribe("get_60S_voice_flag",self.get_60S_voice_flag)
+        EventMesh.subscribe("set_60S_voice_flag",self.set_60S_voice_flag)
+        
+    def send(self, event, msg):
+        print("send new_voice_msg_list{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        new_voice_msg_list = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"new_voice_msg_list",
+                "rest":"success",
+                "msg":"收到新语音列表."
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(new_voice_msg_list)
+        print("send send_data{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:new_voice_msg_list
+        @param msg: 接收到的下行数据
+        """
+        self.app_new_voice_flag = 1#标志位为1
+        new_voice_list = []
+        if "data" in msg:
+            data = msg["data"]
+            for da in data:
+                new_voice_list.append(da)
+        if len(new_voice_list) > 0:
+            pass
+        EventMesh.publish("set_play_new_voice_list",new_voice_list)
+        print("是否收到留言:{},self._1M_flag = {}".format(EventMesh.publish("get_recv_voice"),self._1M_flag))
+        if EventMesh.publish("get_recv_voice") == 0 and self._1M_flag == 0 and self._1M_nowplay_flag == 0:#当没有音频在播放时,再播放新的,否则保存下来不播放,等60S后一起播放
+            EventMesh.publish("set_recv_voice",1) #设置留言播放状态
+            EventMesh.publish("play_new_voice_list")
+            self._1M_nowplay_flag == 1
+        else:#当有音频在播放时,存下来开始倒计时
+            pass
+            if self._1M_flag == 0 :
+                print("等待留言60S倒计时开启")
+                self._1M_timer.start(60000, 0, self._1M_timer_cb)
+                self._1M_flag = 1
+        EventMesh.publish(WEB_CMD_ENUM.NEW_VOICE_MSG_LIST_S)
+        print("recv new_voice_msg_list{}".format(msg))
+    def _1M_timer_cb(self,*argc):
+        EventMesh.publish("publish_AI_btnInfo",{"func":self._1M_timer_cb_}) 
+
+    def _1M_timer_cb_(self): 
+        #self.iot_web_send_queue.put("ping") 
+        self._1M_flag = 0
+        self._60S_voice_flag = 1
+        get_VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")
+        if get_VC_satrt_flag == None:
+            get_VC_satrt_flag = 0
+        if get_VC_satrt_flag == 0:
+            print("收到多条信息,统一播放")
+            EventMesh.publish("set_history_voice_flag",1)#修改历史留言标志位
+            EventMesh.publish("set_wait_voice_flag",1)#修改60S标志位
+            EventMesh.publish(WEB_CMD_ENUM.GET_NEW_VOICE_MSG_S)
+
+    def get_app_new_voice_flag(self,event,msg):
+        return self.app_new_voice_flag
+    def set_app_new_voice_flag(self,event,msg):
+        self.app_new_voice_flag = msg
+    def get_1M_flag(self,event,msg):
+        return self._1M_flag  
+    def get_60S_voice_flag(self,event,msg):
+        return self._60S_voice_flag
+    def set_60S_voice_flag(self,event,msg):
+        self._60S_voice_flag = msg
+    def set_1M_nowplay_flag(self,event,msg):
+        self._1M_nowplay_flag = msg
+    def set_is_listen_wait(self,event,msg):
+        self.is_listen_wait = msg
+class IOTWeb_put_voice_msg_stateProtoCol(ProtoCol):
+    """put_voice_msg_state"""
+    TYPE = "put_voice_msg_state"
+
+    def send(self, event, msg):
+        print("send put_voice_msg_state{}".format(msg))
+        # 通过websocket发送到服务器端
+       
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print("send send_data{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:put_voice_msg_state
+        @param msg: 接收到的下行数据
+        """
+        print("recv put_voice_msg_state{}".format(msg))
+#收到蓝牙连接记录获取请求
+class IOTWeb_get_bt_connect_record_listProtoCol(ProtoCol):
+    """get_bt_connect_record_list"""
+    TYPE = "get_bt_connect_record_list"
+
+    def send(self, event, msg):
+        print("send get_bt_connect_record_list{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        get_bt_connect_record_list = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"get_bt_connect_record_list",
+                "rest":"success",
+                "msg":"收到蓝牙连接记录获取请求"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(get_bt_connect_record_list)
+        print("send send_data{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND",send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:get_bt_connect_record_list
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"get_bt_connect_record_list",
+        #     "ts":1738283829,
+        #     "msg":"上报蓝牙连接列表",
+        #     "data": None
+        # }
+        EventMesh.publish("send_get_connect_bt_state")
+        EventMesh.publish(WEB_CMD_ENUM.GET_BT_CONNECT_RECORD_LIST_S)
+        print("recv get_bt_connect_record_list{}".format(msg))
+
+#蓝牙开关
+class IOTWeb_switch_bt_stateProtoCol(ProtoCol):
+    """switch_bt_state"""
+    TYPE = "switch_bt_state"
+
+    def send(self, event, msg):
+        # print("send switch_bt_state{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        #         {
+        #     "cmd":"dispatch_rest",
+        #     "ts":1738283829,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"switch_bt_state",
+        #         "rest":"success|error",
+        #         "msg":"收到蓝牙已打开/蓝牙已关闭指令"
+        #     }
+        # }
+        s_msg = "蓝牙已关闭指令"
+        if msg == "1":
+            s_msg = "收到蓝牙已打开"
+        elif msg == "0":
+            s_msg = "蓝牙已关闭指令"
+        switch_bt_state = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"switch_bt_state",
+                "rest":"success",
+                "msg":s_msg
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(switch_bt_state)
+        print("send_data switch_bt_state{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发switch_bt_state
+        @param msg: 接收到的下行数据
+        """
+        s_state = "1"
+        if "data" in msg:
+            data = msg["data"]
+            if data == "0":
+                s_state = "0"
+                EventMesh.publish("set_storage_config",["switch_bt_state","0"])
+                EventMesh.publish("send_bt_switch_toble","0")
+                pass
+            elif data == "1":
+                s_state = "1"
+                EventMesh.publish("set_storage_config",["switch_bt_state","1"])
+                EventMesh.publish("send_bt_switch_toble","1")
+                pass
+        print("recv switch_bt_state{}".format(msg))
+        
+        EventMesh.publish(WEB_CMD_ENUM.SWITCH_BT_STATE_S_CMD,s_state)
+
+#蓝牙扫描
+class IOTWeb_bt_scanProtoCol(ProtoCol):
+    """bt_scan"""
+    TYPE = "bt_scan"
+
+    def send(self, event, msg):
+        print("send schedule{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        s_msg = ""
+        if msg == "1":
+            s_msg = "开始扫描附近设备"
+        elif msg == "0":
+            s_msg = "停止扫描附近设备"
+        bt_scan = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"bt_scan",
+                "rest":"success",
+                "msg":s_msg
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(bt_scan)
+        print("send_data bt_scan{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发bt_scan
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"bt_scan",
+        #     "ts":1738283829,
+        #     "msg":"开始扫描附近设备/停止扫描附近设备",
+        #     "data":"0/1"
+        # }
+        s_ret = "1"
+        if "data" in msg:
+            data = msg["data"]
+            if data == "1":
+                s_ret = "1"
+                EventMesh.publish("send_ble_scan","1")
+            elif data == "0":
+                s_ret = "0"
+                EventMesh.publish("send_ble_scan","0")
+                
+            pass
+            
+        print("recv bt_scan{}".format(msg))
+        EventMesh.publish(WEB_CMD_ENUM.BT_SCAN_S_CMD,s_ret)
+
+#蓝牙当前状态
+class IOTWeb_get_bt_stateProtoCol(ProtoCol):
+    """get_bt_state"""
+    TYPE = "get_bt_state"
+
+    def send(self, event, msg):
+        print("send schedule{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        bt_state ={
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"get_bt_state",
+                "rest":msg,
+                "msg":"上报蓝牙开关状态"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(bt_state)
+        print("send_data bt_state{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发bt_state
+        @param msg: 接收到的下行数据
+        """
+        print("recv bt_state{}".format(msg))
+        switch_bt_state = EventMesh.publish("get_storage_config","switch_bt_state")
+        if switch_bt_state == None:
+            switch_bt_state = "1"
+        
+        EventMesh.publish(WEB_CMD_ENUM.GET_BT_STATE_S,str(switch_bt_state))
+
+#蓝牙扫描列表
+class IOTWeb_bt_scan_restProtoCol(ProtoCol):
+    """bt_scan_rest"""
+    TYPE = "bt_scan_rest"
+
+    def send(self, event, msg):
+        print("send bt_scan_rest{}".format(msg))
+        # 通过websocket发送到服务器端
+
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print(" bt_scan_rest{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发bt_scan_rest
+        @param msg: 接收到的下行数据
+        """
+        print("recv bt_scan_rest{}".format(msg))
+        
+
+#连接目标蓝牙设备
+class IOTWeb_connect_btProtoCol(ProtoCol):
+    """connect_bt"""
+    TYPE = "connect_bt"
+
+    def send(self, event, msg):
+        print("send connect_bt{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        s_ret = "success"
+        if msg == 0:
+            s_ret = "error"
+        connect_bt = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"connect_bt",
+                "rest":s_ret,
+                "msg":"收到蓝牙连接请求"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(connect_bt)
+        print("send_data bt_state{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发connect_bt
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"connect_bt",
+        #     "ts":1738283829,
+        #     "msg":"连接蓝牙设备",
+        #     "data":"目标设备mac地址"
+        # }
+        ok = 1
+        if "data" in msg:
+            mac = msg["data"]
+            # mac = "36:72:cc:e6:dd:e8"#deep 
+            # mac = "61:E8:6D:CD:7F:17" #Q36
+            # mac = "75:E8:8C:62:9A:DF" Fineblue Clip
+            if len(mac) == 17:
+                ok = 1
+                EventMesh.publish("send_connect_bt_mac",mac)
+            else:
+                ok = 0
+        EventMesh.publish(WEB_CMD_ENUM.CONNECT_BT_S,ok)
+        print("recv connect_bt{}".format(msg))
+#上报连接记录
+class IOTWeb_bt_connect_record_listProtoCol(ProtoCol):
+    """bt_connect_record_list"""
+    TYPE = "bt_connect_record_list"
+
+    def send(self, event, msg):
+        print("send bt_connect_record_list{}".format(msg))
+        # 通过websocket发送到服务器端
+        send_data = b''
+        send_data = protocol_send_total_data(msg)
+        print("send_data bt_state{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发bt_connect_record_list
+        @param msg: 接收到的下行数据
+        """
+        # {
+        #     "cmd":"post_rest",
+        #     "ts":1738283829,
+        #     "msg":"消息收到响应",
+        #     "data":{
+        #         "cmd":"bt_connect_record_list",
+        #         "rest":"success|error",
+        #         "msg":"收到蓝牙连接记录列表"
+        #     }
+        # }
+        print("recv bt_connect_record_list{}".format(msg))    
+#断开设备蓝牙连接
+class IOTWeb_disconnect_btProtoCol(ProtoCol):
+    """disconnect_bt"""
+    TYPE = "disconnect_bt"
+
+    def send(self, event, msg):
+        print("send disconnect_bt{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        disconnect_bt = {
+            "cmd":"dispatch_rest",
+            "ts":time_tmp,
+            "msg":"消息收到响应",
+            "data":{
+                "cmd":"disconnect_bt",
+                "rest":"success",
+                "msg":"断开蓝牙设备连接请求"
+            }
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(disconnect_bt)
+        print("send_data bt_state{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+
+        @param event:上行下发disconnect_bt
+        @param msg: 接收到的下行数据
+        """
+        #{
+        # 'msg': '断开设备蓝牙', 
+        # 'data': 'AB:12:34:56:78', 
+        # 'ts': 1754555005787, 
+        # 'cmd': 'disconnect_bt'}
+        if "data" in msg:
+            mac = msg["data"]
+            EventMesh.publish("send_disconnect_bt_mac",mac)
+        print("recv disconnect_bt{}".format(msg))
+        EventMesh.publish(WEB_CMD_ENUM.DISCONNECT_BT_S)
+class IOTWeb_find_deviceProtoCol(ProtoCol):
+    """find_device"""
+    TYPE = "find_device"
+
+    def send(self, event, msg):
+        # print("c{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        come_trans = {
+            "msg": "查找设备",
+            "data": None,
+            "cmd": "find_device",
+            "ts": time_tmp
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(come_trans)
+        print("send_data find_device{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:上行下发translate_app_enter
+        @param msg: 接收到的下行数据
+        """
+        tts_str = "我在这呀,我在这呀,我在这呀 "
+        EventMesh.publish("play_tts", [tts_str, 1, "find_device"])
+
+#接收小程序控制上传设备状态
+class IOTWeb_get_device_stateProtoCol(ProtoCol):
+    """get_device_state"""
+    TYPE = "get_device_state"
+
+    def send(self, event, msg):
+        battery_state = (EventMesh.publish("screen_get_battery"))[0]
+        battery = (EventMesh.publish("screen_get_battery"))[1]
+        if battery >= 100 or battery == None:
+            battery = 100
+        VC_TTS_volume =  EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置
+        if VC_TTS_volume == None:
+            VC_TTS_volume =100
+        Timeversion = version_util.get_unified_version()
+        ts = utime.mktime(utime.localtime())
+        come_trans = {
+            "data":{
+                    "version":Timeversion,#版本号
+                    "charge":battery_state,#0表示未充电,1表示充电中
+                    "currentRoleName":"",#当前音色的name
+                    "volume":str(VC_TTS_volume),#百分比数值
+                    "wifiRssi":str(net.csqQueryPoll()//6),#wifi强弱:0,1,2,3,4,5。转成这几个梯度
+                    "battery":str(battery),#电量
+                    "bargein": "0",#不能手动设置打断模式
+                    "deviceId":modem.getDevImei(),#设备号
+                },
+            "cmd":"device_info",
+            "msg": "上报设备信息",
+            "ts": ts
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(come_trans)
+        print("send_data get_device_state{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:get_device_state
+        @param msg: 接收到的下行数据
+        """
+        EventMesh.publish(WEB_CMD_ENUM.GET_DEVICE_STATE_S)
+
+#接收小程序控制设备音量
+class IOTWeb_switch_volumeProtoCol(ProtoCol):
+    """switch_volume"""
+    TYPE = "switch_volume"
+
+    def send(self, event, msg):
+        # print("c{}".format(msg))
+        # 通过websocket发送到服务器端
+        date_tmp = utime.localtime()
+        time_tmp = utime.mktime(date_tmp)
+        come_trans = {
+            "msg": "设置音量",
+            "data": None,
+            "cmd": "switch_volume",
+            "ts": time_tmp
+        }
+        send_data = b''
+        send_data = protocol_send_total_data(come_trans)
+        print("send_data switch_volume{}".format(send_data))
+        EventMesh.publish("CHANNEL_IOT_WEB_SEND", send_data)
+
+    def recv(self, event, msg):
+        """
+        @param event:switch_volume
+        @param msg: 接收到的下行数据
+        """
+        volume = int(msg["data"]["target"]) + 100
+        print("音量修改为:{}".format(volume-100))
+        EventMesh.publish("change_volume",volume)
+
+
+class ProtoColResolver(object):
+    def __init__(self):
+        self.protocol_list = []
+
+    def add_support(self, prot):
+        self.protocol_list.append(prot)
+        prot.initialization()
+
+    def handler(self, data):
+        # 这里接收数据
+        
+        EventMesh.debug_log("handler -> {}".format(len(data)))
+        # if data.startswith('Init;'):
+        #     data = data[5:]
+        #     EventMesh.publish(constants.EVENT.SET_KEY, {"value": ujson.loads(data)['KEY']})
+            
+        # elif data.startswith("Update;"):
+        #     data = data[7:]
+        #     EventMesh.publish(constants.PROTOCOL.H04_RECV, data)
+        try:
+            if len(data) > 0: 
+                playpolad_data(data)
+                # EventMesh.publish(ProtoCol_type, data) #返回协议read
+        except Exception as e:
+            EventMesh.debug_log("handler:ERROR:")
+            usys.print_exception(e)
+            EventMesh.debug_log("handler:error data:{}".format(len(data)))
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/test/tts_local_mp3_concurrent_test.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/test/tts_local_mp3_concurrent_test.py
new file mode 100644 (file)
index 0000000..44ade20
--- /dev/null
@@ -0,0 +1,372 @@
+# -*- coding: UTF-8 -*-\r
+"""\r
+TTS(PCM 流) + 本地 MP3 并行播放自测\r
+\r
+模拟 VC 长对话场景:\r
+  - 服务端 TTS:audio.Audio.PCM + stream_write(16kHz PCM)\r
+  - 本地短音:audio.Audio(0).play()(如 think.mp3 / start.mp3)\r
+\r
+用法(设备 REPL 或脚本入口):\r
+  import tts_local_mp3_concurrent_test\r
+  tts_local_mp3_concurrent_test.run_all()\r
+\r
+或:\r
+  exec(open('.../tts_local_mp3_concurrent_test.py').read())\r
+  run_all()\r
+\r
+烧录前请确认本地 MP3 路径存在(任选其一):\r
+  U:/static/think.mp3、U:/static/start.mp3、/usr/tesr.mp3\r
+可选 PCM 文件:/usr/tts_tesr_01.wav(标准 44 字节头 16kHz mono 16bit)\r
+无文件时使用内置生成的测试音调 PCM。\r
+"""\r
+\r
+import audio\r
+import utime\r
+import usys\r
+import _thread\r
+\r
+try:\r
+    import uos\r
+except ImportError:\r
+    uos = None\r
+\r
+try:\r
+    from machine import Pin\r
+except ImportError:\r
+    Pin = None\r
+\r
+# ---- 与 vc_session 一致 ----\r
+SAMPLE_RATE = 16000\r
+CHANNELS = 1\r
+PCM_CHUNK_MS = 40\r
+TEST_DURATION_MS = 4000\r
+LOCAL_MP3_CANDIDATES = (\r
+    "U:/static/think.mp3",\r
+    "U:/static/start.mp3",\r
+    "/usr/tesr.mp3",\r
+    "/usr/tts_tesr.mp3",\r
+)\r
+PCM_WAV_CANDIDATES = (\r
+    "/usr/tts_tesr_01.wav",\r
+    "/usr/tts_tesr.wav",\r
+)\r
+\r
+_play_pcm = None\r
+_aud = None\r
+_local_cb_events = []\r
+_pcm_errors = []\r
+_feed_done = 0\r
+_test_running = 0\r
+\r
+\r
+def _log(msg):\r
+    try:\r
+        print("[tts+mp3-test] {}".format(msg))\r
+    except Exception:\r
+        pass\r
+\r
+\r
+def _file_exists(path):\r
+    if not path or uos is None:\r
+        return False\r
+    try:\r
+        uos.stat(path)\r
+        return True\r
+    except Exception:\r
+        return False\r
+\r
+\r
+def _pick_first_existing(paths):\r
+    for p in paths:\r
+        if _file_exists(p):\r
+            return p\r
+    return None\r
+\r
+\r
+def _local_audio_cb(event):\r
+    try:\r
+        _local_cb_events.append(event)\r
+        if event == 0:\r
+            _log("local mp3: audio-play start")\r
+        elif event == 7:\r
+            _log("local mp3: audio-play finish")\r
+        else:\r
+            _log("local mp3: cb event={}".format(event))\r
+    except Exception:\r
+        pass\r
+\r
+\r
+def _init_audio():\r
+    global _aud\r
+    _aud = audio.Audio(0)\r
+    try:\r
+        if Pin is not None:\r
+            _aud.set_pa(Pin.GPIO29)\r
+    except Exception:\r
+        pass\r
+    try:\r
+        _aud.setCallback(_local_audio_cb)\r
+    except Exception:\r
+        pass\r
+    try:\r
+        _aud.setVolume(10)\r
+    except Exception:\r
+        pass\r
+\r
+\r
+def _as_pcm_write_buf(data):\r
+    """与 vc_session_media 一致:stream_write 需要 bytearray(bytes 切片在部分 MPY 上报错)。"""\r
+    if not data:\r
+        return bytearray()\r
+    if isinstance(data, bytearray):\r
+        return data\r
+    try:\r
+        return bytearray(data)\r
+    except Exception:\r
+        try:\r
+            return bytearray(memoryview(data))\r
+        except Exception:\r
+            return bytearray()\r
+\r
+\r
+def _pcm_stream_write(pcm, piece):\r
+    buf = _as_pcm_write_buf(piece)\r
+    if not buf:\r
+        return 0\r
+    return pcm.stream_write(buf, len(buf))\r
+\r
+\r
+def _pcm_gen_chunk(ms=PCM_CHUNK_MS, freq=523):\r
+    """生成 16bit mono PCM 块(无文件时的 TTS 模拟数据)。"""\r
+    import math\r
+    n = SAMPLE_RATE * ms // 1000\r
+    if n < 1:\r
+        n = 1\r
+    buf = bytearray(n * 2)\r
+    phase = getattr(_pcm_gen_chunk, "_phase", 0)\r
+    for i in range(n):\r
+        t = (phase + i) / float(SAMPLE_RATE)\r
+        v = int(2800 * math.sin(2.0 * 3.1415926 * freq * t))\r
+        if v > 32767:\r
+            v = 32767\r
+        if v < -32768:\r
+            v = -32768\r
+        buf[2 * i] = v & 0xFF\r
+        buf[2 * i + 1] = (v >> 8) & 0xFF\r
+    _pcm_gen_chunk._phase = phase + n\r
+    return buf\r
+\r
+\r
+def _read_wav_pcm(path, max_bytes=0):\r
+    """读取 WAV 裸 PCM(仅支持标准 44 字节头、16kHz/16bit/mono)。"""\r
+    with open(path, "rb") as f:\r
+        hdr = f.read(44)\r
+        if len(hdr) < 44 or hdr[0:4] != b"RIFF" or hdr[8:12] != b"WAVE":\r
+            raise ValueError("not wav")\r
+        data = f.read() if not max_bytes else f.read(max_bytes)\r
+    return bytearray(data) if data else bytearray()\r
+\r
+\r
+def _pcm_open():\r
+    """对齐 VC_Play_Start:打开 PCM 下行流。"""\r
+    global _play_pcm\r
+    _play_pcm = None\r
+    utime.sleep_ms(50)\r
+    last_err = None\r
+    for i in range(3):\r
+        try:\r
+            if i:\r
+                utime.sleep_ms(60 + i * 40)\r
+            _play_pcm = audio.Audio.PCM(0, 1, SAMPLE_RATE, 1, 1, 2)\r
+            ret = _play_pcm.stream_write_start()\r
+            _log("PCM stream_write_start ret={} try={}".format(ret, i))\r
+            return ret\r
+        except Exception as e:\r
+            last_err = e\r
+            try:\r
+                if _play_pcm is not None:\r
+                    _play_pcm.close()\r
+            except Exception:\r
+                pass\r
+            _play_pcm = None\r
+    if last_err:\r
+        raise last_err\r
+    return -1\r
+\r
+\r
+def _pcm_close():\r
+    global _play_pcm\r
+    try:\r
+        if _play_pcm is not None:\r
+            try:\r
+                _play_pcm.stream_stop()\r
+            except Exception:\r
+                pass\r
+            try:\r
+                _play_pcm.close()\r
+            except Exception:\r
+                pass\r
+    except Exception as e:\r
+        _log("PCM close err: {}".format(e))\r
+    _play_pcm = None\r
+\r
+\r
+def _tts_feed_thread(duration_ms, wav_path=None):\r
+    """后台线程:向 PCM 流持续写入,模拟 TTS 下行。"""\r
+    global _feed_done, _pcm_errors\r
+    _feed_done = 0\r
+    end_ms = utime.ticks_add(utime.ticks_ms(), duration_ms)\r
+    pcm_data = None\r
+    pcm_off = 0\r
+    if wav_path:\r
+        try:\r
+            pcm_data = _read_wav_pcm(wav_path)\r
+            _log("TTS feed use wav pcm bytes={}".format(len(pcm_data)))\r
+        except Exception as e:\r
+            _log("read wav fail {} -> synth tone".format(e))\r
+            pcm_data = None\r
+    chunk_samples = SAMPLE_RATE * PCM_CHUNK_MS // 1000\r
+    chunk_bytes = chunk_samples * 2\r
+    wrote = 0\r
+    while _test_running and utime.ticks_diff(end_ms, utime.ticks_ms()) > 0:\r
+        try:\r
+            if _play_pcm is None:\r
+                break\r
+            if pcm_data:\r
+                if pcm_off >= len(pcm_data):\r
+                    pcm_off = 0\r
+                piece = pcm_data[pcm_off : pcm_off + chunk_bytes]\r
+                pcm_off += len(piece)\r
+                if not piece:\r
+                    piece = _pcm_gen_chunk()\r
+            else:\r
+                piece = _pcm_gen_chunk()\r
+            ret = _pcm_stream_write(_play_pcm, piece)\r
+            if ret is None or ret < 0:\r
+                _pcm_errors.append("stream_write ret={}".format(ret))\r
+            else:\r
+                wrote += ret\r
+        except Exception as e:\r
+            _pcm_errors.append(str(e))\r
+            usys.print_exception(e)\r
+            break\r
+        utime.sleep_ms(PCM_CHUNK_MS // 2)\r
+    _feed_done = 1\r
+    _log("TTS feed thread exit wrote={} errors={}".format(wrote, len(_pcm_errors)))\r
+\r
+\r
+def _local_mp3_play(path):\r
+    """对齐 _vc_local_play:aud.play 播本地 MP3,不停 PCM。"""\r
+    _log("local aud.play path={}".format(path))\r
+    try:\r
+        _aud.play(1, 1, path)\r
+        return True\r
+    except Exception as e:\r
+        _log("local aud.play FAIL: {}".format(e))\r
+        usys.print_exception(e)\r
+        return False\r
+\r
+\r
+def _stop_all():\r
+    global _test_running\r
+    _test_running = 0\r
+    utime.sleep_ms(80)\r
+    try:\r
+        _aud.stopPlayStream()\r
+    except Exception:\r
+        pass\r
+    try:\r
+        _aud.stopAll()\r
+    except Exception:\r
+        pass\r
+    _pcm_close()\r
+    utime.sleep_ms(100)\r
+\r
+\r
+def scenario_sequential(local_path, wav_path=None):\r
+    """先 TTS 再本地 MP3,作基线。"""\r
+    global _test_running, _pcm_errors, _local_cb_events\r
+    _pcm_errors = []\r
+    _local_cb_events = []\r
+    _log("=== sequential: TTS then local mp3 ===")\r
+    _test_running = 1\r
+    try:\r
+        _pcm_open()\r
+        _tts_feed_thread(TEST_DURATION_MS, wav_path)\r
+        utime.sleep_ms(TEST_DURATION_MS + 200)\r
+        _pcm_close()\r
+        utime.sleep_ms(300)\r
+        _local_mp3_play(local_path)\r
+        utime.sleep_ms(2500)\r
+    finally:\r
+        _stop_all()\r
+    _log("sequential done local_cb={} pcm_err={}".format(\r
+        _local_cb_events, len(_pcm_errors)))\r
+\r
+\r
+def scenario_concurrent(local_path, wav_path=None, overlap_delay_ms=200):\r
+    """\r
+    TTS PCM 与本地 MP3 并行(模拟长对话边播 TTS 边 think/start 短音)。\r
+    overlap_delay_ms:开 PCM 后延迟多久播本地 MP3。\r
+    """\r
+    global _test_running, _pcm_errors, _local_cb_events\r
+    _pcm_errors = []\r
+    _local_cb_events = []\r
+    _log("=== concurrent: TTS + local mp3 overlap_delay={}ms ===".format(\r
+        overlap_delay_ms))\r
+    _test_running = 1\r
+    tid = None\r
+    try:\r
+        _pcm_open()\r
+        try:\r
+            _thread.stack_size(12 * 1024)\r
+        except Exception:\r
+            pass\r
+        tid = _thread.start_new_thread(\r
+            _tts_feed_thread, (TEST_DURATION_MS + 1500, wav_path))\r
+        utime.sleep_ms(overlap_delay_ms)\r
+        ok_local = _local_mp3_play(local_path)\r
+        wait_ms = TEST_DURATION_MS + 2000\r
+        t0 = utime.ticks_ms()\r
+        while utime.ticks_diff(utime.ticks_ms(), t0) < wait_ms:\r
+            if _feed_done:\r
+                break\r
+            utime.sleep_ms(50)\r
+        _log("concurrent local_ok={} pcm_errors={} local_cb={}".format(\r
+            ok_local, _pcm_errors, _local_cb_events))\r
+    except Exception as e:\r
+        _log("concurrent FAIL: {}".format(e))\r
+        usys.print_exception(e)\r
+    finally:\r
+        _test_running = 0\r
+        _stop_all()\r
+    return len(_pcm_errors) == 0\r
+\r
+\r
+def run_all():\r
+    _log("start")\r
+    _init_audio()\r
+    local_path = _pick_first_existing(LOCAL_MP3_CANDIDATES)\r
+    wav_path = _pick_first_existing(PCM_WAV_CANDIDATES)\r
+    if not local_path:\r
+        _log("ERROR: no local mp3 found, candidates={}".format(LOCAL_MP3_CANDIDATES))\r
+        return\r
+    _log("local_mp3={} wav_pcm={}".format(local_path, wav_path or "(synth)"))\r
+    try:\r
+        scenario_sequential(local_path, wav_path)\r
+        utime.sleep_ms(800)\r
+        ok = scenario_concurrent(local_path, wav_path, overlap_delay_ms=150)\r
+        if ok:\r
+            _log("RESULT: concurrent OK (no pcm stream_write errors)")\r
+        else:\r
+            _log("RESULT: concurrent HAS errors, see pcm_errors above")\r
+    except Exception as e:\r
+        _log("run_all exception")\r
+        usys.print_exception(e)\r
+    finally:\r
+        _stop_all()\r
+    _log("end")\r
+\r
+\r
+if __name__ == "__main__":\r
+    run_all()\r
index 4d9d34a..1f4ab88 100644 (file)
-# -*- coding: utf-8 -*-\r
-"""按键、电源、USB、按键队列。"""\r
-from common import Abstract\r
-try:\r
-    from qp_log import qp_print as print\r
-except Exception:\r
-    pass\r
-import EventMesh\r
-import usys\r
-import utime\r
-import audio\r
-import osTimer\r
-import dataCall\r
-import sim\r
-from machine import Pin, ExtInt\r
-from machine import WDT\r
-import modem\r
-import ntptime\r
-import _thread\r
-from queue import Queue\r
-from misc import Power, USB, PowerKey\r
-import constants\r
-try:\r
-    import vc_config\r
-except Exception:\r
-    vc_config = None\r
-from vc_common import dev_type\r
-from vc_hw import set_ipv4_datacall, _BatteryCfg\r
-from vc_uart import AI_UART\r
-\r
-audio_buf = ['chaobingfatis', 'chenggong', 'didianguanji', 'IMEIhao', 'jianquancuowu', 'key_prompt', 'liuliangtishi', 'Low_power_warning', 'R_CHARGE_FULL', 'R_CHARGE_IN', 'R_CHARGE_OUT', 'R_READ_TIME_0', 'R_READ_TIME_1', 'R_READ_TIME_2', 'R_READ_TIME_3', 'R_READ_TIME_4', 'R_READ_TIME_5', 'R_READ_TIME_6', 'R_READ_TIME_7', 'R_READ_TIME_8', 'R_READ_TIME_9', 'R_READ_TIME_A', 'R_READ_TIME_B', 'R_READ_TIME_C', 'R_READ_TIME_D', 'R_READ_TIME_E', 'R_READ_TIME_F', 'R_READ_TIME_G', 'R_READ_TIME_H', 'R_READ_TIME_I', 'R_READ_TIME_J', 'R_READ_TIME_K', 'R_READ_TIME_L', 'R_READ_TIME_M', 'R_READ_TIME_N', 'R_READ_TIME_O', 'R_READ_TIME_P', 'R_READ_TIME_Q', 'R_READ_TIME_R', 'R_READ_TIME_S', 'R_READ_TIME_T', 'R_READ_TIME_U', 'R_READ_TIME_V', 'R_READ_TIME_W', 'R_READ_TIME_X', 'R_READ_TIME_Y', 'R_READ_TIME_Z', 'send_tone', 'shouhou', 'xinhaoruo1', 'zhengzaiguanji']\r
-class AI_dev(Abstract):\r
-    # AI\r
-    def __init__(self):\r
-        # if(dev_type != "7PIN"):\r
-        #     set_ipv4_datacall()\r
-        self.is_wdt = 0 #是否开启看门狗\r
-        #AT+QDAI=1,0,0,1,0,0,1,1PCM音频输出设置为主模式、短同步、16 kHz采样率和256 kHz时钟频率,采样位深为16bit,使用的时间间隙数量为1,并且使用第一个时间间隙。\r
-        try:\r
-            bt_pm_en = False\r
-            pin_bt_on = None\r
-            pin_mot = None\r
-            qdai_cfg = None\r
-            if "vc_config" in globals() and vc_config:\r
-                bt_pm_en = bool(vc_config.get_attr("BT_POWER_MOTOR_ENABLED", False))\r
-                pin_bt_on = vc_config.get_attr("PIN_BT_ON_GPIO", None)\r
-                pin_mot = vc_config.get_attr("PIN_MOT_GPIO", None)\r
-                qdai_cfg = vc_config.get_attr("AUDIO_QDAI_CFG", None)\r
-        except Exception:\r
-            bt_pm_en = False\r
-            pin_bt_on = None\r
-            pin_mot = None\r
-            qdai_cfg = None\r
-\r
-        if bt_pm_en:\r
-            try:\r
-                if pin_bt_on:\r
-                    self.PIN_BT_ON = Pin(getattr(Pin, pin_bt_on), Pin.OUT, Pin.PULL_DISABLE, 0)\r
-                if pin_mot:\r
-                    self.PIN_MOT = Pin(getattr(Pin, pin_mot), Pin.OUT, Pin.PULL_DISABLE, 1)\r
-            except Exception:\r
-                pass\r
-            try:\r
-                utime.sleep_ms(200)\r
-                if hasattr(self, "PIN_MOT"):\r
-                    self.PIN_MOT.write(0)\r
-                if hasattr(self, "PIN_BT_ON"):\r
-                    self.PIN_BT_ON.write(1)\r
-            except Exception:\r
-                pass\r
-            try:\r
-                if isinstance(qdai_cfg, (tuple, list)) and len(qdai_cfg) == 8:\r
-                    audio.set_qdai_cfg(*qdai_cfg)  # 用iis做音频输入输出\r
-            except Exception:\r
-                pass\r
-        self.KeyAI_status = 0\r
-        self.KeyAI_timer_count = 0\r
-        self.KeyAI_long_press = 0\r
-        self.KeySos_long_press = 0\r
-        self.KeyAI_press = 0\r
-        self.last_ai_key_time = 0#让ai按键不要频繁调用\r
-        if self.is_wdt == 1:\r
-            self.wdt = WDT(30) #开启看门狗\r
-        # self.wdt_timer = osTimer()\r
-        # self.wdt_timer.start(5000, 1, self.wdt_timer_cb)\r
-        \r
-        count = 0\r
-        while True:\r
-            count += 1\r
-            if count > 20 :\r
-                break\r
-            data = dataCall.getInfo(1, 0)\r
-            print("data call INFO {}".format(data))\r
-            if data[2][0]:\r
-                self.network_state = 1\r
-                break\r
-            print("等待网络 {}...".format(count))\r
-            utime.sleep_ms(500)\r
-        self.KeyAI_timer = osTimer()\r
-        self.KeySOS_timer = osTimer()\r
-        self.play_imei_timer = osTimer()\r
-        self.play_imei_timer_count=0\r
-        self.KeySOS_timer_count = 0\r
-        self.KeySOS_timer_flag = 0\r
-        self._single_power_ai_merged = False\r
-        self._pwk_shutdown_ticks = 30  # 100ms 节拍,默认 3s\r
-        try:\r
-            ai_gpio = "GPIO22"\r
-            pwk_gpio = "GPIO26"\r
-            bt_wakeup_en = False\r
-            wakeup_cta1_gpio = None\r
-            wakeup_bt_gpio = None\r
-            if "vc_config" in globals() and vc_config:\r
-                raw_ai = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22")\r
-                if raw_ai is None or raw_ai == "":\r
-                    ai_gpio = None\r
-                else:\r
-                    ai_gpio = raw_ai\r
-                pwk_gpio = vc_config.get_attr("PWK_KEY_GPIO", "GPIO26") or "GPIO26"\r
-                bt_wakeup_en = bool(vc_config.get_attr("BT_WAKEUP_4G_ENABLED", False))\r
-                wakeup_cta1_gpio = vc_config.get_attr("EXTINT_WAKEUP_CTA1_GPIO", None)\r
-                wakeup_bt_gpio = vc_config.get_attr("PIN_WAKEUP_BT_GPIO", None)\r
-                try:\r
-                    self._single_power_ai_merged = bool(\r
-                        vc_config.get_bool("SINGLE_POWER_KEY_AI_MERGED", False)\r
-                    ) or (ai_gpio is None)\r
-                except Exception:\r
-                    self._single_power_ai_merged = ai_gpio is None\r
-                try:\r
-                    _pwk_ms = int(vc_config.get_int("PWK_LONG_PRESS_SHUTDOWN_MS", 3000))\r
-                except Exception:\r
-                    _pwk_ms = 3000\r
-                self._pwk_shutdown_ticks = max(1, (_pwk_ms + 99) // 100)\r
-        except Exception:\r
-            ai_gpio = "GPIO22"\r
-            pwk_gpio = "GPIO26"\r
-            bt_wakeup_en = False\r
-            wakeup_cta1_gpio = None\r
-            wakeup_bt_gpio = None\r
-\r
-        if ai_gpio:\r
-            self.extint_AI = ExtInt(\r
-                getattr(ExtInt, ai_gpio),\r
-                ExtInt.IRQ_RISING_FALLING,\r
-                ExtInt.PULL_PU,\r
-                self.keyAI_extint_cb,\r
-            )\r
-            self.extint_AI.enable()\r
-        else:\r
-            self.extint_AI = None\r
-\r
-        self.pwk_key = ExtInt(getattr(ExtInt, pwk_gpio), ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_PU, self.pwk_extint_cb)\r
-\r
-        if bt_wakeup_en:\r
-            try:\r
-                if wakeup_cta1_gpio:\r
-                    self.extint_wakeup_cta1 = ExtInt(getattr(ExtInt, wakeup_cta1_gpio), ExtInt.IRQ_RISING, ExtInt.PULL_DISABLE, self.wakeup_extint_cb)\r
-                if wakeup_bt_gpio:\r
-                    self.PIN_wakeup_bt = Pin(getattr(Pin, wakeup_bt_gpio), Pin.OUT, Pin.PULL_DISABLE, 0)\r
-                self.extint_wakeup_cta1_timer = osTimer()\r
-                if hasattr(self, "extint_wakeup_cta1"):\r
-                    self.extint_wakeup_cta1.enable()\r
-            except Exception:\r
-                pass\r
-\r
-        self.pwk_key_status = 0\r
-        self.pwk_key_timer_count = 0\r
-        self.pwk_key_long_press = 0\r
-        self.pwk_key_press = 0\r
-        self.powerKey_status = 0\r
-        self.pwk_key_timer = osTimer()\r
-        self.pwk_is_double_timer = osTimer()\r
-        self.double_click_threshold = 1000#双击间隔时间\r
-        self.pwk_press_count = 0#按下次数\r
-        self.last_key_time = 0          # 上一次有效触发时间\r
-        self.key_debounce_interval = 20 # 防抖窗口20ms,标准值\r
-        #self.pwk_key = ExtInt(ExtInt.GPIO26, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_DISABLE, self.pwk_extint_cb)\r
-        \r
-        self.pwk_key.enable() \r
-        print("AI_dev:1")\r
-        self.btnInfo_queue = Queue(100)\r
-        self.btnInfo_queue_count = 0\r
-        _thread.stack_size(16*1024)\r
-        self.btnInfo_queue_thread_id = _thread.start_new_thread(self.btnInfo_queue_thread, ())\r
-        _thread.stack_size(8*1024)\r
-        EventMesh.subscribe("publish_AI_btnInfo",self.publish_AI_btnInfo)\r
-        EventMesh.subscribe("get_battery",self.get_battery)\r
-        EventMesh.subscribe("audio_montage_play",self.audio_montage_play)\r
-        # EventMesh.subscribe("paly_iccid_imei",self.paly_iccid_imei)\r
-        EventMesh.subscribe("paly_device_res_error",self.paly_device_res_error)\r
-        EventMesh.subscribe("Power_off",self.pwk_key_Long_Press)\r
-        EventMesh.subscribe("wakeup_BT",self.wakeup_BT)\r
-        EventMesh.subscribe("sleep_BT",self.sleep_BT)\r
-        EventMesh.subscribe("sys_sleep_cb",self.sys_sleep_cb)#系统休眠前回调\r
-        EventMesh.subscribe("sys_up_cb",self.sys_up_cb)#系统唤醒后回调\r
-        \r
-        EventMesh.subscribe("GET_USB_STATE",self.get_usb_state)\r
-        EventMesh.subscribe("SET_SPEECH_RATE",self.set_speech_rate)\r
-        EventMesh.subscribe("rest_TB",self.rest_TB)\r
-        \r
-        print("AI_dev:2")\r
-        self.pk = PowerKey()\r
-        self.pk.powerKeyEventRegister(self.powerKey_callback)\r
-        self.usb = USB()\r
-        self.usb.setCallback(self.usb_event_cb)\r
-        self.usb_status = self.usb.getStatus()\r
-        self.usb_charge_flag = 0\r
-        self._usb_evt_last_ms = 0\r
-        self._usb_evt_last_state = -1\r
-        self.Low_power_warning_timer = 0\r
-        self.AI_dev_timer = osTimer()\r
-        self.AI_dev_timer.start(10000, 1, self.AI_dev_timer_cb)\r
-        print("AI_dev:AI_dev_timer")\r
-        self.aud = audio.Audio(0)\r
-        self.Low_power_warning_flag = 0\r
-        self._low_warn_last_ms = 0\r
-        self._empty_shutdown_done = 0\r
-        self.VC_TTS_volume =  EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置\r
-        if self.VC_TTS_volume == None:\r
-            self.VC_TTS_volume = 100\r
-        self.aud.setVolume(self.VC_TTS_volume //10)\r
-        EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
-        #self.aud.play(1,0,'U:/static/xianyikaiji.mp3')#开机音频\r
-        #EventMesh.publish("audio_montage_play",["xianyikaiji.mp3"])\r
-        #EventMesh.publish("publish_AI_btnInfo",{"func":self.ntptime_cb})\r
-        self.check_rest_timer_count = 0\r
-        try:\r
-            if "vc_config" in globals() and vc_config and vc_config.uart_supported():\r
-                self.ai_uart = AI_UART()\r
-        except Exception:\r
-            pass\r
-\r
-        self.Keycall1_press_count = 0\r
-        self.ble_switch_flag_time = osTimer()\r
-        self.ble_switch_flag_time.start(20000, 0, self.ble_switch_flag_time_init)\r
-        # AI_hw_status 先于本模块初始化,开机已插 USB 时需补同步一次\r
-        try:\r
-            if int(self.usb_status or 0) == 1:\r
-                EventMesh.publish("hw_usb_state", 1)\r
-                EventMesh.publish("publish_AI_btnInfo", {"func": self.usb_event, "data": 1})\r
-        except Exception:\r
-            pass\r
-    def ble_switch_flag_time_init(self,*args):\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.ble_switch_flag_time_cb})\r
-    def ble_switch_flag_time_cb(self,*args):\r
-        bt_switch_flag =  EventMesh.publish("get_storage_config","switch_bt_state")#bt开关\r
-        if bt_switch_flag == None:\r
-            bt_switch_flag = "1"\r
-        EventMesh.publish("send_bt_switch_toble",str(bt_switch_flag))\r
-        # EventMesh.publish("send_imei_to_ble")\r
-        ble_switch_flag =  EventMesh.publish("get_storage_config","switch_ble_state")#ble开关\r
-        if ble_switch_flag == None:\r
-            ble_switch_flag = "1"\r
-        EventMesh.publish("send_ble_switch_toble",str(ble_switch_flag))\r
-        vc_tts_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")\r
-        if vc_tts_volume == None:\r
-            vc_tts_volume=100\r
-        # EventMesh.publish("adjust_volume_toBle",str(vc_tts_volume))\r
-\r
-        pass\r
-        print("ble_switch_flag_time_cb!!!!!!!")\r
-    def ntptime_cb(self):\r
-        #ntptime.settime(8)\r
-        print("timer:{}!!!!!!!!\r\n".format(utime.localtime()))\r
-    def usb_event_cb(self, state):\r
-        # USB 插拔抖动过滤(日志中 powerKey 与 usb_evt 0/1 连跳会导致误播 OUT/IN)\r
-        try:\r
-            now = utime.ticks_ms()\r
-            last_ms = getattr(self, "_usb_evt_last_ms", 0)\r
-            last_st = getattr(self, "_usb_evt_last_state", -1)\r
-            if last_ms and utime.ticks_diff(now, last_ms) < 800 and last_st != state:\r
-                return\r
-            self._usb_evt_last_ms = now\r
-            self._usb_evt_last_state = state\r
-        except Exception:\r
-            pass\r
-        # 转发给精简硬件监控(AI_hw_status),避免 USB callback 被覆盖导致“后面失效”\r
-        try:\r
-            EventMesh.publish("hw_usb_state", state)\r
-        except Exception:\r
-            pass\r
-        try:\r
-            _BatteryCfg.instance().on_usb_change(1 if state == 1 else 0)\r
-        except Exception:\r
-            pass\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.usb_event,"data":state})\r
-\r
-    def usb_event(self, state):    \r
-        status = state\r
-        self.usb_charge_flag = 0\r
-        if status == 1:#插入USB\r
-            print("charge R_CHARGE_in")\r
-            self.usb_status = 1\r
-            VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
-            if VC_satrt_flag == None:\r
-                VC_satrt_flag = 0\r
-            \r
-            voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
-            if voice_record_satrt_flag == None:\r
-                voice_record_satrt_flag = 0\r
-            if VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
-                self.aud.stop()\r
-                #EventMesh.publish("flicker_led_red")#充电闪灯\r
-                #self.aud.play(1,1,'U:/static/R_CHARGE_IN.mp3')\r
-                try:\r
-                    if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
-                        EventMesh.publish("set_led_state", {"charging": "charging"})\r
-                except Exception:\r
-                    pass\r
-                EventMesh.publish("audio_montage_play",["R_CHARGE_IN.mp3"])\r
-            self.Low_power_warning_flag  = 0\r
-            try:\r
-                self._empty_shutdown_done = 0\r
-            except Exception:\r
-                pass\r
-            pass\r
-        elif status == 0:#拨出usbs\r
-            self.usb_status = 0\r
-            VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
-            if VC_satrt_flag == None:\r
-                VC_satrt_flag = 0\r
-            voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
-            if voice_record_satrt_flag == None:\r
-                voice_record_satrt_flag = 0\r
-            if VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
-                self.aud.stop()\r
-                try:\r
-                    if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
-                        EventMesh.publish("set_led_state", {"charging": "none"})\r
-                except Exception:\r
-                    pass\r
-                EventMesh.publish("audio_montage_play",["R_CHARGE_OUT.mp3"])\r
-                #self.aud.play(1,1,'U:/static/R_CHARGE_OUT.mp3')\r
-    def get_usb_state(self,event,msg):\r
-        return self.usb_status\r
-    \r
-    def set_speech_rate(self,event,msg):\r
-        #msg的取值是0.5~2.0\r
-        #而语速初始值是0,范围是-50~50\r
-        rate = 0\r
-        if msg == 0.5:\r
-            rate = -50\r
-        elif msg < 1:\r
-            rate = (msg - 1)* 100\r
-        elif msg >= 1:\r
-            rate = (msg - 1)* 50\r
-        rate = round(rate)\r
-        print("速率已经设置为:{}".format(rate))\r
-        EventMesh.publish("rate_change",rate)\r
-        return rate\r
-    def btnInfo_queue_thread(self):\r
-        while True:\r
-            data = self.btnInfo_queue.get()\r
-            self.btnInfo_queue_count-=1\r
-            #EventMesh.debug_log("Info_queue:data = {}".format(data))\r
-            if data != None:\r
-                try:\r
-                    if "data" in data :\r
-                        data["func"](data["data"])\r
-                    else :\r
-                        data["func"]()\r
-                except Exception as e:\r
-                    #EventMesh.debug_log("btnInfo_queue:ERROR:{}".format(e))\r
-                    if "data" in data :\r
-                        print(data["data"])\r
-                    usys.print_exception(e)\r
-    def publish_AI_btnInfo(self,event,msg):\r
-        #print("publish_AI_btnInfo 1")\r
-        self.btnInfo_queue.put(msg)\r
-        self.btnInfo_queue_count+=1\r
-        if self.btnInfo_queue_count > 80:\r
-            self.btnInfo_queue_count =0\r
-            print("事件处理线程队列出问题,重启!!!!!!!!!!!!!!")\r
-            print("线程:{} 队列:{}".format(_thread.threadIsRunning(self.btnInfo_queue_thread_id),self.btnInfo_queue.size()))\r
-            # if _thread.threadIsRunning(self.btnInfo_queue_thread_id):\r
-            #     _thread.stop_thread(self.btnInfo_queue_thread_id)\r
-            # _thread.stack_size(10*1024)\r
-            # self.btnInfo_queue_thread_id = _thread.start_new_thread(self.btnInfo_queue_thread, ())\r
-            # _thread.stack_size(8*1024)\r
-            # self.btnInfo_queue.clear()\r
-            # self.btnInfo_queue = Queue(100)\r
-            EventMesh.publish("VC_error") #播放报错\r
-            utime.sleep_ms(1000)\r
-            self.aud.stopPlayStream()\r
-            self.aud.stopAll()\r
-            Power.powerRestart()\r
-            EventMesh.publish("set_storage_config",["powerRestart",1])\r
-        #print("publish_AI_btnInfo 2")\r
-    \r
-    def sys_sleep_cb(self,event=None,msg=None):#休眠前回调\r
-        try:\r
-            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
-                return\r
-        except Exception:\r
-            return\r
-        print("sys_sleep_cb!!!!!!!!!!!!!!!")\r
-        EventMesh.publish("sleep_BT")#休眠蓝牙\r
-        self.PIN_wakeup_bt.write(0)\r
-    def sys_up_cb(self,event=None,msg=None):#唤醒前回调\r
-        try:\r
-            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
-                return\r
-        except Exception:\r
-            return\r
-        print("sys_up_cb!!!!!!!!!!!!!!!")\r
-        self.wakeup_BT()\r
-        self.PIN_wakeup_bt.write(1)\r
-    def rest_TB(self,event=None,msg=None):\r
-        print("rest_TB!!!!!!!!!!!!!!!")\r
-        self.PIN_BT_ON.write(0)\r
-        utime.sleep_ms(100)\r
-        self.PIN_BT_ON.write(1)\r
-    def wakeup_BT(self,event=None,msg=None):#4G唤醒蓝牙\r
-        try:\r
-            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
-                return\r
-        except Exception:\r
-            return\r
-        print("wakeup_BT!!!!!!!!!!!!!!!")\r
-        wakeup_cta1_io = self.extint_wakeup_cta1.read_level()\r
-        if wakeup_cta1_io == 1:\r
-            return 1\r
-        self.PIN_wakeup_bt.write(0)\r
-        count = 5\r
-        while(True):\r
-            count-=1\r
-            if count<=0:\r
-                return -1\r
-            wakeup_cta1_io = self.extint_wakeup_cta1.read_level()\r
-            print("bt io状态:{}".format(wakeup_cta1_io))\r
-            if wakeup_cta1_io == 1:\r
-                self.PIN_wakeup_bt.write(1)\r
-                return 1\r
-            utime.sleep_ms(100)\r
-            if count%2 == 0:\r
-                self.PIN_wakeup_bt.write(1)\r
-            else:\r
-                self.PIN_wakeup_bt.write(0)\r
-\r
-        \r
-    def sleep_BT(self,event=None,msg=None):#使蓝牙休眠\r
-        try:\r
-            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
-                return\r
-        except Exception:\r
-            return\r
-        print("sleep_BT!!!!!")\r
-        print("sleep_BT!!!!!")\r
-        print("sleep_BT!!!!!")\r
-        print("sleep_BT!!!!!")\r
-        self.PIN_wakeup_bt.write(0)\r
-        count = 300\r
-        while(True):\r
-            count-=1\r
-            if count<=0:\r
-                return -1\r
-            if self.extint_wakeup_cta1.read_level() == 0:\r
-                return 1\r
-            utime.sleep_ms(1)\r
-   \r
-   \r
-    def wakeup_extint_cb(self,args):#蓝牙唤醒4g\r
-        try:\r
-            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
-                return\r
-        except Exception:\r
-            return\r
-        print("wakeup_extint_cb:!!!!!!!!!")\r
-        EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
-        self.PIN_wakeup_bt.write(1)\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.extint_wakeup_cta1_cb})\r
-    def extint_wakeup_cta1_cb(self):\r
-        #self.extint_wakeup_cta1_timer.start(100, 1, self.extint_wakeup_cta1_timer_cb)\r
-        pass\r
-    def extint_wakeup_cta1_timer_cb(self, *args, **kwargs):\r
-        if self.extint_wakeup_cta1.read_level() == 0:\r
-            self.extint_wakeup_cta1_timer.stop()\r
-            #self.PIN_wakeup_bt.write(0)\r
-\r
-    def powerKey_callback(self, status):\r
-        try:\r
-            EventMesh.debug_log("==pwr:", status)\r
-        except Exception:\r
-            print("powerKey_callback:{}".format(status))\r
-        self.powerKey_status = status\r
-        if self.pwk_key_press == 1:\r
-            return\r
-\r
-    def pwk_extint_cb(self,args):#pwk\r
-        current_time = 0\r
-        interval = 0\r
-\r
-        try:\r
-            #按键电平判断\r
-            press_level = 0\r
-            current_level = self.pwk_key.read_level()\r
-            if current_level != press_level:\r
-                return  # 非按下状态,直接返回\r
-\r
-            #消抖处理\r
-            current_time = utime.ticks_ms()\r
-            if utime.ticks_diff(current_time, self.last_key_time) < self.key_debounce_interval:\r
-                return\r
-            self.last_key_time = current_time  # 更新最后按键时间\r
-            print("pwk 按键按下")\r
-\r
-            #仅处理"按下瞬间"事件(避免重复触发)\r
-            if len(args) < 2:\r
-                print("args参数不足,跳过")\r
-                return\r
-            self.pwk_key_status = int(args[1])\r
-            if self.pwk_key_status != 1:\r
-                return\r
-\r
-            #双击计数逻辑\r
-            self.pwk_press_count += 1\r
-            current_time = utime.ticks_ms()\r
-\r
-            #第一次按下(启动长按定时器+双击等待)\r
-            if self.pwk_press_count == 1:\r
-                self.pwk_first_press_time = current_time  # 记录首次按下时间\r
-                self.pwk_key_long_press = 0               # 重置长按标记\r
-                self.pwk_key_timer_count = 0              # 重置定时器计数\r
-                self.pwk_key_press = 1                    # 标记按键按下\r
-                \r
-                # 停止旧定时器,启动长按/单击判定定时器(100ms单次触发,循环靠回调自启)\r
-                self.pwk_key_timer.stop()\r
-                self.pwk_key_timer.start(100, 1, self.pwk_key_timer_cb)\r
-                print("第一次按下,等待第二次按下")\r
-                if getattr(self, "_single_power_ai_merged", False):\r
-                    self._merged_pwk_ai_press_start()\r
-\r
-            #第二次按下(双击判断)\r
-            elif self.pwk_press_count == 2:\r
-                # 计算两次按下间隔\r
-                interval = utime.ticks_diff(current_time, self.pwk_first_press_time)\r
-                \r
-                #符合双击阈值:执行双击逻辑\r
-                if interval <= self.double_click_threshold:\r
-                    self.pwk_key_timer.stop()  # 停止长按定时器(关键:避免触发长按)\r
-                    self.pwk_key_double_press() # 执行双击业务\r
-                    print("双击生效")\r
-                    # 重置所有状态\r
-                    self.pwk_press_count = 0\r
-                    self.pwk_key_press = 0\r
-                    self.pwk_key_timer_count = 0\r
-\r
-                #间隔过长:视为新的第一次按下\r
-                else:\r
-                    self.pwk_first_press_time = current_time  # 更新首次按下时间\r
-                    self.pwk_press_count = 1                  # 重置为第一次按下\r
-                    print("间隔过长,视为新的第一次按下")\r
-\r
-            #按下次数超过2次(防计数溢出)\r
-            else:\r
-                self.pwk_press_count = 0\r
-                self.pwk_key_timer.stop()\r
-                print("按键次数过多,重置计数")\r
-\r
-        except Exception as e:\r
-            # 异常时重置所有状态,避免卡死\r
-            self.pwk_press_count = 0\r
-            self.pwk_key_press = 0\r
-            self.pwk_key_timer_count = 0\r
-            self.pwk_key_timer.stop()\r
-            usys.print_exception(e)\r
-\r
-\r
-    def pwk_is_double_judge(self, *args):\r
-        # if self.press_count == 1:  # 仅一次按下,判定为单击\r
-            \r
-        # 重置计数(无论何种情况,超时后清零)\r
-        self.pwk_press_count = 0\r
-\r
-\r
-    def pwk_key_timer_cb(self, *args, **kwargs):\r
-        EventMesh.publish("set_wait_first_flag",0)\r
-        EventMesh.publish("set_history_board_flag",0)\r
-        EventMesh.publish("set_play_new_voice_state",0)\r
-        EventMesh.publish("set_history_voice_flag",0)\r
-        EventMesh.publish("set_wait_voice_flag",0)\r
-        EventMesh.publish("set_stop_play",0)\r
-    #     EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_timer_cb_})\r
-    # def pwk_key_timer_cb_(self) :\r
-        try:\r
-            self.pwk_key_status = self.pwk_key.read_level()\r
-            self.pwk_key_timer_count +=1\r
-            if self.pwk_key_timer_count >= self._pwk_shutdown_ticks:\r
-                #print("pwk_key_long_press {}".format(self.pwk_key_long_press))\r
-                if self.pwk_key_long_press == 0:\r
-                        self.pwk_key_long_press = 1\r
-                        #EventMesh.publish("AI_key_long_press")\r
-                        # EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])\r
-                        self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频\r
-                        EventMesh.publish("set_led_state", {"battery": "empty"})\r
-                        utime.sleep_ms(2000)\r
-                        self.aud.stopPlayStream()\r
-                        self.aud.stopAll()\r
-                        Power.powerDown()\r
-                        #print("AI_key_release")\r
-                        #EventMesh.publish("pwk_key_long_press")\r
-                        #EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_Long_Press})  \r
-            if self.pwk_key_status == 1:#松开\r
-                #print("pwk_key_timer_cb:{}".format(self.pwk_key_timer_count))\r
-                try:\r
-                    key_count = vc_config.get_int("KEY_RELEASE_DEBOUNCE_COUNT", 5)\r
-                except Exception:\r
-                    key_count = 5\r
-                if self.pwk_key_timer_count >= key_count: #防抖200ms\r
-                    #EventMesh.publish("AI_key_release")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_release})\r
-                    self.pwk_key.enable()\r
-                    self.pwk_key_timer_count = 0\r
-                    self.pwk_key_press = 0\r
-                    self.pwk_press_count = 0#按下次数\r
-                    self.pwk_key_timer.stop()\r
-            \r
-        except Exception as e: \r
-            usys.print_exception(e) \r
-            \r
-    def _ai_int_disable_for_hold(self):\r
-        if self.extint_AI is not None:\r
-            self.extint_AI.disable()\r
-\r
-    def _ai_int_enable_after_hold(self):\r
-        if self.extint_AI is not None:\r
-            self.extint_AI.enable()\r
-\r
-    def _key_ai_read_level(self):\r
-        if self.extint_AI is not None:\r
-            return self.extint_AI.read_level()\r
-        return self.pwk_key.read_level()\r
-\r
-    def _merged_pwk_ai_press_start(self):\r
-        """单电源键:按下瞬间走原 AI 键按下链路(松手走 AI_key_release,不按电源短按 Close)。"""\r
-        try:\r
-            current_time = utime.ticks_ms()\r
-            if utime.ticks_diff(current_time, self.last_ai_key_time) < 1:\r
-                return\r
-            self.last_ai_key_time = current_time\r
-            if EventMesh.publish("get_wait_voice_flag") == 1:\r
-                print("播放未读")\r
-                EventMesh.publish("set_wait_voicerupt_flag", 1)\r
-                return\r
-            if EventMesh.publish("get_recv_voice") == 1:\r
-                print("收到留言")\r
-                EventMesh.publish("set_voicerupt_flag", 1)\r
-                return\r
-            if self.KeyAI_press == 0:\r
-                EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
-                if EventMesh.publish("get_play_new_voice_state") == 1:\r
-                    EventMesh.publish("set_play_new_voice_state", 0)\r
-                EventMesh.publish("set_VC_stop_play_audio_flag", 1)\r
-                self._ai_int_disable_for_hold()\r
-                self.KeyAI_press = 1\r
-                self.KeyAI_long_press = 0\r
-                self.KeyAI_timer.start(100, 1, self.KeyAI_timer_cb)\r
-                self.KeyAI_timer_count = 0\r
-                EventMesh.publish("vc_long_dialogue_key_down")\r
-                EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})\r
-        except Exception as e:\r
-            usys.print_exception(e)\r
-\r
-    def keyAI_extint_cb(self,args):#call 2\r
-        try:\r
-            if self.extint_AI is None:\r
-                return\r
-            #print("AI_btn:keyAI_extint_cb{}".format(args[1]))\r
-            #定义最短触发间隔\r
-            MIN_TRIGGER_INTERVAL = 1  # 可根据需求调整,单位ms\r
-            #记录上次按键触发时间(初始化放类的__init__里:self.last_ai_key_time = 0)\r
-            current_time = utime.ticks_ms()  # 获取当前毫秒时间\r
-            #重复按下,直接返回\r
-            if utime.ticks_diff(current_time, self.last_ai_key_time) < MIN_TRIGGER_INTERVAL:\r
-                print("AI按键触发过频,忽略本次操作")\r
-                return\r
-            #更新上次触发时间\r
-            self.last_ai_key_time = current_time\r
-\r
-            self.KeyAI_status = int(args[1])\r
-            if self.KeyAI_status == 0:\r
-                self.KeyAI_status = 1\r
-            else:\r
-                self.KeyAI_status = 0\r
-            \r
-            if self.KeyAI_status == 0:#按下\r
-                if EventMesh.publish("get_wait_voice_flag") == 1:\r
-                    print("播放未读")\r
-                    EventMesh.publish("set_wait_voicerupt_flag",1)\r
-                    return\r
-                if EventMesh.publish("get_recv_voice") == 1:#当处于收到留言的时候\r
-                    print("收到留言")\r
-                    EventMesh.publish("set_voicerupt_flag",1)\r
-                    return\r
-                if self.KeyAI_press == 0 :\r
-                    EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
-                    if EventMesh.publish("get_play_new_voice_state") == 1:\r
-                        EventMesh.publish("set_play_new_voice_state",0)\r
-                    \r
-                    EventMesh.publish("set_VC_stop_play_audio_flag",1)#按键按下不允许播放之前下发的音频数据\r
-                    self._ai_int_disable_for_hold()\r
-                    self.KeyAI_press = 1\r
-                    self.KeyAI_long_press = 0\r
-                    self.KeyAI_timer.start(100, 1, self.KeyAI_timer_cb)\r
-                    self.KeyAI_timer_count = 0\r
-                    #EventMesh.publish("AI_key_press")\r
-                    # if dev_type == "7PIN":\r
-                        # self.aud.aud_tone_play(10,100)#播放按键音\r
-                        \r
-                    EventMesh.publish("vc_long_dialogue_key_down")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press})  \r
-        except Exception as e: \r
-            usys.print_exception(e)\r
-    def KeyAI_timer_cb(self, *args, **kwargs):\r
-    #     EventMesh.publish("publish_AI_btnInfo",{"func":self.KeyAI_timer_cb_})\r
-    # def KeyAI_timer_cb_(self):\r
-        try:\r
-            self.KeyAI_status = self._key_ai_read_level()\r
-            self.KeyAI_timer_count +=1\r
-            if self.KeyAI_timer_count >= 10*1 : #长按1s \r
-               \r
-                if self.KeyAI_long_press == 0:\r
-                    self.KeyAI_long_press = 1\r
-                    #EventMesh.publish("AI_key_long_press")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_long_press})  \r
-            try:\r
-                _sos_on = vc_config.get_bool("SOS_ENABLED", True)\r
-            except Exception:\r
-                _sos_on = True\r
-            if _sos_on and self.KeyAI_timer_count >= 10*3 : #长按3s 上一次是短按则拨打sos紧急联系人\r
-                print("long sos phone:{}".format(self.KeySOS_timer_flag))\r
-                \r
-                if self.KeySOS_timer_flag == 1:\r
-                    self.KeySOS_timer_flag=0\r
-                    self.KeySos_long_press = 1\r
-                    self.KeySOS_timer.stop()\r
-                    get_sos_flag = EventMesh.publish("get_sos_flag")\r
-                    if get_sos_flag == 0:\r
-                        EventMesh.publish("set_sos_info_call")\r
-                    pass\r
-\r
-            elif self.KeyAI_timer_count >= 10*8:#关机\r
-                try:\r
-                    allow_power_off = vc_config.get_bool("AI_LONG_PRESS_POWER_OFF_ENABLED", False)\r
-                except Exception:\r
-                    allow_power_off = False\r
-                if allow_power_off:\r
-                    EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])\r
-                    #self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频\r
-                    EventMesh.publish("set_led_state", {"battery": "empty"})\r
-                    utime.sleep_ms(2000)\r
-                    self.aud.stopPlayStream()\r
-                    print("stopAll:8")\r
-                    self.aud.stopAll()\r
-                    Power.powerDown()\r
-            if self.KeyAI_status == 1:#松开\r
-                #print("KeyAI_timer_cb:{}".format(self.KeyAI_timer_count))\r
-                try:\r
-                    key_count = vc_config.get_int("KEY_RELEASE_DEBOUNCE_COUNT", 5)\r
-                except Exception:\r
-                    key_count = 5\r
-                if self.KeyAI_timer_count >= key_count: #防抖200ms\r
-                    if self.KeySOS_timer_flag == 1:\r
-                        self.KeySOS_timer_flag = 0\r
-                        self.KeySos_long_press = 0\r
-                        self.KeySOS_timer.stop()\r
-                        print("KeySOS_timer_flag!!!!!!!!!!!!!!!")  #短按两次需要清除标识\r
-\r
-                    if self.KeyAI_long_press == 0:\r
-                        try:\r
-                            _sos_on = vc_config.get_bool("SOS_ENABLED", True)\r
-                        except Exception:\r
-                            _sos_on = True\r
-                        if _sos_on and self.KeySOS_timer_flag == 0:\r
-                            self.KeySOS_timer_flag = 1\r
-                            self.KeySos_long_press = 0\r
-                            self.KeySOS_timer.start(5000, 0, self.KeySos_timer_cb) #短按开启5s定时器 5s之内没有长按3s则清除标识 长按3s拨打sos紧急联系人\r
-                    else:\r
-                        self.KeySOS_timer_flag = 0\r
-                        self.KeySos_long_press = 0\r
-                        self.KeySOS_timer.stop()\r
-                        \r
-                    #EventMesh.publish("AI_key_release")\r
-                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_release})  \r
-                    self._ai_int_enable_after_hold()\r
-                    self.KeyAI_timer.stop()\r
-                    self.KeyAI_timer_count = 0\r
-                    self.KeyAI_press = 0\r
-                    self.KeyAI_long_press=0\r
-            \r
-        except Exception as e: \r
-            usys.print_exception(e) \r
-       \r
-    def AI_key_press(self):\r
-        # EventMesh.publish("wakeup_BT")\r
-        \r
-        # if dev_type == "7PIN" or dev_type == "7PIN_coze" :\r
-        #     # self.aud.aud_tone_play(10,60)#播放按键音\r
-        #     EventMesh.publish("audio_montage_play",["chenggong.mp3"])  \r
-        self.check_rest_timer_count = 0\r
-        print("AI_key_press")\r
-        EventMesh.publish("AI_key_press")\r
-    def KeySos_timer_cb(self,*argc):\r
-        print("KeySos_timer_cb!!!!!!!!!!{} {}".format(self.KeySos_long_press,self.KeySOS_timer_flag))\r
-        \r
-        if self.KeySos_long_press == 1 and self.KeySOS_timer_flag == 1:  #清空sos标识\r
-            self.KeySos_long_press=0\r
-            self.KeySOS_timer_flag=0\r
-           \r
-        self.KeySos_long_press=0\r
-        self.KeySOS_timer_flag = 0\r
-        return\r
-        self.KeySOS_timer_flag = 0\r
-        if self.KeySOS_timer_count >= 3:\r
-            self.KeySOS_timer_count = 0\r
-            get_sos_flag = EventMesh.publish("get_sos_flag")\r
-            if get_sos_flag == 0:\r
-                EventMesh.publish("set_sos_info_call")\r
-        self.KeySOS_timer_count = 0\r
-        pass\r
-\r
-    def AI_key_long_press(self):\r
-        self.check_rest_timer_count = 0\r
-        print("AI_key_long_press")\r
-\r
-        EventMesh.publish("AI_key_long_press")\r
-        \r
-    def AI_key_release(self):\r
-        print("AI_key_release")\r
-        # self.KeySOS_timer_count +=1\r
-        # print("self.KeySOS_timer_count:{} {}".format(self.KeySOS_timer_count,self.KeySOS_timer_flag))\r
-        # if self.KeySOS_timer_flag == 0:\r
-        #     self.KeySOS_timer_flag = 1\r
-        #     self.KeySOS_timer.start(5000, 0, self.KeySos_timer_cb)\r
-        EventMesh.publish("AI_key_release")\r
-        \r
-    def pwk_key_double_press(self):\r
-        self.check_rest_timer_count = 0\r
-        try:\r
-            if getattr(self, "_single_power_ai_merged", False):\r
-                try:\r
-                    self.KeyAI_timer.stop()\r
-                except Exception:\r
-                    pass\r
-                self.KeyAI_timer_count = 0\r
-                self.KeyAI_press = 0\r
-                self.KeyAI_long_press = 0\r
-                self._ai_int_enable_after_hold()\r
-        except Exception:\r
-            pass\r
-        print("pwk_key_double_press")\r
-        EventMesh.publish("pwk_key_double_press")\r
-\r
-    def pwk_key_Long_Press(self,event=None,msg=None):\r
-        self.check_rest_timer_count = 0\r
-        print("pwk_key_long_press!!!!!!!")\r
-        print("pwk_key_long_press!!!!!!!")\r
-        \r
-        # EventMesh.publish("paly_iccid_imei")\r
-        # return\r
-        EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])\r
-        #self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频\r
-        EventMesh.publish("set_led_state", {"battery": "empty"})\r
-        utime.sleep_ms(3000)\r
-        self.aud.stopPlayStream()\r
-        self.aud.stopAll()\r
-        Power.powerDown()\r
-        #print("AI_key_release")\r
-        #EventMesh.publish("pwk_key_long_press")\r
-        \r
-    def pwk_key_release(self):\r
-        self.check_rest_timer_count = 0\r
-        #print("pwk_key_release")\r
-        EventMesh.publish("pwk_key_release")\r
-\r
-    \r
-    def wdt_timer_cb(self, *args, **kwargs):\r
-        if self.is_wdt == 1:\r
-            self.wdt.feed()\r
-\r
-    def AI_dev_timer_cb(self, *args, **kwargs):\r
-        #print("AI_dev_timer_cb")\r
-        EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_dev_timer_event})  \r
-    def _battery_empty_shutdown_seq(self):\r
-        """REQ-BAT-03:红快闪 3 次 → 灭灯 → 关机提示音 → 关机(btn 线程执行)。"""\r
-        try:\r
-            EventMesh.publish("battery_priority_mute_vc")\r
-        except Exception:\r
-            pass\r
-        try:\r
-            n, ms = 3, 500\r
-            if "vc_config" in globals() and vc_config:\r
-                n = int(vc_config.get_int("BATTERY_EMPTY_FLASH_COUNT", 3))\r
-                ms = int(vc_config.get_int("BATTERY_EMPTY_FLASH_PERIOD_MS", 500))\r
-            EventMesh.publish("set_led_state", {"alert": "red_flash_3"})\r
-            utime.sleep_ms(n * ms * 2 + 200)\r
-            EventMesh.publish("set_led_state", {"ai_state": "idle", "battery": "normal", "charging": "none"})\r
-        except Exception:\r
-            pass\r
-        try:\r
-            EventMesh.publish("audio_montage_play", ["didianguanji.mp3"])\r
-            utime.sleep_ms(4500)\r
-            self.aud.stopPlayStream()\r
-            self.aud.stopAll()\r
-        except Exception:\r
-            pass\r
-        try:\r
-            Power.powerDown()\r
-        except Exception:\r
-            pass\r
-\r
-    def AI_dev_timer_event(self):\r
-        #EventMesh.publish("audio_montage_play",["liuliangtishi.mp3"])\r
-        EventMesh.publish("send_power_init_data")\r
-        if self.is_wdt == 1:\r
-            self.wdt.feed()\r
-        VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
-        if VC_satrt_flag == None:\r
-            VC_satrt_flag = 0\r
-        voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
-        if voice_record_satrt_flag == None:\r
-            voice_record_satrt_flag = 0\r
-        _bcfg = _BatteryCfg.instance()\r
-        Vbatt = _bcfg.get_vbatt_mv_charge() if self.usb_status == 1 else _bcfg.get_vbatt_mv()\r
-        if self.usb_status == 1 and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
-            try:\r
-                if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
-                    EventMesh.publish("set_led_state", {"charging": "charging"})#充电红灯常亮\r
-            except Exception:\r
-                pass\r
-        if self.usb_status == 1 and Vbatt >= _bcfg.charge_full_mv and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
-            try:\r
-                if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
-                    EventMesh.publish("set_led_state", {"charging": "full"})\r
-            except Exception:\r
-                pass\r
-            if self.usb_charge_flag == 0 :\r
-                EventMesh.publish("audio_montage_play",["R_CHARGE_FULL.mp3"])\r
-                #self.aud.play(1,0,'U:/static/R_CHARGE_FULL.mp3')#充电已满\r
-                self.usb_charge_flag = 1\r
-        try:\r
-            low_soc = int(_bcfg.low_soc)\r
-        except Exception:\r
-            low_soc = 25\r
-        warn_interval_ms = 300000\r
-        try:\r
-            if "vc_config" in globals() and vc_config:\r
-                warn_interval_ms = int(vc_config.get_int("BATTERY_LOW_WARN_INTERVAL_MS", 300000))\r
-        except Exception:\r
-            pass\r
-        soc = None\r
-        try:\r
-            soc = self.get_battery()\r
-        except Exception:\r
-            soc = None\r
-        if self.usb_status == 0:\r
-            if _bcfg.should_shutdown_empty(Vbatt):\r
-                if getattr(self, "_empty_shutdown_done", 0) == 0:\r
-                    self._empty_shutdown_done = 1\r
-                    try:\r
-                        print("[BAT] empty shutdown vbatt={} soc={} empty_mv={}".format(\r
-                            Vbatt, soc, _bcfg.empty_mv))\r
-                    except Exception:\r
-                        pass\r
-                    EventMesh.publish("publish_AI_btnInfo", {"func": self._battery_empty_shutdown_seq})\r
-            elif soc is not None and soc < low_soc:\r
-                if voice_record_satrt_flag == 0:\r
-                    try:\r
-                        now = utime.ticks_ms()\r
-                        last = getattr(self, "_low_warn_last_ms", 0)\r
-                        if (not last) or utime.ticks_diff(now, last) >= warn_interval_ms:\r
-                            self._low_warn_last_ms = now\r
-                            try:\r
-                                print("[BAT] low warn soc={} interval_ms={}".format(soc, warn_interval_ms))\r
-                            except Exception:\r
-                                pass\r
-                            EventMesh.publish("audio_montage_play", ["Low_power_warning.mp3"])\r
-                    except Exception:\r
-                        pass\r
-            else:\r
-                try:\r
-                    self._low_warn_last_ms = 0\r
-                    self._empty_shutdown_done = 0\r
-                except Exception:\r
-                    pass\r
-                if VC_satrt_flag == 0 and Vbatt is not None and Vbatt >= int(_bcfg.low_mv):\r
-                    try:\r
-                        if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
-                            EventMesh.publish("set_led_state", {"charging": "none", "battery": "normal"})\r
-                    except Exception:\r
-                        pass\r
-\r
-        self.check_rest_timer_count += 1\r
-        if self.check_rest_timer_count >= 8640:#24小时没按键,则关机\r
-            self.pwk_key_Long_Press()\r
-        \r
-    def get_battery(self, event=None, msg=None):\r
-        # SoC 基于 _BatteryCfg 10 点滑动平均电压,与 protocol 心跳/AI_hw 一致\r
-        try:\r
-            return _BatteryCfg.instance().soc_now()\r
-        except Exception:\r
-            return 0\r
-    def _audio_is_battery_priority(self, msg):\r
-        try:\r
-            for i in (msg or []):\r
-                n = str(i)\r
-                if ("Low_power" in n) or ("didianguanji" in n):\r
-                    return True\r
-        except Exception:\r
-            pass\r
-        return False\r
-\r
-    def audio_montage_play(self,event,msg):\r
-        self.wakeup_BT()\r
-        bat_pri = self._audio_is_battery_priority(msg)\r
-        if bat_pri:\r
-            try:\r
-                EventMesh.publish("battery_priority_mute_vc")\r
-            except Exception:\r
-                pass\r
-        else:\r
-            # 仅服务端 TTS PCM 在播时跳过;低电/关机提示优先级更高(见上)\r
-            try:\r
-                if EventMesh.publish("get_vc_tts_playback_active") == 1:\r
-                    print("[audio-arb] skip local montage_play during tts_pcm")\r
-                    return\r
-            except Exception:\r
-                pass\r
-        self.aud.stopPlayStream()\r
-        self.aud.stopAll()\r
-        str_data='mp3files='\r
-        for i in msg:\r
-            try:\r
-                str_tmp = i.replace(".mp3","")\r
-                str_data+="{}.mp3+".format(audio_buf.index(str_tmp))\r
-            except Exception as e:\r
-                    usys.print_exception(e)    \r
-        str_data = str_data[:-1]\r
-        print("audio_montage_play:{}".format(str_data))\r
-        self.aud.montage_play('U:/static/audio.bin',1,1,str_data)\r
-        self.play_imei_timer_count=1\r
-        self.play_imei_timer.stop()\r
-        self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)\r
-        \r
-    def paly_iccid_imei(self,event,msg):\r
-        \r
-        self.wakeup_BT()\r
-        self.aud.stopPlayStream()\r
-        self.aud.stopAll()\r
-        str_data = 'mp3files={}.mp3+'.format(audio_buf.index("liuliangtishi"))#流量提示\r
-        imei  = modem.getDevImei()\r
-        iccid = sim.getIccid()\r
-        print("paly_iccid_imei:ICCID:{}".format(iccid))\r
-        if iccid != -1 and len(iccid) > 5:\r
-            iccid = iccid.upper()\r
-            if len(iccid) <20:\r
-                for i in range(20-len(iccid)):\r
-                    iccid = "0"+iccid\r
-            str_data += '{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+'.format(audio_buf.index("R_READ_TIME_{}".format(iccid[0]))\\r
-                ,audio_buf.index("R_READ_TIME_{}".format(iccid[1])),audio_buf.index("R_READ_TIME_{}".format(iccid[2])),audio_buf.index("R_READ_TIME_{}".format(iccid[3])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(iccid[4])),audio_buf.index("R_READ_TIME_{}".format(iccid[5])),audio_buf.index("R_READ_TIME_{}".format(iccid[6])),audio_buf.index("R_READ_TIME_{}".format(iccid[7])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(iccid[8])),audio_buf.index("R_READ_TIME_{}".format(iccid[9])),audio_buf.index("R_READ_TIME_{}".format(iccid[10])),audio_buf.index("R_READ_TIME_{}".format(iccid[11])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(iccid[12])),audio_buf.index("R_READ_TIME_{}".format(iccid[13])),audio_buf.index("R_READ_TIME_{}".format(iccid[14])),audio_buf.index("R_READ_TIME_{}".format(iccid[15])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(iccid[16])),audio_buf.index("R_READ_TIME_{}".format(iccid[17])),audio_buf.index("R_READ_TIME_{}".format(iccid[18])),audio_buf.index("R_READ_TIME_{}".format(iccid[19])))\r
-        str_data += '{}.mp3+'.format(audio_buf.index("shouhou"))#可联系售后解决,联系方式见售后客服电话是400-886-8777或到微信公众号柒牌SEVENBRAND自行充值:\r
-        self.aud.montage_play('U:/static/audio.bin',1,1,str_data)\r
-        #开定时器不让系统休眠\r
-        self.play_imei_timer_count=1\r
-        self.play_imei_timer.stop()\r
-        self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)\r
-\r
-    def play_imei_timer_cb(self,event=None,msg=None):\r
-        if self.play_imei_timer_count == 1:\r
-            if EventMesh.publish("get_audio_play_state") == 0:\r
-                print("音频文件播放完成")\r
-                self.play_imei_timer_count=0\r
-                self.play_imei_timer.stop()\r
-                return\r
-        EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
-\r
-    #鉴权出错,播放imei号\r
-    def paly_device_res_error(self,event,msg):\r
-        self.wakeup_BT()\r
-        EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
-        str_data = 'mp3files={}.mp3+'.format(audio_buf.index("jianquancuowu"))#AI功能异常,请联系客户查询,设备号为:\r
-        imei  = modem.getDevImei()\r
-        imei = imei.upper()\r
-        str_data += '{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3'.format(audio_buf.index("R_READ_TIME_{}".format(imei[0]))\\r
-                ,audio_buf.index("R_READ_TIME_{}".format(imei[1])),audio_buf.index("R_READ_TIME_{}".format(imei[2])),audio_buf.index("R_READ_TIME_{}".format(imei[3])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(imei[4])),audio_buf.index("R_READ_TIME_{}".format(imei[5])),audio_buf.index("R_READ_TIME_{}".format(imei[6])),audio_buf.index("R_READ_TIME_{}".format(imei[7])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(imei[8])),audio_buf.index("R_READ_TIME_{}".format(imei[9])),audio_buf.index("R_READ_TIME_{}".format(imei[10])),audio_buf.index("R_READ_TIME_{}".format(imei[11])),\\r
-                audio_buf.index("R_READ_TIME_{}".format(imei[12])),audio_buf.index("R_READ_TIME_{}".format(imei[13])),audio_buf.index("R_READ_TIME_{}".format(imei[14])))\r
-        #print("str_data:{}".format(str_data))\r
-        self.aud.montage_play('U:/static/audio.bin',1,1,str_data)\r
-        self.play_imei_timer_count=1\r
-        self.play_imei_timer.stop()\r
-        self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)\r
-\r
+# -*- coding: utf-8 -*-
+"""按键、电源、USB、按键队列。"""
+from common import Abstract
+try:
+    from qp_log import qp_print as print
+except Exception:
+    pass
+import EventMesh
+import usys
+import utime
+import audio
+import osTimer
+import dataCall
+import sim
+from machine import Pin, ExtInt
+from machine import WDT
+import modem
+import ntptime
+import _thread
+from queue import Queue
+from misc import Power, USB, PowerKey
+import constants
+try:
+    import vc_config
+except Exception:
+    vc_config = None
+from vc_common import dev_type
+from vc_hw import set_ipv4_datacall, _BatteryCfg
+from vc_uart import AI_UART
+
+audio_buf = ['chaobingfatis', 'chenggong', 'didianguanji', 'IMEIhao', 'jianquancuowu', 'key_prompt', 'liuliangtishi', 'Low_power_warning', 'R_CHARGE_FULL', 'R_CHARGE_IN', 'R_CHARGE_OUT', 'R_READ_TIME_0', 'R_READ_TIME_1', 'R_READ_TIME_2', 'R_READ_TIME_3', 'R_READ_TIME_4', 'R_READ_TIME_5', 'R_READ_TIME_6', 'R_READ_TIME_7', 'R_READ_TIME_8', 'R_READ_TIME_9', 'R_READ_TIME_A', 'R_READ_TIME_B', 'R_READ_TIME_C', 'R_READ_TIME_D', 'R_READ_TIME_E', 'R_READ_TIME_F', 'R_READ_TIME_G', 'R_READ_TIME_H', 'R_READ_TIME_I', 'R_READ_TIME_J', 'R_READ_TIME_K', 'R_READ_TIME_L', 'R_READ_TIME_M', 'R_READ_TIME_N', 'R_READ_TIME_O', 'R_READ_TIME_P', 'R_READ_TIME_Q', 'R_READ_TIME_R', 'R_READ_TIME_S', 'R_READ_TIME_T', 'R_READ_TIME_U', 'R_READ_TIME_V', 'R_READ_TIME_W', 'R_READ_TIME_X', 'R_READ_TIME_Y', 'R_READ_TIME_Z', 'send_tone', 'shouhou', 'xinhaoruo1', 'zhengzaiguanji']
+class AI_dev(Abstract):
+    # AI
+    def __init__(self):
+        # if(dev_type != "7PIN"):
+        #     set_ipv4_datacall()
+        self.is_wdt = 0 #是否开启看门狗
+        #AT+QDAI=1,0,0,1,0,0,1,1PCM音频输出设置为主模式、短同步、16 kHz采样率和256 kHz时钟频率,采样位深为16bit,使用的时间间隙数量为1,并且使用第一个时间间隙。
+        try:
+            bt_pm_en = False
+            pin_bt_on = None
+            pin_mot = None
+            qdai_cfg = None
+            if "vc_config" in globals() and vc_config:
+                bt_pm_en = bool(vc_config.get_attr("BT_POWER_MOTOR_ENABLED", False))
+                pin_bt_on = vc_config.get_attr("PIN_BT_ON_GPIO", None)
+                pin_mot = vc_config.get_attr("PIN_MOT_GPIO", None)
+                qdai_cfg = vc_config.get_attr("AUDIO_QDAI_CFG", None)
+        except Exception:
+            bt_pm_en = False
+            pin_bt_on = None
+            pin_mot = None
+            qdai_cfg = None
+
+        if bt_pm_en:
+            try:
+                if pin_bt_on:
+                    self.PIN_BT_ON = Pin(getattr(Pin, pin_bt_on), Pin.OUT, Pin.PULL_DISABLE, 0)
+                if pin_mot:
+                    self.PIN_MOT = Pin(getattr(Pin, pin_mot), Pin.OUT, Pin.PULL_DISABLE, 1)
+            except Exception:
+                pass
+            try:
+                utime.sleep_ms(200)
+                if hasattr(self, "PIN_MOT"):
+                    self.PIN_MOT.write(0)
+                if hasattr(self, "PIN_BT_ON"):
+                    self.PIN_BT_ON.write(1)
+            except Exception:
+                pass
+            try:
+                if isinstance(qdai_cfg, (tuple, list)) and len(qdai_cfg) == 8:
+                    audio.set_qdai_cfg(*qdai_cfg)  # 用iis做音频输入输出
+            except Exception:
+                pass
+        self.KeyAI_status = 0
+        self.KeyAI_timer_count = 0
+        self.KeyAI_long_press = 0
+        self.KeySos_long_press = 0
+        self.KeyAI_press = 0
+        self.last_ai_key_time = 0#让ai按键不要频繁调用
+        if self.is_wdt == 1:
+            self.wdt = WDT(30) #开启看门狗
+        # self.wdt_timer = osTimer()
+        # self.wdt_timer.start(5000, 1, self.wdt_timer_cb)
+        
+        count = 0
+        while True:
+            count += 1
+            if count > 20 :
+                break
+            data = dataCall.getInfo(1, 0)
+            print("data call INFO {}".format(data))
+            if data[2][0]:
+                self.network_state = 1
+                break
+            print("等待网络 {}...".format(count))
+            utime.sleep_ms(500)
+        self.KeyAI_timer = osTimer()
+        self.KeySOS_timer = osTimer()
+        self.play_imei_timer = osTimer()
+        self.play_imei_timer_count=0
+        self.KeySOS_timer_count = 0
+        self.KeySOS_timer_flag = 0
+        self._single_power_ai_merged = False
+        self._pwk_shutdown_ticks = 30  # 100ms 节拍,默认 3s
+        try:
+            ai_gpio = "GPIO22"
+            pwk_gpio = "GPIO26"
+            bt_wakeup_en = False
+            wakeup_cta1_gpio = None
+            wakeup_bt_gpio = None
+            if "vc_config" in globals() and vc_config:
+                raw_ai = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22")
+                if raw_ai is None or raw_ai == "":
+                    ai_gpio = None
+                else:
+                    ai_gpio = raw_ai
+                pwk_gpio = vc_config.get_attr("PWK_KEY_GPIO", "GPIO26") or "GPIO26"
+                bt_wakeup_en = bool(vc_config.get_attr("BT_WAKEUP_4G_ENABLED", False))
+                wakeup_cta1_gpio = vc_config.get_attr("EXTINT_WAKEUP_CTA1_GPIO", None)
+                wakeup_bt_gpio = vc_config.get_attr("PIN_WAKEUP_BT_GPIO", None)
+                try:
+                    self._single_power_ai_merged = bool(
+                        vc_config.get_bool("SINGLE_POWER_KEY_AI_MERGED", False)
+                    ) or (ai_gpio is None)
+                except Exception:
+                    self._single_power_ai_merged = ai_gpio is None
+                try:
+                    _pwk_ms = int(vc_config.get_int("PWK_LONG_PRESS_SHUTDOWN_MS", 3000))
+                except Exception:
+                    _pwk_ms = 3000
+                self._pwk_shutdown_ticks = max(1, (_pwk_ms + 99) // 100)
+        except Exception:
+            ai_gpio = "GPIO22"
+            pwk_gpio = "GPIO26"
+            bt_wakeup_en = False
+            wakeup_cta1_gpio = None
+            wakeup_bt_gpio = None
+
+        if ai_gpio:
+            self.extint_AI = ExtInt(
+                getattr(ExtInt, ai_gpio),
+                ExtInt.IRQ_RISING_FALLING,
+                ExtInt.PULL_PU,
+                self.keyAI_extint_cb,
+            )
+            self.extint_AI.enable()
+        else:
+            self.extint_AI = None
+
+        self.pwk_key = ExtInt(getattr(ExtInt, pwk_gpio), ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_PU, self.pwk_extint_cb)
+
+        if bt_wakeup_en:
+            try:
+                if wakeup_cta1_gpio:
+                    self.extint_wakeup_cta1 = ExtInt(getattr(ExtInt, wakeup_cta1_gpio), ExtInt.IRQ_RISING, ExtInt.PULL_DISABLE, self.wakeup_extint_cb)
+                if wakeup_bt_gpio:
+                    self.PIN_wakeup_bt = Pin(getattr(Pin, wakeup_bt_gpio), Pin.OUT, Pin.PULL_DISABLE, 0)
+                self.extint_wakeup_cta1_timer = osTimer()
+                if hasattr(self, "extint_wakeup_cta1"):
+                    self.extint_wakeup_cta1.enable()
+            except Exception:
+                pass
+
+        self.pwk_key_status = 0
+        self.pwk_key_timer_count = 0
+        self.pwk_key_long_press = 0
+        self.pwk_key_press = 0
+        self.powerKey_status = 0
+        self.pwk_key_timer = osTimer()
+        self.pwk_is_double_timer = osTimer()
+        self.double_click_threshold = 1000#双击间隔时间
+        self.pwk_press_count = 0#按下次数
+        self.last_key_time = 0          # 上一次有效触发时间
+        self.key_debounce_interval = 20 # 防抖窗口20ms,标准值
+        #self.pwk_key = ExtInt(ExtInt.GPIO26, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_DISABLE, self.pwk_extint_cb)
+        
+        self.pwk_key.enable() 
+        print("AI_dev:1")
+        self.btnInfo_queue = Queue(100)
+        self.vc_prio_queue = Queue(32)
+        self.btnInfo_queue_count = 0
+        _thread.stack_size(16*1024)
+        self.btnInfo_queue_thread_id = _thread.start_new_thread(self.btnInfo_queue_thread, ())
+        _thread.stack_size(8*1024)
+        EventMesh.subscribe("publish_AI_btnInfo",self.publish_AI_btnInfo)
+        EventMesh.subscribe("vc_orchestrator_purge", self._vc_orchestrator_purge)
+        EventMesh.subscribe("get_battery",self.get_battery)
+        EventMesh.subscribe("audio_montage_play",self.audio_montage_play)
+        # EventMesh.subscribe("paly_iccid_imei",self.paly_iccid_imei)
+        EventMesh.subscribe("paly_device_res_error",self.paly_device_res_error)
+        EventMesh.subscribe("Power_off",self.pwk_key_Long_Press)
+        EventMesh.subscribe("wakeup_BT",self.wakeup_BT)
+        EventMesh.subscribe("sleep_BT",self.sleep_BT)
+        EventMesh.subscribe("sys_sleep_cb",self.sys_sleep_cb)#系统休眠前回调
+        EventMesh.subscribe("sys_up_cb",self.sys_up_cb)#系统唤醒后回调
+        
+        EventMesh.subscribe("GET_USB_STATE",self.get_usb_state)
+        EventMesh.subscribe("SET_SPEECH_RATE",self.set_speech_rate)
+        EventMesh.subscribe("rest_TB",self.rest_TB)
+        
+        print("AI_dev:2")
+        self.pk = PowerKey()
+        self.pk.powerKeyEventRegister(self.powerKey_callback)
+        self.usb = USB()
+        self.usb.setCallback(self.usb_event_cb)
+        self.usb_status = self.usb.getStatus()
+        self.usb_charge_flag = 0
+        self._usb_evt_last_ms = 0
+        self._usb_evt_last_state = -1
+        self.Low_power_warning_timer = 0
+        self.AI_dev_timer = osTimer()
+        self.AI_dev_timer.start(10000, 1, self.AI_dev_timer_cb)
+        print("AI_dev:AI_dev_timer")
+        self.aud = audio.Audio(0)
+        self.Low_power_warning_flag = 0
+        self._low_warn_last_ms = 0
+        self._empty_shutdown_done = 0
+        self.VC_TTS_volume =  EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置
+        if self.VC_TTS_volume == None:
+            self.VC_TTS_volume = 100
+        self.aud.setVolume(self.VC_TTS_volume //10)
+        EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
+        #self.aud.play(1,0,'U:/static/xianyikaiji.mp3')#开机音频
+        #EventMesh.publish("audio_montage_play",["xianyikaiji.mp3"])
+        #EventMesh.publish("publish_AI_btnInfo",{"func":self.ntptime_cb})
+        self.check_rest_timer_count = 0
+        try:
+            if "vc_config" in globals() and vc_config and vc_config.uart_supported():
+                self.ai_uart = AI_UART()
+        except Exception:
+            pass
+
+        self.Keycall1_press_count = 0
+        self.ble_switch_flag_time = osTimer()
+        self.ble_switch_flag_time.start(20000, 0, self.ble_switch_flag_time_init)
+        # AI_hw_status 先于本模块初始化,开机已插 USB 时需补同步一次
+        try:
+            if int(self.usb_status or 0) == 1:
+                EventMesh.publish("hw_usb_state", 1)
+                EventMesh.publish("publish_AI_btnInfo", {"func": self.usb_event, "data": 1})
+        except Exception:
+            pass
+    def ble_switch_flag_time_init(self,*args):
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.ble_switch_flag_time_cb})
+    def ble_switch_flag_time_cb(self,*args):
+        bt_switch_flag =  EventMesh.publish("get_storage_config","switch_bt_state")#bt开关
+        if bt_switch_flag == None:
+            bt_switch_flag = "1"
+        EventMesh.publish("send_bt_switch_toble",str(bt_switch_flag))
+        # EventMesh.publish("send_imei_to_ble")
+        ble_switch_flag =  EventMesh.publish("get_storage_config","switch_ble_state")#ble开关
+        if ble_switch_flag == None:
+            ble_switch_flag = "1"
+        EventMesh.publish("send_ble_switch_toble",str(ble_switch_flag))
+        vc_tts_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")
+        if vc_tts_volume == None:
+            vc_tts_volume=100
+        # EventMesh.publish("adjust_volume_toBle",str(vc_tts_volume))
+
+        pass
+        print("ble_switch_flag_time_cb!!!!!!!")
+    def ntptime_cb(self):
+        #ntptime.settime(8)
+        print("timer:{}!!!!!!!!\r\n".format(utime.localtime()))
+    def usb_event_cb(self, state):
+        # USB 插拔抖动过滤(日志中 powerKey 与 usb_evt 0/1 连跳会导致误播 OUT/IN)
+        try:
+            now = utime.ticks_ms()
+            last_ms = getattr(self, "_usb_evt_last_ms", 0)
+            last_st = getattr(self, "_usb_evt_last_state", -1)
+            if last_ms and utime.ticks_diff(now, last_ms) < 800 and last_st != state:
+                return
+            self._usb_evt_last_ms = now
+            self._usb_evt_last_state = state
+        except Exception:
+            pass
+        # 转发给精简硬件监控(AI_hw_status),避免 USB callback 被覆盖导致“后面失效”
+        try:
+            EventMesh.publish("hw_usb_state", state)
+        except Exception:
+            pass
+        try:
+            _BatteryCfg.instance().on_usb_change(1 if state == 1 else 0)
+        except Exception:
+            pass
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.usb_event,"data":state})
+
+    def usb_event(self, state):    
+        status = state
+        self.usb_charge_flag = 0
+        if status == 1:#插入USB
+            print("charge R_CHARGE_in")
+            self.usb_status = 1
+            VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行
+            if VC_satrt_flag == None:
+                VC_satrt_flag = 0
+            
+            voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行
+            if voice_record_satrt_flag == None:
+                voice_record_satrt_flag = 0
+            if VC_satrt_flag == 0 and voice_record_satrt_flag == 0:
+                self.aud.stop()
+                #EventMesh.publish("flicker_led_red")#充电闪灯
+                #self.aud.play(1,1,'U:/static/R_CHARGE_IN.mp3')
+                try:
+                    if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
+                        EventMesh.publish("set_led_state", {"charging": "charging"})
+                except Exception:
+                    pass
+                EventMesh.publish("audio_montage_play",["R_CHARGE_IN.mp3"])
+            self.Low_power_warning_flag  = 0
+            try:
+                self._empty_shutdown_done = 0
+            except Exception:
+                pass
+            pass
+        elif status == 0:#拨出usbs
+            self.usb_status = 0
+            VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行
+            if VC_satrt_flag == None:
+                VC_satrt_flag = 0
+            voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行
+            if voice_record_satrt_flag == None:
+                voice_record_satrt_flag = 0
+            if VC_satrt_flag == 0 and voice_record_satrt_flag == 0:
+                self.aud.stop()
+                try:
+                    if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
+                        EventMesh.publish("set_led_state", {"charging": "none"})
+                except Exception:
+                    pass
+                EventMesh.publish("audio_montage_play",["R_CHARGE_OUT.mp3"])
+                #self.aud.play(1,1,'U:/static/R_CHARGE_OUT.mp3')
+    def get_usb_state(self,event,msg):
+        return self.usb_status
+    
+    def set_speech_rate(self,event,msg):
+        #msg的取值是0.5~2.0
+        #而语速初始值是0,范围是-50~50
+        rate = 0
+        if msg == 0.5:
+            rate = -50
+        elif msg < 1:
+            rate = (msg - 1)* 100
+        elif msg >= 1:
+            rate = (msg - 1)* 50
+        rate = round(rate)
+        print("速率已经设置为:{}".format(rate))
+        EventMesh.publish("rate_change",rate)
+        return rate
+    def _vc_orchestrator_drop_stale(self, data):
+        if not data or "vc_track_gen" not in data:
+            return False
+        try:
+            cur = EventMesh.publish("vc_turn_gen_get")
+            if cur is None:
+                return False
+            if int(data["vc_track_gen"]) != int(cur):
+                print("[ORCH] skip stale queue gen={} cur={}".format(
+                    data["vc_track_gen"], cur))
+                return True
+        except Exception:
+            pass
+        return False
+
+    def _vc_orchestrator_drain_queue_gen(self, q, min_gen):
+        try:
+            n = q.size()
+        except Exception:
+            return
+        kept = []
+        for _ in range(n):
+            try:
+                item = q.get()
+            except Exception:
+                break
+            if (
+                item
+                and "vc_track_gen" in item
+                and int(item["vc_track_gen"]) < int(min_gen)
+            ):
+                continue
+            kept.append(item)
+        for item in kept:
+            try:
+                q.put(item)
+            except Exception:
+                pass
+
+    def _vc_orchestrator_purge(self, event=None, msg=None):
+        try:
+            min_gen = int(msg) if msg is not None else 0
+        except Exception:
+            min_gen = 0
+        self._vc_orchestrator_drain_queue_gen(self.btnInfo_queue, min_gen)
+        self._vc_orchestrator_drain_queue_gen(self.vc_prio_queue, min_gen)
+
+    def _vc_btn_queue_dispatch(self, data):
+        if data is None:
+            return
+        if self._vc_orchestrator_drop_stale(data):
+            return
+        try:
+            if "data" in data:
+                data["func"](data["data"])
+            else:
+                data["func"]()
+        except Exception as e:
+            if "data" in data:
+                print(data["data"])
+            usys.print_exception(e)
+
+    def btnInfo_queue_thread(self):
+        while True:
+            data = None
+            try:
+                if self.vc_prio_queue.empty() != True:
+                    data = self.vc_prio_queue.get()
+                else:
+                    data = self.btnInfo_queue.get()
+            except Exception:
+                try:
+                    data = self.btnInfo_queue.get()
+                except Exception:
+                    continue
+            self.btnInfo_queue_count -= 1
+            self._vc_btn_queue_dispatch(data)
+    def publish_AI_btnInfo(self,event,msg):
+        #print("publish_AI_btnInfo 1")
+        try:
+            if msg and msg.get("vc_prio"):
+                self.vc_prio_queue.put(msg)
+            else:
+                self.btnInfo_queue.put(msg)
+        except Exception:
+            self.btnInfo_queue.put(msg)
+        self.btnInfo_queue_count+=1
+        if self.btnInfo_queue_count > 80:
+            self.btnInfo_queue_count =0
+            print("事件处理线程队列出问题,重启!!!!!!!!!!!!!!")
+            print("线程:{} 队列:{}".format(_thread.threadIsRunning(self.btnInfo_queue_thread_id),self.btnInfo_queue.size()))
+            # if _thread.threadIsRunning(self.btnInfo_queue_thread_id):
+            #     _thread.stop_thread(self.btnInfo_queue_thread_id)
+            # _thread.stack_size(10*1024)
+            # self.btnInfo_queue_thread_id = _thread.start_new_thread(self.btnInfo_queue_thread, ())
+            # _thread.stack_size(8*1024)
+            # self.btnInfo_queue.clear()
+            # self.btnInfo_queue = Queue(100)
+            EventMesh.publish("VC_error") #播放报错
+            utime.sleep_ms(1000)
+            self.aud.stopPlayStream()
+            self.aud.stopAll()
+            Power.powerRestart()
+            EventMesh.publish("set_storage_config",["powerRestart",1])
+        #print("publish_AI_btnInfo 2")
+    
+    def sys_sleep_cb(self,event=None,msg=None):#休眠前回调
+        try:
+            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):
+                return
+        except Exception:
+            return
+        print("sys_sleep_cb!!!!!!!!!!!!!!!")
+        EventMesh.publish("sleep_BT")#休眠蓝牙
+        self.PIN_wakeup_bt.write(0)
+    def sys_up_cb(self,event=None,msg=None):#唤醒前回调
+        try:
+            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):
+                return
+        except Exception:
+            return
+        print("sys_up_cb!!!!!!!!!!!!!!!")
+        self.wakeup_BT()
+        self.PIN_wakeup_bt.write(1)
+    def rest_TB(self,event=None,msg=None):
+        print("rest_TB!!!!!!!!!!!!!!!")
+        self.PIN_BT_ON.write(0)
+        utime.sleep_ms(100)
+        self.PIN_BT_ON.write(1)
+    def wakeup_BT(self,event=None,msg=None):#4G唤醒蓝牙
+        try:
+            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):
+                return
+        except Exception:
+            return
+        print("wakeup_BT!!!!!!!!!!!!!!!")
+        wakeup_cta1_io = self.extint_wakeup_cta1.read_level()
+        if wakeup_cta1_io == 1:
+            return 1
+        self.PIN_wakeup_bt.write(0)
+        count = 5
+        while(True):
+            count-=1
+            if count<=0:
+                return -1
+            wakeup_cta1_io = self.extint_wakeup_cta1.read_level()
+            print("bt io状态:{}".format(wakeup_cta1_io))
+            if wakeup_cta1_io == 1:
+                self.PIN_wakeup_bt.write(1)
+                return 1
+            utime.sleep_ms(100)
+            if count%2 == 0:
+                self.PIN_wakeup_bt.write(1)
+            else:
+                self.PIN_wakeup_bt.write(0)
+
+        
+    def sleep_BT(self,event=None,msg=None):#使蓝牙休眠
+        try:
+            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):
+                return
+        except Exception:
+            return
+        print("sleep_BT!!!!!")
+        print("sleep_BT!!!!!")
+        print("sleep_BT!!!!!")
+        print("sleep_BT!!!!!")
+        self.PIN_wakeup_bt.write(0)
+        count = 300
+        while(True):
+            count-=1
+            if count<=0:
+                return -1
+            if self.extint_wakeup_cta1.read_level() == 0:
+                return 1
+            utime.sleep_ms(1)
+   
+   
+    def wakeup_extint_cb(self,args):#蓝牙唤醒4g
+        try:
+            if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):
+                return
+        except Exception:
+            return
+        print("wakeup_extint_cb:!!!!!!!!!")
+        EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+        self.PIN_wakeup_bt.write(1)
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.extint_wakeup_cta1_cb})
+    def extint_wakeup_cta1_cb(self):
+        #self.extint_wakeup_cta1_timer.start(100, 1, self.extint_wakeup_cta1_timer_cb)
+        pass
+    def extint_wakeup_cta1_timer_cb(self, *args, **kwargs):
+        if self.extint_wakeup_cta1.read_level() == 0:
+            self.extint_wakeup_cta1_timer.stop()
+            #self.PIN_wakeup_bt.write(0)
+
+    def powerKey_callback(self, status):
+        try:
+            EventMesh.debug_log("==pwr:", status)
+        except Exception:
+            print("powerKey_callback:{}".format(status))
+        self.powerKey_status = status
+        if self.pwk_key_press == 1:
+            return
+
+    def pwk_extint_cb(self,args):#pwk
+        current_time = 0
+        interval = 0
+
+        try:
+            #按键电平判断
+            press_level = 0
+            current_level = self.pwk_key.read_level()
+            if current_level != press_level:
+                return  # 非按下状态,直接返回
+
+            #消抖处理
+            current_time = utime.ticks_ms()
+            if utime.ticks_diff(current_time, self.last_key_time) < self.key_debounce_interval:
+                return
+            self.last_key_time = current_time  # 更新最后按键时间
+            print("pwk 按键按下")
+
+            #仅处理"按下瞬间"事件(避免重复触发)
+            if len(args) < 2:
+                print("args参数不足,跳过")
+                return
+            self.pwk_key_status = int(args[1])
+            if self.pwk_key_status != 1:
+                return
+
+            #双击计数逻辑
+            self.pwk_press_count += 1
+            current_time = utime.ticks_ms()
+
+            #第一次按下(启动长按定时器+双击等待)
+            if self.pwk_press_count == 1:
+                self.pwk_first_press_time = current_time  # 记录首次按下时间
+                self.pwk_key_long_press = 0               # 重置长按标记
+                self.pwk_key_timer_count = 0              # 重置定时器计数
+                self.pwk_key_press = 1                    # 标记按键按下
+                
+                # 停止旧定时器,启动长按/单击判定定时器(100ms单次触发,循环靠回调自启)
+                self.pwk_key_timer.stop()
+                self.pwk_key_timer.start(100, 1, self.pwk_key_timer_cb)
+                print("第一次按下,等待第二次按下")
+                if getattr(self, "_single_power_ai_merged", False):
+                    self._merged_pwk_ai_press_start()
+
+            #第二次按下(双击判断)
+            elif self.pwk_press_count == 2:
+                # 计算两次按下间隔
+                interval = utime.ticks_diff(current_time, self.pwk_first_press_time)
+                
+                #符合双击阈值:执行双击逻辑
+                if interval <= self.double_click_threshold:
+                    self.pwk_key_timer.stop()  # 停止长按定时器(关键:避免触发长按)
+                    self.pwk_key_double_press() # 执行双击业务
+                    print("双击生效")
+                    # 重置所有状态
+                    self.pwk_press_count = 0
+                    self.pwk_key_press = 0
+                    self.pwk_key_timer_count = 0
+
+                #间隔过长:视为新的第一次按下
+                else:
+                    self.pwk_first_press_time = current_time  # 更新首次按下时间
+                    self.pwk_press_count = 1                  # 重置为第一次按下
+                    print("间隔过长,视为新的第一次按下")
+
+            #按下次数超过2次(防计数溢出)
+            else:
+                self.pwk_press_count = 0
+                self.pwk_key_timer.stop()
+                print("按键次数过多,重置计数")
+
+        except Exception as e:
+            # 异常时重置所有状态,避免卡死
+            self.pwk_press_count = 0
+            self.pwk_key_press = 0
+            self.pwk_key_timer_count = 0
+            self.pwk_key_timer.stop()
+            usys.print_exception(e)
+
+
+    def pwk_is_double_judge(self, *args):
+        # if self.press_count == 1:  # 仅一次按下,判定为单击
+            
+        # 重置计数(无论何种情况,超时后清零)
+        self.pwk_press_count = 0
+
+
+    def pwk_key_timer_cb(self, *args, **kwargs):
+        EventMesh.publish("set_wait_first_flag",0)
+        EventMesh.publish("set_history_board_flag",0)
+        EventMesh.publish("set_play_new_voice_state",0)
+        EventMesh.publish("set_history_voice_flag",0)
+        EventMesh.publish("set_wait_voice_flag",0)
+        EventMesh.publish("set_stop_play",0)
+    #     EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_timer_cb_})
+    # def pwk_key_timer_cb_(self) :
+        try:
+            self.pwk_key_status = self.pwk_key.read_level()
+            self.pwk_key_timer_count +=1
+            if self.pwk_key_timer_count >= self._pwk_shutdown_ticks:
+                #print("pwk_key_long_press {}".format(self.pwk_key_long_press))
+                if self.pwk_key_long_press == 0:
+                        self.pwk_key_long_press = 1
+                        #EventMesh.publish("AI_key_long_press")
+                        # EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])
+                        self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频
+                        EventMesh.publish("set_led_state", {"battery": "empty"})
+                        utime.sleep_ms(2000)
+                        self.aud.stopPlayStream()
+                        self.aud.stopAll()
+                        Power.powerDown()
+                        #print("AI_key_release")
+                        #EventMesh.publish("pwk_key_long_press")
+                        #EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_Long_Press})  
+            if self.pwk_key_status == 1:#松开
+                #print("pwk_key_timer_cb:{}".format(self.pwk_key_timer_count))
+                try:
+                    key_count = vc_config.get_int("KEY_RELEASE_DEBOUNCE_COUNT", 5)
+                except Exception:
+                    key_count = 5
+                if self.pwk_key_timer_count >= key_count: #防抖200ms
+                    #EventMesh.publish("AI_key_release")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_release})
+                    self.pwk_key.enable()
+                    self.pwk_key_timer_count = 0
+                    self.pwk_key_press = 0
+                    self.pwk_press_count = 0#按下次数
+                    self.pwk_key_timer.stop()
+            
+        except Exception as e: 
+            usys.print_exception(e) 
+            
+    def _ai_int_disable_for_hold(self):
+        if self.extint_AI is not None:
+            self.extint_AI.disable()
+
+    def _ai_int_enable_after_hold(self):
+        if self.extint_AI is not None:
+            self.extint_AI.enable()
+
+    def _key_ai_read_level(self):
+        if self.extint_AI is not None:
+            return self.extint_AI.read_level()
+        return self.pwk_key.read_level()
+
+    def _merged_pwk_ai_press_start(self):
+        """单电源键:按下瞬间走原 AI 键按下链路(松手走 AI_key_release,不按电源短按 Close)。"""
+        try:
+            current_time = utime.ticks_ms()
+            if utime.ticks_diff(current_time, self.last_ai_key_time) < 1:
+                return
+            self.last_ai_key_time = current_time
+            if EventMesh.publish("get_wait_voice_flag") == 1:
+                print("播放未读")
+                EventMesh.publish("set_wait_voicerupt_flag", 1)
+                return
+            if EventMesh.publish("get_recv_voice") == 1:
+                print("收到留言")
+                EventMesh.publish("set_voicerupt_flag", 1)
+                return
+            if self.KeyAI_press == 0:
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)
+                if EventMesh.publish("get_play_new_voice_state") == 1:
+                    EventMesh.publish("set_play_new_voice_state", 0)
+                EventMesh.publish("set_VC_stop_play_audio_flag", 1)
+                self._ai_int_disable_for_hold()
+                self.KeyAI_press = 1
+                self.KeyAI_long_press = 0
+                self.KeyAI_timer.start(100, 1, self.KeyAI_timer_cb)
+                self.KeyAI_timer_count = 0
+                EventMesh.publish("vc_long_dialogue_key_down")
+                EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})
+        except Exception as e:
+            usys.print_exception(e)
+
+    def keyAI_extint_cb(self,args):#call 2
+        try:
+            if self.extint_AI is None:
+                return
+            #print("AI_btn:keyAI_extint_cb{}".format(args[1]))
+            #定义最短触发间隔
+            MIN_TRIGGER_INTERVAL = 1  # 可根据需求调整,单位ms
+            #记录上次按键触发时间(初始化放类的__init__里:self.last_ai_key_time = 0)
+            current_time = utime.ticks_ms()  # 获取当前毫秒时间
+            #重复按下,直接返回
+            if utime.ticks_diff(current_time, self.last_ai_key_time) < MIN_TRIGGER_INTERVAL:
+                print("AI按键触发过频,忽略本次操作")
+                return
+            #更新上次触发时间
+            self.last_ai_key_time = current_time
+
+            self.KeyAI_status = int(args[1])
+            if self.KeyAI_status == 0:
+                self.KeyAI_status = 1
+            else:
+                self.KeyAI_status = 0
+            
+            if self.KeyAI_status == 0:#按下
+                if EventMesh.publish("get_wait_voice_flag") == 1:
+                    print("播放未读")
+                    EventMesh.publish("set_wait_voicerupt_flag",1)
+                    return
+                if EventMesh.publish("get_recv_voice") == 1:#当处于收到留言的时候
+                    print("收到留言")
+                    EventMesh.publish("set_voicerupt_flag",1)
+                    return
+                if self.KeyAI_press == 0 :
+                    EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+                    if EventMesh.publish("get_play_new_voice_state") == 1:
+                        EventMesh.publish("set_play_new_voice_state",0)
+                    
+                    EventMesh.publish("set_VC_stop_play_audio_flag",1)#按键按下不允许播放之前下发的音频数据
+                    self._ai_int_disable_for_hold()
+                    self.KeyAI_press = 1
+                    self.KeyAI_long_press = 0
+                    self.KeyAI_timer.start(100, 1, self.KeyAI_timer_cb)
+                    self.KeyAI_timer_count = 0
+                    #EventMesh.publish("AI_key_press")
+                    # if dev_type == "7PIN":
+                        # self.aud.aud_tone_play(10,100)#播放按键音
+                        
+                    EventMesh.publish("vc_long_dialogue_key_down")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press})  
+        except Exception as e: 
+            usys.print_exception(e)
+    def KeyAI_timer_cb(self, *args, **kwargs):
+    #     EventMesh.publish("publish_AI_btnInfo",{"func":self.KeyAI_timer_cb_})
+    # def KeyAI_timer_cb_(self):
+        try:
+            self.KeyAI_status = self._key_ai_read_level()
+            self.KeyAI_timer_count +=1
+            if self.KeyAI_timer_count >= 10*1 : #长按1s 
+               
+                if self.KeyAI_long_press == 0:
+                    self.KeyAI_long_press = 1
+                    #EventMesh.publish("AI_key_long_press")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_long_press})  
+            try:
+                _sos_on = vc_config.get_bool("SOS_ENABLED", True)
+            except Exception:
+                _sos_on = True
+            if _sos_on and self.KeyAI_timer_count >= 10*3 : #长按3s 上一次是短按则拨打sos紧急联系人
+                print("long sos phone:{}".format(self.KeySOS_timer_flag))
+                
+                if self.KeySOS_timer_flag == 1:
+                    self.KeySOS_timer_flag=0
+                    self.KeySos_long_press = 1
+                    self.KeySOS_timer.stop()
+                    get_sos_flag = EventMesh.publish("get_sos_flag")
+                    if get_sos_flag == 0:
+                        EventMesh.publish("set_sos_info_call")
+                    pass
+
+            elif self.KeyAI_timer_count >= 10*8:#关机
+                try:
+                    allow_power_off = vc_config.get_bool("AI_LONG_PRESS_POWER_OFF_ENABLED", False)
+                except Exception:
+                    allow_power_off = False
+                if allow_power_off:
+                    EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])
+                    #self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频
+                    EventMesh.publish("set_led_state", {"battery": "empty"})
+                    utime.sleep_ms(2000)
+                    self.aud.stopPlayStream()
+                    print("stopAll:8")
+                    self.aud.stopAll()
+                    Power.powerDown()
+            if self.KeyAI_status == 1:#松开
+                #print("KeyAI_timer_cb:{}".format(self.KeyAI_timer_count))
+                try:
+                    key_count = vc_config.get_int("KEY_RELEASE_DEBOUNCE_COUNT", 5)
+                except Exception:
+                    key_count = 5
+                if self.KeyAI_timer_count >= key_count: #防抖200ms
+                    if self.KeySOS_timer_flag == 1:
+                        self.KeySOS_timer_flag = 0
+                        self.KeySos_long_press = 0
+                        self.KeySOS_timer.stop()
+                        print("KeySOS_timer_flag!!!!!!!!!!!!!!!")  #短按两次需要清除标识
+
+                    if self.KeyAI_long_press == 0:
+                        try:
+                            _sos_on = vc_config.get_bool("SOS_ENABLED", True)
+                        except Exception:
+                            _sos_on = True
+                        if _sos_on and self.KeySOS_timer_flag == 0:
+                            self.KeySOS_timer_flag = 1
+                            self.KeySos_long_press = 0
+                            self.KeySOS_timer.start(5000, 0, self.KeySos_timer_cb) #短按开启5s定时器 5s之内没有长按3s则清除标识 长按3s拨打sos紧急联系人
+                    else:
+                        self.KeySOS_timer_flag = 0
+                        self.KeySos_long_press = 0
+                        self.KeySOS_timer.stop()
+                        
+                    #EventMesh.publish("AI_key_release")
+                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_release})  
+                    self._ai_int_enable_after_hold()
+                    self.KeyAI_timer.stop()
+                    self.KeyAI_timer_count = 0
+                    self.KeyAI_press = 0
+                    self.KeyAI_long_press=0
+            
+        except Exception as e: 
+            usys.print_exception(e) 
+       
+    def AI_key_press(self):
+        # EventMesh.publish("wakeup_BT")
+        
+        # if dev_type == "7PIN" or dev_type == "7PIN_coze" :
+        #     # self.aud.aud_tone_play(10,60)#播放按键音
+        #     EventMesh.publish("audio_montage_play",["chenggong.mp3"])  
+        self.check_rest_timer_count = 0
+        print("AI_key_press")
+        EventMesh.publish("AI_key_press")
+    def KeySos_timer_cb(self,*argc):
+        print("KeySos_timer_cb!!!!!!!!!!{} {}".format(self.KeySos_long_press,self.KeySOS_timer_flag))
+        
+        if self.KeySos_long_press == 1 and self.KeySOS_timer_flag == 1:  #清空sos标识
+            self.KeySos_long_press=0
+            self.KeySOS_timer_flag=0
+           
+        self.KeySos_long_press=0
+        self.KeySOS_timer_flag = 0
+        return
+        self.KeySOS_timer_flag = 0
+        if self.KeySOS_timer_count >= 3:
+            self.KeySOS_timer_count = 0
+            get_sos_flag = EventMesh.publish("get_sos_flag")
+            if get_sos_flag == 0:
+                EventMesh.publish("set_sos_info_call")
+        self.KeySOS_timer_count = 0
+        pass
+
+    def AI_key_long_press(self):
+        self.check_rest_timer_count = 0
+        print("AI_key_long_press")
+
+        EventMesh.publish("AI_key_long_press")
+        
+    def AI_key_release(self):
+        print("AI_key_release")
+        # self.KeySOS_timer_count +=1
+        # print("self.KeySOS_timer_count:{} {}".format(self.KeySOS_timer_count,self.KeySOS_timer_flag))
+        # if self.KeySOS_timer_flag == 0:
+        #     self.KeySOS_timer_flag = 1
+        #     self.KeySOS_timer.start(5000, 0, self.KeySos_timer_cb)
+        EventMesh.publish("AI_key_release")
+        
+    def pwk_key_double_press(self):
+        self.check_rest_timer_count = 0
+        try:
+            if getattr(self, "_single_power_ai_merged", False):
+                try:
+                    self.KeyAI_timer.stop()
+                except Exception:
+                    pass
+                self.KeyAI_timer_count = 0
+                self.KeyAI_press = 0
+                self.KeyAI_long_press = 0
+                self._ai_int_enable_after_hold()
+        except Exception:
+            pass
+        print("pwk_key_double_press")
+        EventMesh.publish("pwk_key_double_press")
+
+    def pwk_key_Long_Press(self,event=None,msg=None):
+        self.check_rest_timer_count = 0
+        print("pwk_key_long_press!!!!!!!")
+        print("pwk_key_long_press!!!!!!!")
+        
+        # EventMesh.publish("paly_iccid_imei")
+        # return
+        EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])
+        #self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频
+        EventMesh.publish("set_led_state", {"battery": "empty"})
+        utime.sleep_ms(3000)
+        self.aud.stopPlayStream()
+        self.aud.stopAll()
+        Power.powerDown()
+        #print("AI_key_release")
+        #EventMesh.publish("pwk_key_long_press")
+        
+    def pwk_key_release(self):
+        self.check_rest_timer_count = 0
+        #print("pwk_key_release")
+        EventMesh.publish("pwk_key_release")
+
+    
+    def wdt_timer_cb(self, *args, **kwargs):
+        if self.is_wdt == 1:
+            self.wdt.feed()
+
+    def AI_dev_timer_cb(self, *args, **kwargs):
+        #print("AI_dev_timer_cb")
+        EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_dev_timer_event})  
+    def _battery_empty_shutdown_seq(self):
+        """REQ-BAT-03:红快闪 3 次 → 灭灯 → 关机提示音 → 关机(btn 线程执行)。"""
+        try:
+            EventMesh.publish("battery_priority_mute_vc")
+        except Exception:
+            pass
+        try:
+            n, ms = 3, 500
+            if "vc_config" in globals() and vc_config:
+                n = int(vc_config.get_int("BATTERY_EMPTY_FLASH_COUNT", 3))
+                ms = int(vc_config.get_int("BATTERY_EMPTY_FLASH_PERIOD_MS", 500))
+            EventMesh.publish("set_led_state", {"alert": "red_flash_3"})
+            utime.sleep_ms(n * ms * 2 + 200)
+            EventMesh.publish("set_led_state", {"ai_state": "idle", "battery": "normal", "charging": "none"})
+        except Exception:
+            pass
+        try:
+            EventMesh.publish("audio_montage_play", ["didianguanji.mp3"])
+            utime.sleep_ms(4500)
+            self.aud.stopPlayStream()
+            self.aud.stopAll()
+        except Exception:
+            pass
+        try:
+            Power.powerDown()
+        except Exception:
+            pass
+
+    def AI_dev_timer_event(self):
+        #EventMesh.publish("audio_montage_play",["liuliangtishi.mp3"])
+        EventMesh.publish("send_power_init_data")
+        if self.is_wdt == 1:
+            self.wdt.feed()
+        VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行
+        if VC_satrt_flag == None:
+            VC_satrt_flag = 0
+        voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行
+        if voice_record_satrt_flag == None:
+            voice_record_satrt_flag = 0
+        _bcfg = _BatteryCfg.instance()
+        Vbatt = _bcfg.get_vbatt_mv_charge() if self.usb_status == 1 else _bcfg.get_vbatt_mv()
+        if self.usb_status == 1 and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:
+            try:
+                if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
+                    EventMesh.publish("set_led_state", {"charging": "charging"})#充电红灯常亮
+            except Exception:
+                pass
+        if self.usb_status == 1 and Vbatt >= _bcfg.charge_full_mv and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:
+            try:
+                if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
+                    EventMesh.publish("set_led_state", {"charging": "full"})
+            except Exception:
+                pass
+            if self.usb_charge_flag == 0 :
+                EventMesh.publish("audio_montage_play",["R_CHARGE_FULL.mp3"])
+                #self.aud.play(1,0,'U:/static/R_CHARGE_FULL.mp3')#充电已满
+                self.usb_charge_flag = 1
+        try:
+            low_soc = int(_bcfg.low_soc)
+        except Exception:
+            low_soc = 25
+        warn_interval_ms = 300000
+        try:
+            if "vc_config" in globals() and vc_config:
+                warn_interval_ms = int(vc_config.get_int("BATTERY_LOW_WARN_INTERVAL_MS", 300000))
+        except Exception:
+            pass
+        soc = None
+        try:
+            soc = self.get_battery()
+        except Exception:
+            soc = None
+        if self.usb_status == 0:
+            if _bcfg.should_shutdown_empty(Vbatt):
+                if getattr(self, "_empty_shutdown_done", 0) == 0:
+                    self._empty_shutdown_done = 1
+                    try:
+                        print("[BAT] empty shutdown vbatt={} soc={} empty_mv={}".format(
+                            Vbatt, soc, _bcfg.empty_mv))
+                    except Exception:
+                        pass
+                    EventMesh.publish("publish_AI_btnInfo", {"func": self._battery_empty_shutdown_seq})
+            elif soc is not None and soc < low_soc:
+                if voice_record_satrt_flag == 0:
+                    try:
+                        now = utime.ticks_ms()
+                        last = getattr(self, "_low_warn_last_ms", 0)
+                        if (not last) or utime.ticks_diff(now, last) >= warn_interval_ms:
+                            self._low_warn_last_ms = now
+                            try:
+                                print("[BAT] low warn soc={} interval_ms={}".format(soc, warn_interval_ms))
+                            except Exception:
+                                pass
+                            EventMesh.publish("audio_montage_play", ["Low_power_warning.mp3"])
+                    except Exception:
+                        pass
+            else:
+                try:
+                    self._low_warn_last_ms = 0
+                    self._empty_shutdown_done = 0
+                except Exception:
+                    pass
+                if VC_satrt_flag == 0 and Vbatt is not None and Vbatt >= int(_bcfg.low_mv):
+                    try:
+                        if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
+                            EventMesh.publish("set_led_state", {"charging": "none", "battery": "normal"})
+                    except Exception:
+                        pass
+
+        self.check_rest_timer_count += 1
+        if self.check_rest_timer_count >= 8640:#24小时没按键,则关机
+            self.pwk_key_Long_Press()
+        
+    def get_battery(self, event=None, msg=None):
+        # SoC 基于 _BatteryCfg 10 点滑动平均电压,与 protocol 心跳/AI_hw 一致
+        try:
+            return _BatteryCfg.instance().soc_now()
+        except Exception:
+            return 0
+    def _audio_is_battery_priority(self, msg):
+        try:
+            for i in (msg or []):
+                n = str(i)
+                if ("Low_power" in n) or ("didianguanji" in n):
+                    return True
+        except Exception:
+            pass
+        return False
+
+    def audio_montage_play(self,event,msg):
+        self.wakeup_BT()
+        bat_pri = self._audio_is_battery_priority(msg)
+        if bat_pri:
+            try:
+                EventMesh.publish("battery_priority_mute_vc")
+            except Exception:
+                pass
+        else:
+            # 仅服务端 TTS PCM 在播时跳过;低电/关机提示优先级更高(见上)
+            try:
+                if EventMesh.publish("get_vc_tts_playback_active") == 1:
+                    print("[audio-arb] skip local montage_play during tts_pcm")
+                    return
+            except Exception:
+                pass
+        self.aud.stopPlayStream()
+        self.aud.stopAll()
+        str_data='mp3files='
+        for i in msg:
+            try:
+                str_tmp = i.replace(".mp3","")
+                str_data+="{}.mp3+".format(audio_buf.index(str_tmp))
+            except Exception as e:
+                    usys.print_exception(e)    
+        str_data = str_data[:-1]
+        print("audio_montage_play:{}".format(str_data))
+        self.aud.montage_play('U:/static/audio.bin',1,1,str_data)
+        self.play_imei_timer_count=1
+        self.play_imei_timer.stop()
+        self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)
+        
+    def paly_iccid_imei(self,event,msg):
+        
+        self.wakeup_BT()
+        self.aud.stopPlayStream()
+        self.aud.stopAll()
+        str_data = 'mp3files={}.mp3+'.format(audio_buf.index("liuliangtishi"))#流量提示
+        imei  = modem.getDevImei()
+        iccid = sim.getIccid()
+        print("paly_iccid_imei:ICCID:{}".format(iccid))
+        if iccid != -1 and len(iccid) > 5:
+            iccid = iccid.upper()
+            if len(iccid) <20:
+                for i in range(20-len(iccid)):
+                    iccid = "0"+iccid
+            str_data += '{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+'.format(audio_buf.index("R_READ_TIME_{}".format(iccid[0]))\
+                ,audio_buf.index("R_READ_TIME_{}".format(iccid[1])),audio_buf.index("R_READ_TIME_{}".format(iccid[2])),audio_buf.index("R_READ_TIME_{}".format(iccid[3])),\
+                audio_buf.index("R_READ_TIME_{}".format(iccid[4])),audio_buf.index("R_READ_TIME_{}".format(iccid[5])),audio_buf.index("R_READ_TIME_{}".format(iccid[6])),audio_buf.index("R_READ_TIME_{}".format(iccid[7])),\
+                audio_buf.index("R_READ_TIME_{}".format(iccid[8])),audio_buf.index("R_READ_TIME_{}".format(iccid[9])),audio_buf.index("R_READ_TIME_{}".format(iccid[10])),audio_buf.index("R_READ_TIME_{}".format(iccid[11])),\
+                audio_buf.index("R_READ_TIME_{}".format(iccid[12])),audio_buf.index("R_READ_TIME_{}".format(iccid[13])),audio_buf.index("R_READ_TIME_{}".format(iccid[14])),audio_buf.index("R_READ_TIME_{}".format(iccid[15])),\
+                audio_buf.index("R_READ_TIME_{}".format(iccid[16])),audio_buf.index("R_READ_TIME_{}".format(iccid[17])),audio_buf.index("R_READ_TIME_{}".format(iccid[18])),audio_buf.index("R_READ_TIME_{}".format(iccid[19])))
+        str_data += '{}.mp3+'.format(audio_buf.index("shouhou"))#可联系售后解决,联系方式见售后客服电话是400-886-8777或到微信公众号柒牌SEVENBRAND自行充值:
+        self.aud.montage_play('U:/static/audio.bin',1,1,str_data)
+        #开定时器不让系统休眠
+        self.play_imei_timer_count=1
+        self.play_imei_timer.stop()
+        self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)
+
+    def play_imei_timer_cb(self,event=None,msg=None):
+        if self.play_imei_timer_count == 1:
+            if EventMesh.publish("get_audio_play_state") == 0:
+                print("音频文件播放完成")
+                self.play_imei_timer_count=0
+                self.play_imei_timer.stop()
+                return
+        EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+
+    #鉴权出错,播放imei号
+    def paly_device_res_error(self,event,msg):
+        self.wakeup_BT()
+        EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+        str_data = 'mp3files={}.mp3+'.format(audio_buf.index("jianquancuowu"))#AI功能异常,请联系客户查询,设备号为:
+        imei  = modem.getDevImei()
+        imei = imei.upper()
+        str_data += '{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3'.format(audio_buf.index("R_READ_TIME_{}".format(imei[0]))\
+                ,audio_buf.index("R_READ_TIME_{}".format(imei[1])),audio_buf.index("R_READ_TIME_{}".format(imei[2])),audio_buf.index("R_READ_TIME_{}".format(imei[3])),\
+                audio_buf.index("R_READ_TIME_{}".format(imei[4])),audio_buf.index("R_READ_TIME_{}".format(imei[5])),audio_buf.index("R_READ_TIME_{}".format(imei[6])),audio_buf.index("R_READ_TIME_{}".format(imei[7])),\
+                audio_buf.index("R_READ_TIME_{}".format(imei[8])),audio_buf.index("R_READ_TIME_{}".format(imei[9])),audio_buf.index("R_READ_TIME_{}".format(imei[10])),audio_buf.index("R_READ_TIME_{}".format(imei[11])),\
+                audio_buf.index("R_READ_TIME_{}".format(imei[12])),audio_buf.index("R_READ_TIME_{}".format(imei[13])),audio_buf.index("R_READ_TIME_{}".format(imei[14])))
+        #print("str_data:{}".format(str_data))
+        self.aud.montage_play('U:/static/audio.bin',1,1,str_data)
+        self.play_imei_timer_count=1
+        self.play_imei_timer.stop()
+        self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)
+
index ef5443b..728d240 100644 (file)
-# -*- coding: utf-8 -*-\r
-"""VC 公共配置与轻量工具(无业务类)。"""\r
-from common import Abstract, LogAdapter\r
-try:\r
-    from qp_log import qp_print as print\r
-except Exception:\r
-    pass\r
-import EventMesh\r
-import uwebsocket\r
-import usys\r
-import gc\r
-import urandom\r
-import uhashlib\r
-import ubinascii\r
-import ujson\r
-import osTimer\r
-import _thread\r
-from queue import Queue\r
-import utime\r
-import audio\r
-import uos\r
-from machine import Pin\r
-from machine import ExtInt\r
-from machine import WDT\r
-import modem\r
-import sim\r
-import net\r
-import base64\r
-import ucryptolib\r
-import urandom as random\r
-import request\r
-import ql_fs\r
-from misc import Power,USB,PowerKey\r
-import ntptime\r
-import machine\r
-import dataCall\r
-import constants\r
-import versions\r
-import version_util\r
-from machine import UART\r
-try:\r
-    import vc_config\r
-except Exception as e:\r
-    usys.print_exception(e)\r
-try:\r
-    import vc_text_intent\r
-except Exception:\r
-    vc_text_intent = None\r
-try:\r
-    import VAD\r
-except Exception as e:\r
-    usys.print_exception(e)\r
-try:   \r
-    import usr.ai_alarm as _ai_alarm\r
-except Exception as e:\r
-    usys.print_exception(e)\r
-    import ai_alarm as _ai_alarm\r
-PRESS_AUDIO_MODE = 0 #是按键音 0 是使用音频文件\r
-# dev_type = "G10"\r
-#dev_type = "7PIN_BK"#小瑞\r
-#dev_type = "7PIN_BK_coze"#k扣子小瑞\r
-#dev_type = "7PIN_BK_EU"#英文小瑞\r
-#dev_type = "7PIN_coze"#柒牌扣子\r
-dev_type = "7PIN"#柒牌(默认值,实际优先从 usr/default_config.py 覆盖)\r
-# dev_type = "7PIN_1_5"#柒牌1.5\r
-#dev_type = 'momo'#食梦貘\r
-#dev_type = 'sjgq'#四季甘泉\r
-#dev_type = 'szbst'#深圳百事通\r
-#dev_type = 'jswm'#金三完美\r
-#dev_type = 'jswm_BOX'#金三完美柒牌盒子\r
-#dev_type = 'csh'#慈善会\r
-\r
-# 统一从配置加载 DEV_TYPE(usr 覆盖优先)\r
-try:\r
-    if "vc_config" in globals() and vc_config:\r
-        dev_type = vc_config.get_dev_type(dev_type)\r
-except Exception:\r
-    pass\r
-\r
-# 1=打印 VC 周期性保活专用行「VC: ping ping」;默认 0(WS send 明细里已跳过 ping,无需再开此项)\r
-VC_HEARTBEAT_LOG = 0\r
-# 1=打印音频帧级日志:Record_len / send_data len / 下发音频长度;默认 0(只看 uplink START/END)\r
-# VC_AUDIO_FRAME_LOG = 0\r
-# 1=启用我们新增的协议层“严格门控/缓冲/兜底”行为;0=临时排除这些改动(用于定位协议影响)\r
-# 受控项(与对端 V2.5+ 能力对齐;关=回退旧端行为):\r
-# - 上行:收到 started 前缓冲/丢弃 PCM;PTT 模式下 audio.create 前禁发 PCM\r
-# - 兜底:llm.completed 后若久无 tts.started,端侧可重启会话(不做 force_tts)\r
-# 注意:audio.played 不受此开关控制——金三 V2.5 服务端依赖端侧播完后上报才能进入下一轮 ASR;\r
-#       若误关会导致问候结束后首轮用户说话无 vad/asr。\r
-VC_PROTO_GUARDS = 0\r
-# 1=打印 VC WebSocket 每条收发的完整文本(流量大、易刷屏);0 仅一行 event 摘要(llm.delta 省略)\r
-VC_WS_IO_LOG = 0\r
-# 长对话 20s 无 TTS idle:仅 tts.* / started / TTS 音频字节重置 close_timerout;asr/vad/llm 不重置。\r
-# 帧级 WebSocket PING/PONG 在 uwebsocket.recv() 内已处理,不会进入 AI_client_VC_recv。\r
-# 金三 V2.5 文档示例:WebSocket 文本帧 "ping" 保活;对端可能回 pong / heartbeat 类事件。\r
-_VC_KEEPALIVE_RX_EVENTS = frozenset(("pong", "ping", "heartbeat", "keepalive"))\r
-\r
-\r
-def _vc_out_json_event_meta(s):\r
-    """解析 WS 出站 JSON:event 可能在 header 或顶层(Start/audio.create vs synthesis)。"""\r
-    try:\r
-        jd = ujson.loads(s)\r
-        h = jd.get("header")\r
-        if isinstance(h, dict):\r
-            ev = h.get("event") or ""\r
-            if ev:\r
-                return ev, h.get("trace_id") or ""\r
-        ev = jd.get("event") or ""\r
-        return ev, ""\r
-    except Exception:\r
-        return "", ""\r
-\r
+# -*- coding: utf-8 -*-
+"""VC 公共配置与轻量工具(无业务类)。"""
+from common import Abstract, LogAdapter
+try:
+    from qp_log import qp_print as print
+except Exception:
+    pass
+import EventMesh
+import uwebsocket
+import usys
+import gc
+import urandom
+import uhashlib
+import ubinascii
+import ujson
+import osTimer
+import _thread
+from queue import Queue
+import utime
+import audio
+import uos
+from machine import Pin
+from machine import ExtInt
+from machine import WDT
+import modem
+import sim
+import net
+import base64
+import ucryptolib
+import urandom as random
+import request
+import ql_fs
+from misc import Power,USB,PowerKey
+import ntptime
+import machine
+import dataCall
+import constants
+import versions
+import version_util
+from machine import UART
+try:
+    import vc_config
+except Exception as e:
+    usys.print_exception(e)
+try:
+    import vc_text_intent
+except Exception:
+    vc_text_intent = None
+try:
+    import VAD
+except Exception as e:
+    usys.print_exception(e)
+try:   
+    import usr.ai_alarm as _ai_alarm
+except Exception as e:
+    usys.print_exception(e)
+    import ai_alarm as _ai_alarm
+PRESS_AUDIO_MODE = 0 #是按键音 0 是使用音频文件
+# dev_type = "G10"
+#dev_type = "7PIN_BK"#小瑞
+#dev_type = "7PIN_BK_coze"#k扣子小瑞
+#dev_type = "7PIN_BK_EU"#英文小瑞
+#dev_type = "7PIN_coze"#柒牌扣子
+dev_type = "7PIN"#柒牌(默认值,实际优先从 usr/default_config.py 覆盖)
+# dev_type = "7PIN_1_5"#柒牌1.5
+#dev_type = 'momo'#食梦貘
+#dev_type = 'sjgq'#四季甘泉
+#dev_type = 'szbst'#深圳百事通
+#dev_type = 'jswm'#金三完美
+#dev_type = 'jswm_BOX'#金三完美柒牌盒子
+#dev_type = 'csh'#慈善会
+
+# 统一从配置加载 DEV_TYPE(usr 覆盖优先)
+try:
+    if "vc_config" in globals() and vc_config:
+        dev_type = vc_config.get_dev_type(dev_type)
+except Exception:
+    pass
+
+# 1=打印 VC 周期性保活专用行「VC: ping ping」;默认 0(WS send 明细里已跳过 ping,无需再开此项)
+VC_HEARTBEAT_LOG = 0
+# 1=打印音频帧级日志:Record_len / send_data len / 下发音频长度;默认 0(只看 uplink START/END)
+# VC_AUDIO_FRAME_LOG = 0
+# 1=启用我们新增的协议层“严格门控/缓冲/兜底”行为;0=临时排除这些改动(用于定位协议影响)
+# 受控项(与对端 V2.5+ 能力对齐;关=回退旧端行为):
+# - 上行:收到 started 前缓冲/丢弃 PCM;PTT 模式下 audio.create 前禁发 PCM
+# - 兜底:llm.completed 后若久无 tts.started,端侧可重启会话(不做 force_tts)
+# 注意:audio.played 不受此开关控制——金三 V2.5 服务端依赖端侧播完后上报才能进入下一轮 ASR;
+#       若误关会导致问候结束后首轮用户说话无 vad/asr。
+VC_PROTO_GUARDS = 0
+# 1=打印 VC WebSocket 每条收发的完整文本(流量大、易刷屏);0 仅一行 event 摘要(llm.delta 省略)
+VC_WS_IO_LOG = 0
+# 长对话 20s 无 TTS idle:仅 tts.* / started / TTS 音频字节重置 close_timerout;asr/vad/llm 不重置。
+# 帧级 WebSocket PING/PONG 在 uwebsocket.recv() 内已处理,不会进入 AI_client_VC_recv。
+# 金三 V2.5 文档示例:WebSocket 文本帧 "ping" 保活;对端可能回 pong / heartbeat 类事件。
+_VC_KEEPALIVE_RX_EVENTS = frozenset(("pong", "ping", "heartbeat", "keepalive"))
+
+
+def _vc_out_json_event_meta(s):
+    """解析 WS 出站 JSON:event 可能在 header 或顶层(Start/audio.create vs synthesis)。"""
+    try:
+        jd = ujson.loads(s)
+        h = jd.get("header")
+        if isinstance(h, dict):
+            ev = h.get("event") or ""
+            if ev:
+                return ev, h.get("trace_id") or ""
+        ev = jd.get("event") or ""
+        return ev, ""
+    except Exception:
+        return "", ""
+
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_interrupt.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_interrupt.py
new file mode 100644 (file)
index 0000000..88db152
--- /dev/null
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-\r
+"""长对话/PTT 打断上下文:绑定 turn_gen,收敛 is_ruppt_tts 等 legacy 标志。"""\r
+try:\r
+    import utime\r
+except Exception:\r
+    utime = None\r
+\r
+\r
+class InterruptContext(object):\r
+    KIND_NONE = ""\r
+    KIND_LONG_SHORT = "long_short"\r
+    KIND_PTT_SHORT = "ptt_short"\r
+    KIND_PTT_LONG = "ptt_long"\r
+\r
+    def __init__(self):\r
+        self.active = 0\r
+        self.kind = self.KIND_NONE\r
+        self.gen = 0\r
+        self.pending_uplink = 0\r
+        self.drop_tts = 0\r
+        self.ruppt_tts = 0\r
+        self.settled_ms = 0\r
+        self.cancel_deadline_ms = 0\r
+\r
+    def clear(self, vc=None):\r
+        self.active = 0\r
+        self.kind = self.KIND_NONE\r
+        self.gen = 0\r
+        self.pending_uplink = 0\r
+        self.drop_tts = 0\r
+        self.ruppt_tts = 0\r
+        self.settled_ms = 0\r
+        self.cancel_deadline_ms = 0\r
+        if vc is not None:\r
+            self._sync_legacy(vc)\r
+\r
+    def begin(self, vc, kind, pending_uplink=0, cancel_wait_ms=800):\r
+        try:\r
+            gen = int(getattr(vc, "_vc_turn_gen", 0))\r
+        except Exception:\r
+            gen = 0\r
+        self.active = 1\r
+        self.kind = kind or self.KIND_NONE\r
+        self.gen = gen\r
+        self.pending_uplink = 1 if pending_uplink else 0\r
+        self.drop_tts = 1\r
+        self.ruppt_tts = 1\r
+        self.settled_ms = 0\r
+        if utime and cancel_wait_ms:\r
+            try:\r
+                self.cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), int(cancel_wait_ms))\r
+            except Exception:\r
+                self.cancel_deadline_ms = 0\r
+        else:\r
+            self.cancel_deadline_ms = 0\r
+        self._sync_legacy(vc)\r
+        return gen\r
+\r
+    def settle(self, vc):\r
+        """打断开麦完成:清 is_ruppt/drop 以允许新 TTS;保留 settled_ms 供迟到 canceled 判 stale。"""\r
+        if utime:\r
+            try:\r
+                self.settled_ms = utime.ticks_ms()\r
+            except Exception:\r
+                self.settled_ms = 0\r
+        self.pending_uplink = 0\r
+        self.active = 0\r
+        self.drop_tts = 0\r
+        self.ruppt_tts = 0\r
+        self._sync_legacy(vc)\r
+\r
+    def end_drop_window(self, vc):\r
+        """新一轮 TTS 开始:结束丢弃窗口。"""\r
+        self.drop_tts = 0\r
+        self.ruppt_tts = 0\r
+        self.settled_ms = 0\r
+        self.pending_uplink = 0\r
+        self.active = 0\r
+        self._sync_legacy(vc)\r
+\r
+    def is_stale_gen(self, vc):\r
+        try:\r
+            return int(getattr(vc, "_vc_turn_gen", 0)) != int(self.gen)\r
+        except Exception:\r
+            return False\r
+\r
+    def _sync_legacy(self, vc):\r
+        """与 vc_session_io / media 中既有 getattr 兼容。"""\r
+        try:\r
+            vc._vc_pending_interrupt_uplink = 1 if self.pending_uplink else 0\r
+            vc.is_ruppt_tts = 1 if self.ruppt_tts else 0\r
+            vc._drop_tts_audio = 1 if self.drop_tts else 0\r
+            vc._long_dialogue_interrupt_settled_ms = int(self.settled_ms or 0)\r
+            vc._vc_cancel_deadline_ms = int(self.cancel_deadline_ms or 0)\r
+        except Exception:\r
+            pass\r
+\r
+    def load_legacy(self, vc):\r
+        try:\r
+            self.pending_uplink = 1 if getattr(vc, "_vc_pending_interrupt_uplink", 0) else 0\r
+            self.ruppt_tts = 1 if getattr(vc, "is_ruppt_tts", 0) else 0\r
+            self.drop_tts = 1 if getattr(vc, "_drop_tts_audio", 0) else 0\r
+            self.settled_ms = int(getattr(vc, "_long_dialogue_interrupt_settled_ms", 0) or 0)\r
+            self.cancel_deadline_ms = int(getattr(vc, "_vc_cancel_deadline_ms", 0) or 0)\r
+            self.active = 1 if (self.pending_uplink or self.ruppt_tts or self.drop_tts) else 0\r
+        except Exception:\r
+            pass\r
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_key_router.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_key_router.py
new file mode 100644 (file)
index 0000000..d9436cb
--- /dev/null
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-\r
+"""按键表驱动:mode × UX × 事件 → 既有 vc_session 方法。"""\r
+try:\r
+    import EventMesh\r
+    import constants\r
+    import utime\r
+except Exception:\r
+    EventMesh = None\r
+    constants = None\r
+    utime = None\r
+\r
+from vc_ux import derive_ux_state, UX_IDLE, UX_RECORDING, UX_THINKING, UX_TTS, ux_name\r
+\r
+try:\r
+    import vc_config\r
+except Exception:\r
+    vc_config = None\r
+\r
+\r
+class KeyRouter(object):\r
+    @staticmethod\r
+    def dispatch_press(vc):\r
+        try:\r
+            if int(getattr(vc, "VC_mode", 0)) == 1:\r
+                return KeyRouter._ptt_press(vc)\r
+            return KeyRouter._long_ai_press(vc)\r
+        except Exception as e:\r
+            try:\r
+                import usys\r
+                usys.print_exception(e)\r
+            except Exception:\r
+                pass\r
+            return False\r
+\r
+    @staticmethod\r
+    def handle_ptt_release(vc):\r
+        if int(getattr(vc, "VC_mode", 0)) != 1:\r
+            return False\r
+        if getattr(vc, "AI_key_long_press_flag", 0) == 1:\r
+            return False\r
+        ux = derive_ux_state(vc)\r
+        try:\r
+            print("[KEY][router] ptt_release ux={}".format(ux_name(ux)))\r
+        except Exception:\r
+            pass\r
+        if ux == UX_TTS:\r
+            try:\r
+                if vc._ptt_is_active_tts_playback():\r
+                    vc._ptt_stop_tts_on_short_press()\r
+                    return True\r
+            except Exception:\r
+                pass\r
+        if ux == UX_THINKING:\r
+            try:\r
+                vc._ptt_stop_dialog_to_idle()\r
+                return True\r
+            except Exception:\r
+                pass\r
+        if ux == UX_IDLE:\r
+            try:\r
+                vc.key_prompt()\r
+                return True\r
+            except Exception:\r
+                pass\r
+        return False\r
+\r
+    @staticmethod\r
+    def handle_power_short(vc):\r
+        """Power 短按:cancel → Close → 清 sid(设计稿幻灯片 6)。"""\r
+        ux = derive_ux_state(vc)\r
+        try:\r
+            print("[KEY][router] power_short ux={}".format(ux_name(ux)))\r
+        except Exception:\r
+            pass\r
+        try:\r
+            vc._vc_send_protocol_cancel("power_short/ux={}".format(ux_name(ux)))\r
+        except Exception:\r
+            pass\r
+        try:\r
+            vc._vc_close_reason = "power_short"\r
+        except Exception:\r
+            pass\r
+        try:\r
+            if EventMesh:\r
+                EventMesh.publish("publish_AI_btnInfo", {"func": vc.Close})\r
+        except Exception:\r
+            pass\r
+        try:\r
+            if EventMesh and constants:\r
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
+        except Exception:\r
+            pass\r
+        return True\r
+\r
+    @staticmethod\r
+    def _long_ai_press(vc):\r
+        ux = derive_ux_state(vc)\r
+        try:\r
+            print("[KEY][router] ai_press ux={} {}".format(\r
+                ux_name(ux), getattr(vc, "_vc_fsm_log_snip", lambda: "")()))\r
+        except Exception:\r
+            pass\r
+        if ux == UX_RECORDING:\r
+            try:\r
+                return vc._vc_restart_record_same_dialog()\r
+            except Exception:\r
+                pass\r
+        if ux in (UX_THINKING, UX_TTS):\r
+            try:\r
+                vc._long_dialogue_interrupt_on_short_press()\r
+                return True\r
+            except Exception:\r
+                pass\r
+        if vc._ai_key_press_long_dialogue_session():\r
+            return True\r
+        if vc._ai_key_press_long_dialogue_wake():\r
+            if EventMesh and constants:\r
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
+            return True\r
+        return KeyRouter._long_fallback_start(vc)\r
+\r
+    @staticmethod\r
+    def _long_fallback_start(vc):\r
+        try:\r
+            vc.Close_flag = 0\r
+            vc._skip_started_uplink_once = 0\r
+            vc._vc_long_dialogue_key_arm_session()\r
+            vc._long_dialogue_publish_listening("key.press/fallback")\r
+            vc.get_config_timer_count = 0\r
+            vc.key_state = 1\r
+            vc.AI_TTS_str = ""\r
+            vc.AI_ASR_str = ""\r
+            vc.AI_ASR_str_tmp = ""\r
+            EventMesh.publish("publish_AI_btnInfo", {"func": vc.AI_VC_start})\r
+            vc.AI_key_long_press_flag = 0\r
+            return True\r
+        except Exception:\r
+            return False\r
+\r
+    @staticmethod\r
+    def _ptt_press(vc):\r
+        return True\r
+\r
+    @staticmethod\r
+    def ptt_release_after_long_press(vc):\r
+        """PTT 长按松手:录音过短 → notify TTS + idle。"""\r
+        if getattr(vc, "VC_record_state_flag", 0) == 0:\r
+            return False\r
+        min_ms = 400\r
+        tts = "录音时间太短,请按住圆形按钮说话"\r
+        try:\r
+            if vc_config:\r
+                min_ms = int(vc_config.get_int("PTT_MIN_RECORD_MS", min_ms))\r
+                tts = vc_config.get_attr("PTT_RECORD_TOO_SHORT_TTS", tts) or tts\r
+        except Exception:\r
+            pass\r
+        t_open = int(getattr(vc, "_ptt_record_open_ms", 0) or 0)\r
+        if not t_open or utime is None:\r
+            return False\r
+        try:\r
+            if utime.ticks_diff(utime.ticks_ms(), t_open) >= min_ms:\r
+                return False\r
+        except Exception:\r
+            return False\r
+        try:\r
+            vc._ptt_abort_short_record(tts)\r
+        except Exception:\r
+            pass\r
+        return True\r
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_media_arb.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_media_arb.py
new file mode 100644 (file)
index 0000000..c834eac
--- /dev/null
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-\r
+"""媒体车道仲裁:llm_pcm / notify_tts / local(对齐设计稿幻灯片 4)。"""\r
+try:\r
+    import EventMesh\r
+except Exception:\r
+    EventMesh = None\r
+\r
+LANE_LLM_PCM = 1\r
+LANE_NOTIFY = 2\r
+LANE_LOCAL = 3\r
+\r
+\r
+class MediaArbiter(object):\r
+    @staticmethod\r
+    def llm_active(vc):\r
+        try:\r
+            return vc._vc_tts_playback_active()\r
+        except Exception:\r
+            return False\r
+\r
+    @staticmethod\r
+    def before_local(vc):\r
+        """local 播:仅打断 llm_pcm;不停录、不断 notify WS。"""\r
+        try:\r
+            if MediaArbiter.llm_active(vc):\r
+                try:\r
+                    vc._ptt_hard_mute_local_playback()\r
+                except Exception:\r
+                    pass\r
+                try:\r
+                    vc.VC_Play_End()\r
+                except Exception:\r
+                    pass\r
+        except Exception:\r
+            pass\r
+\r
+    @staticmethod\r
+    def before_notify(vc):\r
+        """notify TTS:关 llm_pcm + local(设计稿;会话接管仍由 play_tts_cb 建链)。"""\r
+        try:\r
+            MediaArbiter.before_local(vc)\r
+            if getattr(vc, "_vc_local_play_busy", None) and vc._vc_local_play_busy():\r
+                try:\r
+                    vc.aud.stopAll()\r
+                except Exception:\r
+                    pass\r
+            if hasattr(vc, "_vc_stop_downlink_for_interrupt"):\r
+                vc._vc_stop_downlink_for_interrupt("notify")\r
+        except Exception:\r
+            pass\r
+\r
+    @staticmethod\r
+    def before_llm_pcm(vc):\r
+        """llm 下行:停 local 短音;notify 走 defer 队列。"""\r
+        try:\r
+            if getattr(vc, "_vc_local_play_busy", None) and vc._vc_local_play_busy():\r
+                try:\r
+                    vc.aud.stopAll()\r
+                except Exception:\r
+                    pass\r
+        except Exception:\r
+            pass\r
+\r
+    @staticmethod\r
+    def montage_allowed(vc):\r
+        """llm 播时缓存/跳过 montage(vc_buttons 已有类似逻辑,集中判断)。"""\r
+        if MediaArbiter.llm_active(vc):\r
+            return False\r
+        try:\r
+            if EventMesh and EventMesh.publish("get_vc_tts_playback_active") == 1:\r
+                return False\r
+        except Exception:\r
+            pass\r
+        return True\r
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_mode_strategy.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_mode_strategy.py
new file mode 100644 (file)
index 0000000..79f60e1
--- /dev/null
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-\r
+"""单对话 / 长对话按键策略:入口仅转发 KeyRouter(设计稿表驱动)。"""\r
+try:\r
+    import EventMesh\r
+except Exception:\r
+    EventMesh = None\r
+try:\r
+    from vc_key_router import KeyRouter\r
+except Exception:\r
+    KeyRouter = None\r
+\r
+\r
+class VCModeStrategy(object):\r
+    def on_key_press(self, vc):\r
+        return False\r
+\r
+    def on_key_release(self, vc):\r
+        return False\r
+\r
+    def on_key_long_press(self, vc):\r
+        return False\r
+\r
+\r
+class LongDialogueStrategy(VCModeStrategy):\r
+    def on_key_press(self, vc):\r
+        if KeyRouter is not None:\r
+            return KeyRouter.dispatch_press(vc)\r
+        return False\r
+\r
+    def on_key_release(self, vc):\r
+        return False\r
+\r
+    def on_key_long_press(self, vc):\r
+        if vc._vc_ms_keys_blocked("AI_long_press"):\r
+            return True\r
+        if getattr(vc, "trans_flag", 0) == 1:\r
+            vc.Close()\r
+            EventMesh.publish("start_voice_record", 4)\r
+        vc.AI_key_long_press_flag = 1\r
+        if hasattr(vc, "_vc_key_router_press_cb"):\r
+            vc._vc_enqueue_btn(vc._vc_key_router_press_cb, priority=1)\r
+        else:\r
+            vc._vc_enqueue_btn(vc._ai_key_press_long_dialogue_dispatch, priority=1)\r
+        return True\r
+\r
+\r
+class PTTStrategy(VCModeStrategy):\r
+    def on_key_press(self, vc):\r
+        if KeyRouter is not None:\r
+            return KeyRouter.dispatch_press(vc)\r
+        return True\r
+\r
+    def on_key_release(self, vc):\r
+        vc._ai_key_release_handle_ptt()\r
+        return True\r
+\r
+    def on_key_long_press(self, vc):\r
+        if vc._vc_ms_keys_blocked("AI_long_press"):\r
+            return True\r
+        if getattr(vc, "trans_flag", 0) == 1:\r
+            vc.Close()\r
+            EventMesh.publish("start_voice_record", 4)\r
+        vc.AI_key_long_press_flag = 1\r
+        try:\r
+            vc._ptt_idle_latch = 0\r
+            vc._ptt_short_press_stop = 0\r
+        except Exception:\r
+            pass\r
+        vc._vc_enqueue_btn(vc.ptt_begin, priority=1)\r
+        return True\r
+\r
+\r
+def strategy_for_mode(vc_mode):\r
+    try:\r
+        if int(vc_mode) == 1:\r
+            return PTTStrategy()\r
+    except Exception:\r
+        pass\r
+    return LongDialogueStrategy()\r
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_notify.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_notify.py
new file mode 100644 (file)
index 0000000..3516c10
--- /dev/null
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-\r
+"""通知通道:server.started 媒体排队、play_tts 与对话会话分流。"""\r
+try:\r
+    from vc_fsm import VCTurnState\r
+except Exception:\r
+    VCTurnState = None\r
+try:\r
+    import vc_config\r
+except Exception:\r
+    vc_config = None\r
+\r
+# play_tts 通道\r
+CHANNEL_TAKEOVER = "takeover"   # 断连重连 WS synthesis(IoT 查找设备/模式切换/无对话)\r
+CHANNEL_DEFER = "defer"         # 对话进行中排队,空闲后播放(is_close_VC=0 的 IoT 查询等)\r
+\r
+# TTS 业务种类(_pending_tts_kind / play_tts 第三参数)\r
+TTS_KIND_FIND_DEVICE = "find_device"\r
+TTS_KIND_MODE_SWITCH_LONG = "mode_switch_long"\r
+TTS_KIND_MODE_SWITCH_PTT = "mode_switch_ptt"\r
+TTS_KIND_BOOT_PROMPT = "boot_prompt"\r
+TTS_KIND_PROMPT = "prompt"\r
+\r
+# 须立即占用会话、不可 defer\r
+_TAKEOVER_KINDS = (\r
+    TTS_KIND_FIND_DEVICE,\r
+    TTS_KIND_MODE_SWITCH_LONG,\r
+    TTS_KIND_MODE_SWITCH_PTT,\r
+    TTS_KIND_BOOT_PROMPT,\r
+    TTS_KIND_PROMPT,\r
+)\r
+\r
+# server.started plan op\r
+OP_LONG_UPLINK = "long_uplink"\r
+OP_ALARM_VOICE = "alarm_voice"\r
+OP_VOICE_URL = "voice_url"\r
+OP_SYNTHESIS = "synthesis"\r
+OP_SYNTHESIS_HIS_TIMER = "synthesis_his_timer"\r
+OP_SYNTHESIS_WAIT_TIMER = "synthesis_wait_timer"\r
+OP_SYNTHESIS_WAIT_SINGLE = "synthesis_wait_single"\r
+OP_SYNTHESIS_VOICE_TIMER = "synthesis_voice_timer"\r
+OP_SYNTHESIS_MUSIC_TIMER = "synthesis_music_timer"\r
+OP_IDLE_UPLINK = "idle_uplink"\r
+OP_PTT_ROUND = "ptt_round"\r
+OP_UPLOAD_TONE = "upload_tone"\r
+\r
+\r
+def dialog_session_active(vc):\r
+    """对话占用中:通知类 play_tts 应 defer。"""\r
+    try:\r
+        if getattr(vc, "Close_flag", 0) == 1:\r
+            return False\r
+        if getattr(vc, "VC_satrt_flag", 0) != 1:\r
+            return False\r
+        if getattr(vc, "AI_client_VC_isconnect", 0) != 1:\r
+            return False\r
+        if getattr(vc, "VC_Play_flag", 0) == 1:\r
+            return True\r
+        if getattr(vc, "VC_record_state_flag", 0) != 0:\r
+            return True\r
+        # 7PIN_1_5 无闹钟业务;alarm_play_flag 仅 legacy(如 play_single_voice)\r
+        if getattr(vc, "music_play_flag", 0) == 1:\r
+            return True\r
+        if VCTurnState is not None and getattr(vc, "fsm", None) is not None:\r
+            t = getattr(vc.fsm, "turn", VCTurnState.Idle)\r
+            if t not in (VCTurnState.Idle, VCTurnState.Listening):\r
+                return True\r
+    except Exception:\r
+        pass\r
+    return False\r
+\r
+\r
+class NotifyScheduler(object):\r
+    @staticmethod\r
+    def classify_play_tts(vc, is_close_vc):\r
+        try:\r
+            if is_close_vc:\r
+                return CHANNEL_TAKEOVER\r
+        except Exception:\r
+            pass\r
+        try:\r
+            k = getattr(vc, "_pending_tts_kind", "") or getattr(vc, "_last_tts_kind", "") or ""\r
+            if k in _TAKEOVER_KINDS:\r
+                return CHANNEL_TAKEOVER\r
+        except Exception:\r
+            pass\r
+        if dialog_session_active(vc):\r
+            return CHANNEL_DEFER\r
+        return CHANNEL_TAKEOVER\r
+\r
+    @staticmethod\r
+    def plan_on_started(vc, skip_long_uplink=0):\r
+        """替代 vc_session_io started 内 if-elif 媒体分支。"""\r
+        plan = []\r
+        try:\r
+            import EventMesh\r
+        except Exception:\r
+            EventMesh = None\r
+\r
+        # 长对话:started 补开麦(原 597-623,与媒体队列正交)\r
+        try:\r
+            if (\r
+                not skip_long_uplink\r
+                and getattr(vc, "VC_mode", 0) == 0\r
+                and getattr(vc, "Close_flag", 0) == 0\r
+            ):\r
+                plan.append({"op": OP_LONG_UPLINK})\r
+        except Exception:\r
+            pass\r
+\r
+        tts = getattr(vc, "AI_TTS_str", "") or ""\r
+        url = getattr(vc, "AI_new_voice_url", "") or ""\r
+        alarm = getattr(vc, "alarm_play_flag", 0) == 1\r
+        music = getattr(vc, "music_play_flag", 0) == 1\r
+\r
+        # 闹钟铃声(其它机型);7PIN_1_5 无闹钟,alarm_play_flag 通常为 0\r
+        if url and alarm:\r
+            plan.append({"op": OP_ALARM_VOICE})\r
+        elif not tts and url and not music:\r
+            plan.append({"op": OP_VOICE_URL})\r
+        elif tts and not url:\r
+            plan.append({"op": OP_SYNTHESIS})\r
+        elif tts and url and not music:\r
+            try:\r
+                his = EventMesh.publish("get_history_voice_flag") == 1 if EventMesh else False\r
+            except Exception:\r
+                his = False\r
+            if his:\r
+                try:\r
+                    wait = EventMesh.publish("get_wait_voice_flag") == 0 if EventMesh else True\r
+                except Exception:\r
+                    wait = True\r
+                if wait:\r
+                    plan.append({"op": OP_SYNTHESIS_HIS_TIMER})\r
+                else:\r
+                    try:\r
+                        notice = getattr(vc, "pwk_notice_flag", 0) == 0\r
+                    except Exception:\r
+                        notice = True\r
+                    if notice:\r
+                        plan.append({"op": OP_SYNTHESIS_WAIT_TIMER})\r
+                    else:\r
+                        plan.append({"op": OP_SYNTHESIS_WAIT_SINGLE})\r
+            else:\r
+                plan.append({"op": OP_SYNTHESIS_VOICE_TIMER})\r
+        elif tts and url and music:\r
+            plan.append({"op": OP_SYNTHESIS_MUSIC_TIMER})\r
+        else:\r
+            try:\r
+                if "vc_config" in globals() and vc_config and vc_config.get_bool(\r
+                    "UPLOAD_START_TONE_ENABLED", False\r
+                ):\r
+                    plan.append({"op": OP_UPLOAD_TONE})\r
+            except Exception:\r
+                pass\r
+            try:\r
+                if getattr(vc, "VC_mode", 0) == 0:\r
+                    plan.append({"op": OP_IDLE_UPLINK})\r
+                else:\r
+                    plan.append({"op": OP_PTT_ROUND})\r
+            except Exception:\r
+                plan.append({"op": OP_IDLE_UPLINK})\r
+        return plan\r
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_overlay.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_overlay.py
new file mode 100644 (file)
index 0000000..639d6f6
--- /dev/null
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-\r
+"""VC 叠加态:模式切换等短时覆盖 FSM 的按键屏蔽(兼容 _vc_ms_phase)。"""\r
+\r
+OVERLAY_IDLE = 0\r
+OVERLAY_MODE_SWITCH_PREP = 1\r
+OVERLAY_MODE_SWITCH_PLAYING = 2\r
+OVERLAY_MODE_SWITCH_CLOSING = 3\r
+OVERLAY_MODE_SWITCH_DONE = 3  # 与 CLOSING 同值,兼容旧 phase=3\r
+\r
+\r
+def overlay_from_vc(vc):\r
+    try:\r
+        return int(getattr(vc, "_vc_overlay", getattr(vc, "_vc_ms_phase", 0)) or 0)\r
+    except Exception:\r
+        return OVERLAY_IDLE\r
+\r
+\r
+def overlay_set(vc, state):\r
+    try:\r
+        vc._vc_overlay = int(state or 0)\r
+        vc._vc_ms_phase = vc._vc_overlay\r
+    except Exception:\r
+        pass\r
+\r
+\r
+def overlay_keys_blocked(vc, tag=""):\r
+    """模式切换 PREP/PLAYING/CLOSE 期间屏蔽 AI/电源短按(单入口)。"""\r
+    try:\r
+        phase = overlay_from_vc(vc)\r
+        if phase in (\r
+            OVERLAY_MODE_SWITCH_PREP,\r
+            OVERLAY_MODE_SWITCH_PLAYING,\r
+            OVERLAY_MODE_SWITCH_CLOSING,\r
+        ):\r
+            try:\r
+                print("[VC][overlay] key blocked phase={} tag={}".format(phase, tag))\r
+            except Exception:\r
+                pass\r
+            return True\r
+        if getattr(vc, "_vc_ms_close_pending", 0) == 1:\r
+            try:\r
+                print("[VC][overlay] key blocked close_pending tag={}".format(tag))\r
+            except Exception:\r
+                pass\r
+            return True\r
+        until = int(getattr(vc, "_vc_ms_key_block_until_ms", 0) or 0)\r
+        if until:\r
+            try:\r
+                import utime\r
+                if utime.ticks_diff(until, utime.ticks_ms()) > 0:\r
+                    try:\r
+                        print("[VC][overlay] key blocked cooldown tag={}".format(tag))\r
+                    except Exception:\r
+                        pass\r
+                    return True\r
+            except Exception:\r
+                pass\r
+    except Exception:\r
+        pass\r
+    return False\r
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_present.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_present.py
new file mode 100644 (file)
index 0000000..7e26832
--- /dev/null
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-\r
+"""LED 呈现:由 UX 四态 + 覆盖态推导 ai_state(唯一 set_led_state 入口)。"""\r
+try:\r
+    from vc_fsm import VCTurnState\r
+except Exception:\r
+    VCTurnState = None\r
+try:\r
+    from vc_ux import derive_ux_state, UX_RECORDING, UX_THINKING, UX_TTS\r
+except Exception:\r
+    derive_ux_state = None\r
+\r
+\r
+def build_present_state(vc):\r
+    """只读聚合,不写 vc。"""\r
+    st = {\r
+        "mode": 0,\r
+        "turn": -1,\r
+        "force_idle": 0,\r
+        "hold_listening": 0,\r
+        "prefer_off": 0,\r
+        "stream_active": 0,\r
+        "ptt_await_server": 0,\r
+        "override_ai": None,\r
+        "override_prefer_off": 0,\r
+    }\r
+    try:\r
+        st["mode"] = int(getattr(vc, "VC_mode", 0))\r
+    except Exception:\r
+        pass\r
+    try:\r
+        st["turn"] = int(getattr(getattr(vc, "fsm", None), "turn", -1))\r
+    except Exception:\r
+        pass\r
+    try:\r
+        ov = getattr(vc, "_led_override", None)\r
+        if ov and isinstance(ov, (tuple, list)) and len(ov) >= 1:\r
+            st["override_ai"] = ov[0]\r
+            st["override_prefer_off"] = 1 if (len(ov) > 1 and ov[1]) else 0\r
+    except Exception:\r
+        pass\r
+    try:\r
+        if hasattr(vc, "_ptt_should_force_idle_led") and vc._ptt_should_force_idle_led():\r
+            st["force_idle"] = 1\r
+    except Exception:\r
+        pass\r
+    try:\r
+        if hasattr(vc, "_vc_should_hold_listening_led") and vc._vc_should_hold_listening_led():\r
+            st["hold_listening"] = 1\r
+    except Exception:\r
+        pass\r
+    try:\r
+        if hasattr(vc, "_vc_led_should_standby_idle") and vc._vc_led_should_standby_idle():\r
+            st["force_idle"] = 1\r
+            if hasattr(vc, "_vc_led_standby_prefer_off") and vc._vc_led_standby_prefer_off():\r
+                st["prefer_off"] = 1\r
+    except Exception:\r
+        pass\r
+    try:\r
+        if hasattr(vc, "_vc_tts_playback_active") and vc._vc_tts_playback_active():\r
+            st["stream_active"] = 1\r
+    except Exception:\r
+        pass\r
+    try:\r
+        st["ptt_await_server"] = 1 if getattr(vc, "_ptt_await_server", 0) else 0\r
+    except Exception:\r
+        pass\r
+    return st\r
+\r
+\r
+def present_to_led_msg(vc, present=None):\r
+    """返回传给 set_led_state 的 dict;不发布。"""\r
+    if present is None:\r
+        present = build_present_state(vc)\r
+    if present.get("override_ai"):\r
+        msg = {"ai_state": present["override_ai"]}\r
+        if present.get("override_prefer_off") or present.get("prefer_off"):\r
+            msg["prefer_off"] = 1\r
+        return msg\r
+    if present.get("force_idle"):\r
+        msg = {"ai_state": "idle"}\r
+        if present.get("prefer_off"):\r
+            msg["prefer_off"] = 1\r
+        return msg\r
+    if present.get("hold_listening"):\r
+        return {"ai_state": "listening"}\r
+    if present.get("ptt_await_server") and int(present.get("mode", 0)) == 1:\r
+        return {"ai_state": "thinking"}\r
+    if present.get("stream_active"):\r
+        return {"ai_state": "playing"}\r
+    if derive_ux_state is not None:\r
+        try:\r
+            ux = derive_ux_state(vc)\r
+            if ux == UX_RECORDING:\r
+                return {"ai_state": "listening"}\r
+            if ux == UX_TTS:\r
+                return {"ai_state": "playing"}\r
+            if ux == UX_THINKING:\r
+                return {"ai_state": "thinking"}\r
+        except Exception:\r
+            pass\r
+    msg = {"ai_state": "idle"}\r
+    if present.get("prefer_off"):\r
+        msg["prefer_off"] = 1\r
+    return msg\r
index 27f4287..f93fd6d 100644 (file)
@@ -4,6 +4,17 @@ from vc_session_imports import *  # noqa: F401,F403
 import _thread
 from vc_session_io import VCSessionIoMixin
 from vc_session_media import VCSessionMediaMixin
+from vc_interrupt import InterruptContext
+from vc_overlay import overlay_keys_blocked, overlay_set, OVERLAY_IDLE
+from vc_present import build_present_state, present_to_led_msg
+from vc_notify import NotifyScheduler
+from vc_notify import (
+    OP_LONG_UPLINK, OP_ALARM_VOICE, OP_VOICE_URL, OP_SYNTHESIS,
+    OP_SYNTHESIS_HIS_TIMER, OP_SYNTHESIS_WAIT_TIMER, OP_SYNTHESIS_WAIT_SINGLE,
+    OP_SYNTHESIS_VOICE_TIMER, OP_SYNTHESIS_MUSIC_TIMER, OP_IDLE_UPLINK,
+    OP_PTT_ROUND, OP_UPLOAD_TONE,
+)
+from vc_mode_strategy import strategy_for_mode
 
 class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def _vc_fsm_log_snip(self):
@@ -39,6 +50,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         try:
             self._ptt_idle_latch = 0
             self._ptt_short_press_stop = 0
+            self._ptt_record_open_ms = 0
             self._ptt_interrupt_active = 0
             self._ptt_led_lock_listening = 0
             self._vc_play_end_suppress_idle_led = 0
@@ -53,15 +65,31 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         try:
             self._long_dialogue_key_arm = 0
             self._long_dialogue_interrupt_settled_ms = 0
+            self._vc_pending_key_press_tone = 0
+            self._notify_queue = []
             self._long_dialogue_end_interrupt()
         except Exception:
             pass
+        try:
+            self._led_override = None
+        except Exception:
+            pass
         try:
             self._drop_tts_audio = 0
             self.is_ruppt_tts = 0
             self._vc_last_interrupt_uplink_ms = 0
         except Exception:
             pass
+        try:
+            self._vc_turn_gen = 0
+            self._ptt_arm_epoch = 0
+        except Exception:
+            pass
+        try:
+            if getattr(self, "_intr", None) is not None:
+                self._intr.clear(self)
+        except Exception:
+            pass
 
     def _vc_should_hold_listening_led(self):
         """当前模式是否应强制保持 listening 灯(不混用另一模式的锁标志)。"""
@@ -100,12 +128,49 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         try:
             if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
                 return True
-            if self._vc_tts_pipeline_busy() or self._vc_local_play_busy():
+            # TTS 播完进入下一轮:play_end 时 PCM 尚未 close,勿用 pipeline_busy 拒开麦
+            if reason in ("local.play_end", "audio.played"):
+                if getattr(self, "_vc_session_started", 0) == 1:
+                    return True
+        except Exception:
+            return False
+        try:
+            if self._vc_tts_pipeline_busy():
                 return False
         except Exception:
             return False
         return True
 
+    def _vc_reset_long_idle_timer(self, tag=""):
+        """长对话 L1:用户开麦/有效轮次活动则清零无 TTS 超时计数。"""
+        try:
+            if getattr(self, "VC_mode", -1) != 0:
+                return
+            self.close_timerout = 0
+        except Exception:
+            pass
+
+    def _vc_long_dialogue_defer_vad_stop(self):
+        try:
+            return (
+                self._vc_is_long_dialogue_mode()
+                and getattr(self, "record_data_save_flag", 0) == 0
+            )
+        except Exception:
+            return False
+
+    def _vc_flush_deferred_vad_uplink_stop(self, reason="asr.completed"):
+        """长对话:asr.completed 再停麦,避免句中 vad.silence 把一句话切成两段。"""
+        try:
+            if not getattr(self, "_vc_defer_vad_uplink_stop", 0):
+                return
+            self._vc_defer_vad_uplink_stop = 0
+            self._server_vad_silence = 1
+            if getattr(self, "VC_record_state_flag", 0) != 0 and self._vc_vad_uplink_stop_eligible():
+                self.request_uplink_stop(reason)
+        except Exception:
+            pass
+
     def _long_dialogue_media_busy(self):
         """长对话:TTS/思考/播报中(按键须走打断,勿误走 continue)。"""
         try:
@@ -121,11 +186,17 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
 
     def _long_dialogue_end_interrupt(self):
         try:
-            self._vc_pending_interrupt_uplink = 0
-            self._vc_cancel_deadline_ms = 0
-            self.is_ruppt_tts = 0
-            self._long_dialogue_interrupt_settled_ms = utime.ticks_ms()
-            # 勿清 _drop_tts_audio:开麦后迟到 TTS 仍须丢弃,避免 VC_Play_Start 抢停录
+            if getattr(self, "_intr", None) is not None:
+                self._intr.settle(self)
+            else:
+                self._vc_pending_interrupt_uplink = 0
+                self._vc_cancel_deadline_ms = 0
+                self.is_ruppt_tts = 0
+                self._drop_tts_audio = 0
+                try:
+                    self._long_dialogue_interrupt_settled_ms = utime.ticks_ms()
+                except Exception:
+                    self._long_dialogue_interrupt_settled_ms = 0
         except Exception:
             pass
 
@@ -156,16 +227,18 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         return False
 
     def _long_dialogue_on_server_tts_started(self):
-        """æ\96°ä¸\80è½®æ\9c\8då\8a¡ç«¯ TTSï¼\88é\9d\9eæ\89\93æ\96­å°¾å\8c\85ï¼\89ï¼\9aå\85\81许ä¸\8bè¡\8cæ\92­æ\94¾。"""
+        """æ\96°ä¸\80è½®æ\9c\8då\8a¡ç«¯ TTSï¼\9aç»\93æ\9d\9fæ\89\93æ\96­ä¸¢å¼\83çª\97å\8f£ï¼\8cå\85\81许 VC_Play_Start ä¸\8eä¸\8bè¡\8cå\86\99å\85¥。"""
         if not self._vc_is_long_dialogue_mode():
             return
         try:
             if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
                 return
-            if getattr(self, "is_ruppt_tts", 0) == 1:
-                return
-            self._drop_tts_audio = 0
-            self._long_dialogue_interrupt_settled_ms = 0
+            if getattr(self, "_intr", None) is not None:
+                self._intr.end_drop_window(self)
+            else:
+                self.is_ruppt_tts = 0
+                self._drop_tts_audio = 0
+                self._long_dialogue_interrupt_settled_ms = 0
         except Exception:
             pass
 
@@ -176,8 +249,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         try:
             if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
                 return False
-            if getattr(self, "is_ruppt_tts", 0) == 1:
-                return False
             if getattr(self, "VC_record_state_flag", 0) == 0:
                 return False
             settled = getattr(self, "_long_dialogue_interrupt_settled_ms", 0)
@@ -206,23 +277,27 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             pass
 
     def _vc_long_dialogue_key_arm_session(self):
-        """é\95¿å¯¹è¯\9dæ\8c\89é\94®å¼\80å½\95æ\84\8få\9b¾ï¼\9aç­\89 server.started å\90\8eå\86\8dæ»´/å¼\80麦。"""
+        """é\95¿å¯¹è¯\9dæ\8c\89é\94®å\94¤é\86\92/ç»­è\81\8aï¼\9aæ»´ä¸\8eè\93\9dç\81¯å\9c¨ _vc_user_record_entry ç»\9fä¸\80å¤\84ç\90\86。"""
         try:
             self._long_dialogue_key_arm = 1
-            self._vc_arm_record_start_tone()
         except Exception:
             pass
 
     def _vc_ptt_mark_record_after_started(self):
-        """PTT é\95¿æ\8c\89ï¼\9aç­\89 started / asr.started å\90\8eå\86\8dæ»´。"""
+        """PTT é\95¿æ\8c\89ï¼\9aç­\89 started / asr.started å\90\8eå¼\80å½\95ï¼\8cæ\97 å¼\80å½\95æ\8f\90示é\9f³ï¼\88REQ-AI-07ï¼\89。"""
         try:
             self._ptt_force_record_on_started = 1
-            self._vc_arm_record_start_tone()
         except Exception:
             pass
 
     def _long_dialogue_key_down_arm(self, event=None, msg=None):
-        """EXTI 线程同步:先于 btn 队列置打断标志,避免 WS canceled 抢跑。"""
+        """EXTI 仅投递;业务在编排线程 _long_dialogue_key_down_arm_impl。"""
+        EventMesh.publish(
+            "publish_AI_btnInfo",
+            {"func": self._long_dialogue_key_down_arm_impl},
+        )
+
+    def _long_dialogue_key_down_arm_impl(self, event=None, msg=None):
         if not self._vc_is_long_dialogue_mode():
             return
         try:
@@ -233,8 +308,17 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         if not self._long_dialogue_media_busy():
             return
         try:
-            self._vc_pending_interrupt_uplink = 1
-            self.is_ruppt_tts = 1
+            wait_ms = 800
+            if "vc_config" in globals() and vc_config:
+                wait_ms = int(vc_config.get_int("LONG_DIALOGUE_CANCEL_WAIT_MS", wait_ms))
+            if getattr(self, "_intr", None) is not None:
+                self._intr.begin(
+                    self, InterruptContext.KIND_LONG_SHORT,
+                    pending_uplink=1, cancel_wait_ms=wait_ms,
+                )
+            else:
+                self._vc_pending_interrupt_uplink = 1
+                self.is_ruppt_tts = 1
         except Exception:
             pass
 
@@ -538,8 +622,196 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 self.fsm.await_played = False
         except Exception:
             pass
+    def _vc_ms_key_block_ms(self):
+        try:
+            if "vc_config" in globals() and vc_config:
+                return int(vc_config.get_int("VC_MODE_SWITCH_KEY_BLOCK_MS", 500))
+        except Exception:
+            pass
+        return 500
+
+    def _vc_ms_keys_blocked(self, tag=""):
+        """兼容旧名;统一走 vc_overlay.overlay_keys_blocked。"""
+        return overlay_keys_blocked(self, tag)
+
+    def _vc_get_strategy(self):
+        try:
+            m = int(getattr(self, "VC_mode", 0))
+        except Exception:
+            m = 0
+        try:
+            cached_m = getattr(self, "_vc_mode_strategy_mode", -1)
+            if cached_m != m or getattr(self, "_vc_mode_strategy", None) is None:
+                self._vc_mode_strategy = strategy_for_mode(m)
+                self._vc_mode_strategy_mode = m
+        except Exception:
+            self._vc_mode_strategy = strategy_for_mode(m)
+            self._vc_mode_strategy_mode = m
+        return self._vc_mode_strategy
+
+    def _vc_notify_queue_push(self, tts_str, is_close_vc):
+        try:
+            q = getattr(self, "_notify_queue", None)
+            if q is None:
+                self._notify_queue = []
+                q = self._notify_queue
+            q.append((tts_str or "", 1 if is_close_vc else 0))
+            print("[VC][notify] defer queue len={}".format(len(q)))
+        except Exception:
+            pass
+
+    def _vc_notify_queue_flush(self):
+        try:
+            q = getattr(self, "_notify_queue", None)
+            if not q:
+                return
+            pending = list(q)
+            self._notify_queue = []
+            for tts_str, is_close in pending:
+                print("[VC][notify] flush play pending={}".format(len(pending)))
+                self.play_tts_cb([tts_str, is_close])
+        except Exception as e:
+            try:
+                usys.print_exception(e)
+            except Exception:
+                pass
+
+    def _vc_apply_notify_started_plan(self, plan):
+        """I3:server.started 媒体/开麦计划(编排线程或 recv 内调用)。"""
+        if not plan:
+            return
+        for item in plan:
+            try:
+                op = item.get("op") if isinstance(item, dict) else item
+            except Exception:
+                continue
+            if op == OP_LONG_UPLINK:
+                try:
+                    if getattr(self, "_long_dialogue_key_arm", 0) == 1:
+                        self._long_dialogue_publish_listening("server.started")
+                except Exception:
+                    pass
+                try:
+                    self._vc_on_server_record_uplink_ready("server.started")
+                except Exception:
+                    pass
+                if getattr(self, "record_data_save_flag", 0) == 0:
+                    if getattr(self, "VC_record_state_flag", 0) == 0:
+                        self.request_uplink_start("server.started")
+                    else:
+                        self._vc_clear_stale_record_state("server.started")
+                        self.request_uplink_start("server.started/retry")
+            elif op == OP_ALARM_VOICE:
+                self.AI_VC_start()
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_new_voice})
+                self.alarm_play_flag = 0
+                self.voice_choose_flag = 1
+            elif op == OP_VOICE_URL:
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_new_voice})
+            elif op == OP_SYNTHESIS:
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+            elif op == OP_SYNTHESIS_HIS_TIMER:
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+                self.his_cnt = 0
+                self.his_timer.start(1000, 1, self.history_play)
+                self.voice_choose_flag = 1
+            elif op == OP_SYNTHESIS_WAIT_TIMER:
+                self.wait_cnt = 0
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+                self.wait_timer.start(1000, 1, self.wait_play)
+                self.voice_choose_flag = 1
+            elif op == OP_SYNTHESIS_WAIT_SINGLE:
+                self.wait_cnt = 0
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+                self.wait_timer.start(1000, 1, self.wait_single_play)
+                self.voice_choose_flag = 1
+            elif op == OP_SYNTHESIS_VOICE_TIMER:
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+                self.voice_cnt = 0
+                self.voice_timer.start(1000, 1, self.voice_judge)
+                self.voice_choose_flag = 1
+            elif op == OP_SYNTHESIS_MUSIC_TIMER:
+                self.music_play_flag = 0
+                EventMesh.publish(
+                    "publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+                self.music_timer.start(1000, 1, self.music_play)
+                self.voice_choose_flag = 1
+            elif op == OP_UPLOAD_TONE:
+                try:
+                    self.chenggong_tone()
+                except Exception:
+                    pass
+            elif op == OP_IDLE_UPLINK:
+                self.request_uplink_start("server.started/idle")
+            elif op == OP_PTT_ROUND:
+                _ptt_rec = getattr(self, "_ptt_force_record_on_started", 0) == 1
+                if _ptt_rec:
+                    try:
+                        self._ptt_force_record_on_started = 0
+                    except Exception:
+                        pass
+                try:
+                    self.AI_client_VC_send_speech()
+                except Exception:
+                    self.request_send_audio_create()
+                try:
+                    if _ptt_rec:
+                        self._vc_user_record_entry("ptt_begin/started")
+                except Exception:
+                    pass
+
+    def _vc_send_protocol_cancel(self, reason=""):
+        """协议 cancel(Power / 打断)。"""
+        try:
+            if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+                self.AI_client_VC_send(ujson.dumps({"event": "cancel", "payload": {}}))
+                print("[VC] cancel reason={}".format(reason))
+        except Exception:
+            pass
+
+    def _ptt_abort_short_record(self, tts_str):
+        """PTT 录音过短:cancel + notify TTS + idle。"""
+        try:
+            self.AI_key_long_press_flag = 0
+            self._ptt_defer_speech_until_asr = 0
+            self._ptt_audio_created = 0
+            self._ptt_record_open_ms = 0
+        except Exception:
+            pass
+        try:
+            self._vc_send_protocol_cancel("ptt_short_record")
+        except Exception:
+            pass
+        try:
+            self.VC_Speech_End()
+        except Exception:
+            pass
+        try:
+            self._ptt_idle_latch = 1
+            self._ptt_await_server = 0
+        except Exception:
+            pass
+        try:
+            if tts_str:
+                EventMesh.publish("play_tts", [tts_str, 0, "prompt"])
+        except Exception:
+            pass
+        try:
+            self._vc_publish_led("idle", "ptt_short_record")
+        except Exception:
+            pass
+
     def _handle_pwk_short_release(self, event=None, msg=None, merged_ai_tail=False):
-        """电源键短按:读留言标志、退会议/翻译;否则 Close,或单键模式下走 AI_key_release。"""
+        """电源键短按:读留言/会议;否则按 UX 表 cancel+Close(清 sid)。"""
+        if self._vc_ms_keys_blocked("pwk_release"):
+            return
         print("pwk_key_release")
         if EventMesh.publish("get_recv_voice") == 1:
             EventMesh.publish("update_play_new_voice_info")
@@ -553,7 +825,11 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         if merged_ai_tail:
             self.AI_key_release(event, msg)
         else:
-            EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
+            try:
+                from vc_key_router import KeyRouter
+                KeyRouter.handle_power_short(self)
+            except Exception:
+                EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
         EventMesh.publish("stop_sos_call")
     def _ptt_is_active_tts_playback(self):
         """下行 TTS 真在播(PCM/VC_Play);不含仅 Thinking 等服务器。"""
@@ -594,6 +870,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def _ptt_should_hold_listening_led(self):
         """单对话打断/cancel 等待/录音:保持聆听蓝快闪,勿经 fsm 回到 playing/thinking。"""
         try:
+            if getattr(self, "_vc_defer_listening_led", 0) == 1:
+                return False
             if getattr(self, "VC_mode", -1) != 1:
                 return False
             if getattr(self, "_ptt_led_lock_listening", 0) == 1:
@@ -640,6 +918,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def _long_dialogue_should_hold_listening_led(self):
         """长对话打断等待/录音:保持蓝慢闪(含 cancel→开麦 窗口)。"""
         try:
+            if getattr(self, "_vc_defer_listening_led", 0) == 1:
+                return False
             if getattr(self, "VC_mode", -1) != 0:
                 return False
             if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
@@ -657,23 +937,28 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         if not self._vc_is_long_dialogue_mode():
             return
         try:
+            self._vc_turn_bump("long_interrupt")
             self.Close_flag = 0
-            self.is_ruppt_tts = 1
-            self._drop_tts_audio = 1
-            self._vc_pending_interrupt_uplink = 1
-            self._vc_arm_record_start_tone()
             self.fsm.await_played = False
             wait_ms = 800
             if "vc_config" in globals() and vc_config:
                 wait_ms = int(vc_config.get_int("LONG_DIALOGUE_CANCEL_WAIT_MS", wait_ms))
-            self._vc_cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), wait_ms)
+            if getattr(self, "_intr", None) is not None:
+                self._intr.begin(
+                    self, InterruptContext.KIND_LONG_SHORT,
+                    pending_uplink=1, cancel_wait_ms=wait_ms,
+                )
+            else:
+                self.is_ruppt_tts = 1
+                self._drop_tts_audio = 1
+                self._vc_pending_interrupt_uplink = 1
+                self._vc_cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), wait_ms)
         except Exception:
             pass
         try:
             self._vc_stop_downlink_for_interrupt("long_press/interrupt")
         except Exception:
             pass
-        self._long_dialogue_publish_listening("long_press/interrupt")
         try:
             if getattr(self, "AI_client_VC_isconnect", 0) == 1:
                 self.AI_client_VC_send(ujson.dumps({"event": "cancel", "payload": {}}))
@@ -694,6 +979,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         self._ptt_short_press_stop = 1
         self._ptt_idle_latch = 1
         try:
+            if self._vc_ptt_need_interrupt_wait() or self._vc_tts_pipeline_busy():
+                self._vc_turn_bump("ptt_short_stop")
             self.is_ruppt_tts = 1
             self._drop_tts_audio = 1
             self._ptt_interrupt_active = 0
@@ -766,6 +1053,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 return True
             self.Close_flag = 0
             self._skip_started_uplink_once = 0
+            self._vc_reset_long_idle_timer("key.press/continue")
             self._vc_long_dialogue_key_arm_session()
             self._long_dialogue_publish_listening("key.press/continue")
             self._vc_clear_stale_record_state("key.press/continue")
@@ -807,11 +1095,64 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             EventMesh.publish("set_led_state", {"alert": "rb_flash_2"})
         except Exception:
             pass
+    def _vc_restart_record_same_dialog(self):
+        """长对话 recording+短按:停录后立即重开(旧 sid,对齐设计稿)。"""
+        if not self._vc_is_long_dialogue_mode():
+            return False
+        try:
+            self.Close_flag = 0
+            self._skip_started_uplink_once = 0
+            self._vc_reset_long_idle_timer("key.restart")
+            self._vc_long_dialogue_key_arm_session()
+            self._long_dialogue_publish_listening("key.restart")
+            self._vc_clear_stale_record_state("key.restart/pre")
+            self.request_uplink_stop("key.restart/stop")
+            self._vc_clear_stale_record_state("key.restart/post")
+            self.request_uplink_start("key.restart/start")
+            return True
+        except Exception as e:
+            usys.print_exception(e)
+        return False
+
+    def _ptt_stop_dialog_to_idle(self):
+        """PTT thinking+短按:停止对话 → idle(设计稿)。"""
+        if not self._vc_is_ptt_mode():
+            return
+        try:
+            self._ptt_await_server = 0
+            self._ptt_idle_latch = 1
+            self.is_ruppt_tts = 1
+            self._drop_tts_audio = 1
+            self._ptt_interrupt_active = 0
+            self.fsm.await_played = False
+        except Exception:
+            pass
+        try:
+            self._vc_stop_downlink_for_interrupt("ptt_thinking_short")
+        except Exception:
+            pass
+        try:
+            if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+                self.AI_client_VC_send(ujson.dumps({"event": "cancel", "payload": {}}))
+                print("[PTT] thinking short -> cancel idle")
+        except Exception:
+            pass
+        try:
+            self._vc_publish_led("idle", "ptt_thinking_short")
+        except Exception:
+            pass
+
     def _ai_key_release_handle_ptt(self):
         """单对话 PTT 松手:仅在上行录音中时 end_speech;禁止误触发 AI_VC_start(否则新 sid / cancel 竞态)。"""
         if not self._vc_is_ptt_mode():
             return
         if self.AI_key_long_press_flag == 0:
+            try:
+                from vc_key_router import KeyRouter
+                if KeyRouter.handle_ptt_release(self):
+                    return
+            except Exception:
+                pass
             try:
                 if self._ptt_is_active_tts_playback():
                     self._ptt_stop_tts_on_short_press()
@@ -819,6 +1160,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             except Exception:
                 pass
         if self.AI_key_long_press_flag == 1:
+            try:
+                from vc_key_router import KeyRouter
+                if KeyRouter.ptt_release_after_long_press(self):
+                    return
+            except Exception:
+                pass
             if getattr(self, "VC_record_state_flag", 0) != 0:
                 try:
                     gap = 200
@@ -831,6 +1178,24 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 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()
+                try:
+                    self._ptt_record_open_ms = 0
+                except Exception:
+                    pass
+            elif getattr(self, "_ptt_audio_created", 0) == 1 or getattr(
+                self, "_ptt_defer_speech_until_asr", 0
+            ) == 1:
+                self.AI_key_long_press_flag = 0
+                try:
+                    self._ptt_defer_speech_until_asr = 0
+                    self._ptt_audio_created = 0
+                    self._ptt_abort_interrupt_arm()
+                    if getattr(self, "AI_client_VC_isconnect", 0) == 1:
+                        self.AI_client_VC_send(
+                            ujson.dumps({"event": "cancel", "payload": {}}))
+                    print("[PTT] release cancel pending audio.create (no record)")
+                except Exception:
+                    pass
             else:
                 self.AI_key_long_press_flag = 0
                 try:
@@ -846,21 +1211,20 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             pass
         print("[PTT] release_short_no_long")
+    def _vc_key_router_press_cb(self, event=None, msg=None):
+        try:
+            from vc_key_router import KeyRouter
+            KeyRouter.dispatch_press(self)
+        except Exception as e:
+            usys.print_exception(e)
+
     def _ai_key_long_press_route(self, event=None, msg=None):
-        """长按:翻译先 Close 再开翻译录音;单对话→ptt_begin;长对话→走长对话短按分发。"""
-        if self.trans_flag == 1:
-            self.Close()
-            EventMesh.publish("start_voice_record", 4)
-        self.AI_key_long_press_flag = 1
-        if self._vc_is_ptt_mode():
-            try:
-                self._ptt_idle_latch = 0
-                self._ptt_short_press_stop = 0
-            except Exception:
-                pass
-            EventMesh.publish("publish_AI_btnInfo", {"func": self.ptt_begin})
-        elif self._vc_is_long_dialogue_mode():
-            EventMesh.publish("publish_AI_btnInfo", {"func": self._ai_key_press_long_dialogue_dispatch})
+        """长按:由 VCModeStrategy 分发(PTT ptt_begin / 长对话短按分发)。"""
+        try:
+            if self._vc_get_strategy().on_key_long_press(self):
+                return
+        except Exception as e:
+            usys.print_exception(e)
     def _ptt_end_led_suppress(self):
         # 保持 _vc_play_end_suppress_idle_led / _ptt_led_lock_listening 直至真开录(VC_Speech_Start)
         pass
@@ -884,7 +1248,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         return False
     def _ptt_should_skip_remote_playback(self):
         return self._ptt_should_drop_inbound_tts()
-    def _ptt_enter_interrupt_arm(self):
+    def _ptt_enter_interrupt_arm(self, bump_gen=True):
         if not self._vc_is_ptt_mode():
             return
         try:
@@ -894,7 +1258,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             pass
         try:
-            self._ptt_arm_epoch = int(getattr(self, "_ptt_arm_epoch", 0)) + 1
+            if bump_gen:
+                self._vc_turn_bump("ptt_interrupt")
             self._ptt_interrupt_active = 1
             self._ptt_led_lock_listening = 1
             self._vc_play_end_suppress_idle_led = 1
@@ -902,6 +1267,20 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             self.is_ruppt_tts = 1
         except Exception:
             pass
+
+    def _ptt_prepare_idle_round(self):
+        """单对话新一轮(非打断):清锁/丢弃标志,不递增 turn_gen。"""
+        if not self._vc_is_ptt_mode():
+            return
+        try:
+            self._ptt_abort_interrupt_arm()
+            self._ptt_idle_latch = 0
+            self._ptt_short_press_stop = 0
+            self._ptt_await_server = 0
+            self._ptt_led_lock_listening = 1
+            self._vc_play_end_suppress_idle_led = 1
+        except Exception:
+            pass
     def _ptt_abort_interrupt_arm(self):
         if not self._vc_is_ptt_mode():
             return
@@ -965,13 +1344,45 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 self.VC_Play_End()
         except Exception:
             pass
-    def _vc_enqueue_btn(self, func, data=None):
-        """统一经 publish_AI_btnInfo 入队,在 btnInfo_queue_thread 串行执行。"""
+    def _vc_turn_current(self):
+        try:
+            return int(getattr(self, "_vc_turn_gen", 0))
+        except Exception:
+            return 0
+
+    def _vc_turn_stale(self, gen):
+        try:
+            if gen is None or int(gen) < 0:
+                return False
+            return int(gen) != self._vc_turn_current()
+        except Exception:
+            return False
+
+    def _vc_turn_bump(self, reason=""):
+        """打断/新轮次:递增 turn_gen 并 purge 队列中过期 VC 项。"""
+        try:
+            self._vc_turn_gen = int(getattr(self, "_vc_turn_gen", 0)) + 1
+            self._ptt_arm_epoch = self._vc_turn_gen
+            print("[ORCH] turn_gen={} reason={}".format(self._vc_turn_gen, reason))
+            EventMesh.publish("vc_orchestrator_purge", self._vc_turn_gen)
+        except Exception:
+            pass
+        return self._vc_turn_current()
+
+    def _vc_turn_gen_get_cb(self, event=None, msg=None):
+        return self._vc_turn_current()
+
+    def _vc_enqueue_btn(self, func, data=None, priority=0, track_gen=False):
+        """入编排队列。仅 track_gen=True 的项参与 turn_gen 过期丢弃(服务端 FSM 等)。"""
         try:
+            msg = {"func": func}
+            if track_gen:
+                msg["vc_track_gen"] = self._vc_turn_current()
+            if priority:
+                msg["vc_prio"] = 1
             if data is not None:
-                EventMesh.publish("publish_AI_btnInfo", {"func": func, "data": data})
-            else:
-                EventMesh.publish("publish_AI_btnInfo", {"func": func})
+                msg["data"] = data
+            EventMesh.publish("publish_AI_btnInfo", msg)
         except Exception:
             pass
     def _vc_ptt_cancel_wait_ms(self):
@@ -982,10 +1393,14 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             pass
         return 2500
     def _vc_ptt_need_interrupt_wait(self):
-        """å\8d\95对è¯\9dï¼\9aæ\9c\8då\8a¡ç«¯ä»\8då\9c¨æ\80\9dè\80\83/æ\94¶ TTS/æ\92­æ\8a¥时,开录前须先 cancel 并等 canceled。"""
+        """å\8d\95对è¯\9dï¼\9aæ\80\9dè\80\83/TTS/æ\92­ä¸­/æ\9d¾æ\89\8bç­\89æ\9c\8då\8a¡å\99¨时,开录前须先 cancel 并等 canceled。"""
         try:
             if getattr(self, "VC_mode", -1) != 1:
                 return False
+            if getattr(self, "_ptt_await_server", 0) == 1:
+                return True
+            if getattr(self, "_vc_serial_canceling", 0) == 1:
+                return True
             turn = getattr(self.fsm, "turn", VCTurnState.Idle)
             if self._vc_tts_pipeline_busy():
                 return True
@@ -999,6 +1414,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         return False
     def _vc_begin_ptt_cancel_wait(self, reason="ptt_interrupt"):
         try:
+            self._ptt_await_server = 0
             self._ptt_pending_arm = 1
             self._vc_serial_canceling = 1
             try:
@@ -1063,6 +1479,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def _ptt_arm_uplink(self, t0=0):
         """btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""
         try:
+            self._ptt_await_server = 0
             self.is_ruppt_tts = 0
         except Exception:
             pass
@@ -1079,17 +1496,9 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             pass
         try:
-            self._vc_arm_record_start_tone()
-            if (
-                getattr(self, "_vc_session_started", 0) == 1
-                and getattr(self, "_vc_pending_record_start_tone", 0) == 1
-            ):
-                self._ptt_defer_speech_until_asr = 1
-            else:
-                self.VC_Speech_Start_now()
+            self._vc_user_record_entry("ptt_begin/arm")
         except Exception:
             pass
-        self._ptt_publish_listening_fast("ptt_begin/recording")
         self._ptt_end_led_suppress()
         try:
             if t0:
@@ -1103,7 +1512,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         # PTT 新一轮开始:清理模式切换/提示音退出标志
         try:
             self.Close_flag = 0
-            self._ptt_await_server = 0
         except Exception:
             pass
 
@@ -1113,27 +1521,69 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             t0 = 0
 
-        self._ptt_enter_interrupt_arm()
-        self._ptt_publish_listening_fast("ptt_begin/pre")
-
-        # 本地先硬静音再 VC_init,避免 DMA/队列尾包漏播
         try:
-            self._ptt_hard_mute_local_playback()
+            if getattr(self, "_vc_serial_canceling", 0) or getattr(self, "_ptt_pending_arm", 0):
+                self._ptt_pending_arm = 1
+                print("[PTT] ptt_begin skip (cancel/arm pending)")
+                self._ptt_end_led_suppress()
+                return
         except Exception:
             pass
+
+        need_cancel = self._vc_ptt_need_interrupt_wait()
         try:
-            self.VC_init()
+            if (
+                not need_cancel
+                and getattr(self, "VC_record_state_flag", 0) != 0
+            ):
+                print("[PTT] ptt_begin skip (already recording)")
+                return
         except Exception:
             pass
+
+        if need_cancel:
+            self._ptt_enter_interrupt_arm(bump_gen=True)
+        else:
+            self._ptt_prepare_idle_round()
+
+        # 聆听灯在 _vc_user_record_uplink_open 与上行一并点亮(ptt_begin/pre 不再抢先亮灯)
+
+        # 已连接会话:勿 VC_init(会误触 Play_End/清队列导致 FSM/灯乱)
         try:
-            if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:
-                self.VC_Play_End()
+            in_session = (
+                getattr(self, "AI_client_VC_isconnect", 0) == 1
+                and getattr(self, "_vc_session_started", 0) == 1
+            )
         except Exception:
-            pass
+            in_session = False
         try:
             self._ptt_hard_mute_local_playback()
         except Exception:
             pass
+        if in_session:
+            try:
+                if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:
+                    self.VC_Play_End()
+            except Exception:
+                pass
+            try:
+                self._ptt_hard_mute_local_playback()
+            except Exception:
+                pass
+        else:
+            try:
+                self.VC_init()
+            except Exception:
+                pass
+            try:
+                if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:
+                    self.VC_Play_End()
+            except Exception:
+                pass
+            try:
+                self._ptt_hard_mute_local_playback()
+            except Exception:
+                pass
 
         # 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错
         try:
@@ -1210,6 +1660,9 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 return False
             if getattr(self, "_ptt_await_server", 0) == 1:
                 return False
+            # 单对话:松手等 ASR/LLM 仅 _ptt_await_server→thinking;TTS 播完勿因 FSM.turn=Playing 再亮思考灯
+            if getattr(self, "VC_mode", -1) == 1:
+                return True
             t = getattr(self.fsm, "turn", VCTurnState.Idle)
             if t in (
                 VCTurnState.Thinking,
@@ -1220,51 +1673,42 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 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="", prefer_off=0):
+    def get_vc_present_state(self, event=None, msg=None):
         try:
-            if self._ptt_should_force_idle_led():
-                if ai_state != "idle":
-                    if self._vc_led_state_debug_enabled():
-                        print("[LED] publish coerce ai={} -> idle reason={} (ptt_stop_latch)".format(
-                            ai_state, reason))
-                    ai_state = "idle"
+            return build_present_state(self)
         except Exception:
-            pass
+            return {}
+
+    def _vc_led_present_sync(self, event=None, msg=None):
+        """Presenter 唯一下发 LED(I2)。"""
         try:
-            if (
-                getattr(self, "_vc_idle_close_in_progress", 0) == 0
-                and self._vc_should_hold_listening_led()
-            ):
-                if ai_state != "listening":
-                    if self._vc_led_state_debug_enabled():
-                        print("[LED] publish coerce ai={} -> listening reason={} (listen_hold)".format(
-                            ai_state, reason))
-                    ai_state = "listening"
+            led_msg = present_to_led_msg(self)
+            if self._vc_led_state_debug_enabled():
+                print("[LED] present_sync -> {}".format(led_msg))
+            EventMesh.publish("set_led_state", led_msg)
         except Exception:
             pass
+
+    def _vc_publish_led(self, ai_state, reason="", prefer_off=0):
+        """ imperative 灯态意图;由 Presenter 与 latch 合并后单次下发。"""
         try:
             if self._vc_led_state_debug_enabled():
-                print("[LED] publish ai={} reason={} VC_mode={} rec={} turn={}".format(
+                print("[LED] publish intent ai={} reason={} VC_mode={} 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:
-            led_msg = {"ai_state": ai_state}
-            if prefer_off:
-                led_msg["prefer_off"] = 1
-            EventMesh.publish("set_led_state", led_msg)
+            self._led_override = (ai_state, 1 if prefer_off else 0)
         except Exception:
             pass
+        self._vc_led_present_sync()
     def _vc_led_standby_prefer_off(self):
         """模式切换/提示音播完待机:优先灭灯,勿被 charging=full 拉成绿灯常亮。"""
         try:
@@ -1277,66 +1721,22 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             pass
         return False
     def _vc_led_sync_from_fsm(self):
-        """按 FSM/真实 TTS 播放恢复 AI 灯;本地 aud.play 短音不参与。"""
+        """按 FSM/媒体/锁推导灯态(I2 Presenter)。"""
         try:
-            if self._ptt_should_force_idle_led():
-                self._vc_publish_led("idle", "fsm_sync/ptt_stop_latch")
-                return
+            self._led_override = None
         except Exception:
             pass
-        try:
-            if self._vc_should_hold_listening_led():
-                self._vc_publish_listening_for_mode("fsm_sync/listen_hold")
-                return
-        except Exception:
-            pass
-        try:
-            if self._vc_led_should_standby_idle():
-                self._vc_publish_led(
-                    "idle",
-                    "fsm_sync/standby_idle",
-                    prefer_off=1 if self._vc_led_standby_prefer_off() else 0,
-                )
-                return
-        except Exception:
-            pass
-        try:
-            if self._vc_is_ptt_mode() 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
+        self._vc_led_present_sync()
     def _fsm_apply_actions(self, actions):
+        """编排线程内直接执行动作,避免 FSM→action→再次入队加深 FIFO。"""
         try:
             for a in actions:
                 if not a:
                     continue
                 if a[0] == "uplink_stop":
-                    self.request_uplink_stop(a[1] if len(a) > 1 else "")
+                    self._uplink_request_stop(a[1] if len(a) > 1 else "")
                 elif a[0] == "uplink_start":
-                    self.request_uplink_start(a[1] if len(a) > 1 else "")
+                    self._uplink_request_start(a[1] if len(a) > 1 else "")
                 elif a[0] == "play_start":
                     if getattr(self, "VC_mode", -1) == 1:
                         if self._ptt_should_skip_remote_playback():
@@ -1347,9 +1747,23 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                         self.fsm.on_local_play_start()
                     except Exception:
                         pass
-                    EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_Start})
+                    self.VC_Play_Start()
                 elif a[0] == "send_audio_played":
-                    self.request_send_audio_played()
+                    try:
+                        self.AI_client_VC_audio_played()
+                    except Exception:
+                        self.request_send_audio_played()
+                elif a[0] == "led_sync":
+                    self._vc_led_present_sync()
+                elif a[0] == "stop_downlink":
+                    self._vc_stop_downlink_for_interrupt(a[1] if len(a) > 1 else "action")
+                elif a[0] == "send_cancel":
+                    try:
+                        if getattr(self, "AI_client_VC_isconnect", 0) == 1:
+                            self.AI_client_VC_send(
+                                ujson.dumps({"event": "cancel", "payload": {}}))
+                    except Exception:
+                        pass
         except Exception:
             pass
     def _print_hw_diag(self):
@@ -1434,9 +1848,30 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             self.VC_mode = 0
         self.is_up_error= 0 #是否上传错误id
         self.Close_flag =0 #=1时,连续对话播放完音频就退出
+        # 叠加态 / 模式切换(vc_overlay,兼容 _vc_ms_phase)
+        self._vc_overlay = OVERLAY_IDLE
+        self._vc_ms_phase = 0
+        self._intr = InterruptContext()
+        self._led_override = None
+        self._vc_pending_key_press_tone = 0
+        self._vc_defer_listening_led = 0
+        self._vc_skip_local_play_stop_record = 0
+        self._vc_user_record_open_reason = ""
+        self._vc_user_record_tone_timer = osTimer()
+        self._notify_queue = []
+        self._vc_mode_strategy = strategy_for_mode(self.VC_mode)
+        self._vc_ms_gen = 0
+        self._vc_ms_active_gen = 0
+        self._vc_ms_play_gen = 0
+        self._vc_ms_pending_tts_str = ""
+        self._vc_ms_close_pending = 0
+        self._vc_ms_key_block_until_ms = 0
+        self._vc_close_reason = ""
         # 服务端 VAD 结束标志:收到 vad.silence 后置 1,用于硬闸门禁止继续上行音频
         # (避免仍持续上行导致服务端推迟/抑制后续 TTS,从而一直停留在思考态蓝绿闪)
         self._server_vad_silence = 0
+        self._vc_defer_vad_uplink_stop = 0
+        self._vc_vad_had_active_after_silence = 0
         # 单轮状态标志:用于判定 completed 是否应自动进入下一轮
         self._turn_has_asr_completed = 0
         self._turn_has_vad_silence = 0
@@ -1457,7 +1892,9 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         self._vc_serial_canceling = 0
         self._ptt_pending_arm = 0
         self._vc_cancel_deadline_ms = 0
-        # PTT 打断:轮次号 + 开录前丢弃旧 TTS / 灯只显示聆听
+        # 编排器轮次号(方案 A):打断/新轮次递增,队列项带 vc_gen 过期丢弃
+        self._vc_turn_gen = 0
+        # PTT 打断轮次(与 _vc_turn_gen 同步,兼容旧逻辑)
         self._ptt_arm_epoch = 0
         self._ptt_interrupt_active = 0
         self._ptt_led_lock_listening = 0
@@ -1784,7 +2221,10 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         if self.is_noread_voice == None:
             self.is_noread_voice = 0#是否还有未读留言(开机判断)
         self.is_ruppt_tts = 0#是否已经打断tts播放
+        EventMesh.subscribe("vc_turn_gen_get", self._vc_turn_gen_get_cb)
         EventMesh.subscribe("vc_long_dialogue_key_down", self._long_dialogue_key_down_arm)
+        EventMesh.subscribe("vc_led_sync", self._vc_led_present_sync)
+        EventMesh.subscribe("get_vc_present_state", self.get_vc_present_state)
         EventMesh.subscribe("AI_key_press", self.AI_key_press)
         EventMesh.subscribe("BT_AI_key_press", self.BT_AI_key_press)
         EventMesh.subscribe("stop_AI_record", self.stop_AI_record)
@@ -2104,6 +2544,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def ai_key_press_audio_cb2(self, event=None, msg = None):
         EventMesh.publish("audio_montage_play",["chenggong.mp3"])  
     def AI_key_press(self, event=None, msg = None):#按键按下
+        if self._vc_ms_keys_blocked("AI_press"):
+            return
         # 按键时打印 LED 聚合状态/最终模式(用于定位“绿灯不灭/闪一下”)
         try:
             if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
@@ -2119,11 +2561,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                     getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
         except Exception:
             pass
-        # 双模式互斥:先 PTT(VC_mode==1),再长对话(VC_mode==0)
         try:
-            if self._ai_key_press_ptt_mode():
-                return
-            if self._ai_key_press_long_dialogue_dispatch():
+            if self._vc_get_strategy().on_key_press(self):
                 return
         except Exception as e:
             usys.print_exception(e)
@@ -2171,9 +2610,9 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         try:
             if self._vc_is_long_dialogue_mode():
                 if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
-                    EventMesh.publish("publish_AI_btnInfo", {"func": self._ai_key_press_long_dialogue_dispatch})
+                    self._vc_enqueue_btn(self._ai_key_press_long_dialogue_dispatch, priority=1)
                     return
-                EventMesh.publish("publish_AI_btnInfo", {"func": self._ai_key_press_long_dialogue_dispatch})
+                self._vc_enqueue_btn(self._ai_key_press_long_dialogue_dispatch, priority=1)
                 return
         except Exception:
             pass
@@ -2222,6 +2661,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def exit_voice_message_timer(self,*argc):
         EventMesh.publish("exit_voice_message_mode")
     def AI_key_release(self, event=None, msg=None):#按键弹起
+        if self._vc_ms_keys_blocked("AI_release"):
+            return
         try:
             if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
                 try:
@@ -2251,41 +2692,135 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             self.voice_flag=0
             EventMesh.publish("play_tts",[tts_play_str,1])
             return
-        if self.VC_mode == 1:
-            self._ai_key_release_handle_ptt()
+        try:
+            if self._vc_get_strategy().on_key_release(self):
+                return
+        except Exception:
+            pass
+    def _vc_mode_switch_prep(self, gen):
+        """模式切换 PREP:软停播/断 WS/停线程,不调用完整 Close(避免 play_pcm.close 掐提示音)。"""
+        try:
+            print("[VC][ms] prep gen={} {}".format(gen, self._vc_fsm_log_snip()))
+        except Exception:
+            pass
+        try:
+            self._vc_ms_play_gen = -1
+        except Exception:
+            pass
+        try:
+            self._vc_stop_downlink_for_interrupt("mode_switch_prep")
+        except Exception:
+            pass
+        try:
+            self._uplink_request_stop("mode_switch_prep")
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_satrt_flag", 0) == 1 or getattr(self, "AI_client_VC_isconnect", 0) == 1:
+                self.websocket_deconnect_VC()
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_satrt_flag", 0) == 1:
+                self.AI_audio_queue_thread_close()
+                self.AI_VC_send_data_queue_thread_close()
+        except Exception:
+            pass
+        try:
+            self.VC_satrt_flag = 0
+            self._vc_session_started = 0
+            self.is_ruppt_tts = 0
+            self._drop_tts_audio = 0
+            self._tts_pending = 0
+            self._need_audio_played_ack = 0
+        except Exception:
+            pass
+        try:
+            if hasattr(self, "fsm") and self.fsm is not None:
+                self.fsm.speech_uplink_open = 0
+                self.fsm.turn = VCTurnState.Idle
+                self.fsm.await_played = False
+                self.fsm.reset_rx_phases()
+        except Exception:
+            pass
+        try:
+            if hasattr(self, "_vc_tts_rx_counters_reset"):
+                self._vc_tts_rx_counters_reset()
+        except Exception:
+            pass
+
+    def _vc_mode_switch_run(self, event=None, msg=None):
+        """btnInfo 串行:PREP -> play_tts(Close_flag=1) -> play_end -> Close。"""
+        try:
+            gen = int(msg) if msg is not None else int(getattr(self, "_vc_ms_gen", 0))
+        except Exception:
+            gen = int(getattr(self, "_vc_ms_gen", 0))
+        try:
+            active = int(getattr(self, "_vc_ms_active_gen", 0))
+        except Exception:
+            active = 0
+        if gen < active:
+            print("[VC][ms] skip stale gen={} active={}".format(gen, active))
+            return
+        phase = int(getattr(self, "_vc_ms_phase", 0))
+        if phase == 2 and gen > active:
+            print("[VC][ms] coalesce interrupt gen={} active={}".format(gen, active))
+        self._vc_ms_active_gen = gen
+        self._vc_ms_phase = 1
+        try:
+            self._vc_mode_switch_prep(gen)
+        except Exception as e:
+            usys.print_exception(e)
+        try:
+            if int(getattr(self, "_vc_ms_active_gen", 0)) != gen:
+                print("[VC][ms] prep aborted gen={}".format(gen))
+                return
+        except Exception:
             return
-    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})
+            self._vc_ms_play_gen = gen
+            self._vc_ms_phase = 2
         except Exception:
             pass
+        tts_str = getattr(self, "_vc_ms_pending_tts_str", "") or ""
+        if not tts_str:
+            tts_str = "已切换到单对话模式" if getattr(self, "VC_mode", 0) == 1 else "已切换到长对话模式"
+        print("[VC][ms] play_tts gen={} mode={}".format(gen, getattr(self, "VC_mode", -1)))
+        EventMesh.publish("play_tts", [tts_str, 1])
+
+    def pwk_key_double_press(self,event,msg):#pwk双击
+        print("come 双击")
         if self.VC_mode == 0:
             self.VC_mode = 1
-            tts_str = "已切换到单对话模式" 
-            # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(单对话)
-            try:
-                self._pending_tts_kind = "mode_switch_ptt"
-                self._last_tts_kind = "mode_switch_ptt"
-            except Exception:
-                pass
+            tts_str = "已切换到单对话模式"
+            kind = "mode_switch_ptt"
         else:
-            self.VC_mode = 0 
-            tts_str = "已切换到长对话模式" 
-            # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(长对话)
-            try:
-                self._pending_tts_kind = "mode_switch_long"
-                self._last_tts_kind = "mode_switch_long"
-            except Exception:
-                pass
-        # 提示音播完即退出(Close_flag=1),保持 idle,等待按键
-        EventMesh.publish("play_tts",[tts_str,1])
-        EventMesh.publish("set_storage_config",["VC_mode",self.VC_mode])
+            self.VC_mode = 0
+            tts_str = "已切换到长对话模式"
+            kind = "mode_switch_long"
+        try:
+            self._pending_tts_kind = kind
+            self._last_tts_kind = kind
+            self._vc_ms_pending_tts_str = tts_str
+        except Exception:
+            pass
+        EventMesh.publish("set_storage_config", ["VC_mode", self.VC_mode])
+        try:
+            self._vc_mode_strategy = strategy_for_mode(self.VC_mode)
+            self._vc_mode_strategy_mode = int(self.VC_mode)
+        except Exception:
+            pass
+        try:
+            self._vc_ms_gen = int(getattr(self, "_vc_ms_gen", 0)) + 1
+        except Exception:
+            self._vc_ms_gen = 1
+        gen = self._vc_ms_gen
+        print("[VC][ms] schedule gen={} mode={}".format(gen, self.VC_mode))
+        self._vc_enqueue_btn(self._vc_mode_switch_run, gen)
     def AI_key_long_press(self, event, msg):#按键长按
         try:
             if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
@@ -2429,6 +2964,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         else:
             self.close_timerout = 0
         if self.VC_mode == 0 and idle_sec > 0 and self.close_timerout > idle_sec:
+            try:
+                if getattr(self, "VC_record_state_flag", 0) != 0:
+                    self.close_timerout = 0
+                    return
+            except Exception:
+                pass
             try:
                 print("[VC][idle_L1] 长对话无TTS反馈超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={} {}".format(
                     self.close_timerout, idle_sec, self.VC_mode,
@@ -2683,6 +3224,18 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def Close_VC(self,event,msg):
         EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) 
     def Close(self):
+        try:
+            ms_phase = int(getattr(self, "_vc_ms_phase", 0))
+            if ms_phase in (1, 2, 3) and getattr(self, "_vc_close_reason", "") != "mode_switch_play_end":
+                print("[VC][ms] Close blocked phase={} reason={}".format(
+                    ms_phase, getattr(self, "_vc_close_reason", "")))
+                return
+        except Exception:
+            pass
+        try:
+            self._vc_close_reason = ""
+        except Exception:
+            pass
         try:
             self._vc_idle_close_in_progress = 1
         except Exception:
@@ -2725,6 +3278,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         self._vc_pending_interrupt_uplink = 0
         try:
             self._vc_pending_record_start_tone = 0
+            self._vc_pending_key_press_tone = 0
             self._ptt_defer_speech_until_asr = 0
         except Exception:
             pass
@@ -2850,6 +3404,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             self._vc_idle_l1_close = 0
         except Exception:
             pass
+        try:
+            self._vc_ms_phase = 0
+            self._vc_ms_play_gen = 0
+            self._vc_ms_close_pending = 0
+        except Exception:
+            pass
     def VC_init2(self):
         self.VC_init()
         
index 47cc7fc..c53e180 100644 (file)
 # -*- coding: utf-8 -*-
 """AI_VC mixin:VC WebSocket 建链、收发线程、协议 JSON 分发。"""
 from vc_session_imports import *  # noqa: F401,F403
+from vc_notify import NotifyScheduler
 # import * 不导入以下划线开头的符号;_thread 必须显式引入(否则 start_new_thread NameError)
 import _thread
 
 class VCSessionIoMixin:
-    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")
+    _VC_STALE_DROP_EVENTS = (
+        "tts.started", "tts.completed", "llm.created", "llm.completed", "completed",
+    )
 
-    def _vc_fsm_dispatch_cancel_cb(self, data):
-        """btn 线程:长对话 canceled(与按键同队列,避免 pending 竞态)。"""
-        try:
-            ev, jsload = data
-        except Exception:
-            return
-        try:
-            if hasattr(self, "_long_dialogue_is_stale_server_cancel") and self._long_dialogue_is_stale_server_cancel():
-                self._long_dialogue_handle_stale_cancel()
-                return
-        except Exception:
-            pass
+    def _vc_server_event_drop_stale(self, ev, gen):
+        """仅丢弃迟到下行/TTS 相关事件;canceled/asr/started 不因 turn_gen 误杀。"""
         try:
-            self._vc_stop_downlink_for_interrupt("canceled")
+            if not self._vc_turn_stale(gen):
+                return False
+            if ev in self._VC_STALE_DROP_EVENTS:
+                return True
         except Exception:
             pass
+        return False
+
+    def _vc_orchestrator_server_event_cb(self, data):
+        """btn 编排线程:服务端协议事件唯一入口(长对话/PTT 统一)。"""
         try:
-            actions = self.fsm.on_server_event(ev, jsload)
-            if hasattr(self, "_long_dialogue_is_stale_server_cancel"):
-                try:
-                    if self._long_dialogue_is_stale_server_cancel():
-                        actions = [a for a in (actions or []) if a and a[0] != "uplink_start"]
-                except Exception:
-                    pass
-            self._fsm_apply_actions(actions)
+            if data is not None and len(data) >= 3:
+                ev, jsload, gen = data[0], data[1], data[2]
+            else:
+                ev, jsload = data[0], data[1]
+                gen = -1
         except Exception:
-            pass
-
-    def _vc_dispatch_server_event(self, ev, jsload_data=None):
+            return
+        if self._vc_server_event_drop_stale(ev, gen):
+            try:
+                print("[ORCH] drop server ev={} gen={} cur={}".format(
+                    ev, gen, self._vc_turn_current()))
+            except Exception:
+                pass
+            return
         if ev == "tts.started":
             try:
                 self._vc_tts_rx_counters_reset()
             except Exception:
                 pass
-        if getattr(self, "VC_mode", -1) == 0:
-            if ev == "tts.started":
+            if self._vc_is_long_dialogue_mode():
                 try:
                     if hasattr(self, "_long_dialogue_on_server_tts_started"):
                         self._long_dialogue_on_server_tts_started()
                 except Exception:
                     pass
-            if ev == "canceled":
-                self._vc_enqueue_btn(self._vc_fsm_dispatch_cancel_cb, (ev, jsload_data))
-                return
-        try:
-            if ev == "canceled":
-                self._vc_stop_downlink_for_interrupt("canceled")
-        except Exception:
-            pass
-        if getattr(self, "VC_mode", -1) == 1:
-            self._vc_enqueue_btn(self._vc_fsm_dispatch_cb, (ev, jsload_data))
-        else:
+        if ev == "canceled":
+            try:
+                if (
+                    self._vc_is_long_dialogue_mode()
+                    and hasattr(self, "_long_dialogue_is_stale_server_cancel")
+                    and self._long_dialogue_is_stale_server_cancel()
+                ):
+                    self._long_dialogue_handle_stale_cancel()
+                    return
+            except Exception:
+                pass
             try:
-                self._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))
+                self._vc_stop_downlink_for_interrupt("canceled")
             except Exception:
                 pass
+        try:
+            actions = self.fsm.on_server_event(ev, jsload)
+            if ev == "canceled" and self._vc_is_long_dialogue_mode():
+                try:
+                    if self._long_dialogue_is_stale_server_cancel():
+                        actions = [
+                            a for a in (actions or [])
+                            if a and a[0] != "uplink_start"
+                        ]
+                except Exception:
+                    pass
+            self._fsm_apply_actions(actions)
+            if ev == "canceled":
+                try:
+                    if (
+                        self._vc_is_long_dialogue_mode()
+                        and hasattr(self, "_long_dialogue_end_interrupt")
+                        and not (
+                            hasattr(self, "_long_dialogue_is_stale_server_cancel")
+                            and self._long_dialogue_is_stale_server_cancel()
+                        )
+                    ):
+                        self._long_dialogue_end_interrupt()
+                except Exception:
+                    pass
+        except Exception as e:
+            usys.print_exception(e)
+        if ev == "canceled" and self._vc_is_ptt_mode():
+            self._vc_on_ptt_cancel_settled(via="canceled")
+
+    def _vc_dispatch_server_event(self, ev, jsload_data=None):
+        """recv 线程:仅入编排队列,不在此修改 FSM。"""
+        gen = self._vc_turn_current()
+        self._vc_enqueue_btn(
+            self._vc_orchestrator_server_event_cb,
+            (ev, jsload_data, gen),
+            track_gen=True,
+        )
     def request_send_start(self):
         EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_start_requset})
     def request_send_audio_create(self):
@@ -230,7 +257,7 @@ class VCSessionIoMixin:
         except Exception:
             pass
         EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"websocket_error"})
-        EventMesh.publish("set_led_state", {"ai_state": "idle"})
+        EventMesh.publish("vc_led_sync")
         if TTS_str == None:
             self.websocket_error_count+=1
             # if self.websocket_error_count <=3:
@@ -581,123 +608,23 @@ class VCSessionIoMixin:
                             self._turn_has_vad_silence = 0
                             self._turn_has_tts_started = 0
                             self._llm_completed_ms = 0
-                            if self.VC_mode == 0 and getattr(self, "Close_flag", 0) == 0:
-                                _skip_uplink = 0
+                            _skip_long_up = 0
+                            if getattr(self, "_skip_started_uplink_once", 0) == 1:
                                 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")
+                                    self._skip_started_uplink_once = 0
+                                    _skip_long_up = 1
+                                    print("[VC][uplink_ctrl] Deny START by server.started cause=boot_prompt")
                                 except Exception:
                                     pass
-                                if not _skip_uplink:
-                                    # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后
-                                    # 出现首轮无音频上行导致“第一次对话没有”的情况。
-                                    try:
-                                        if getattr(self, "_long_dialogue_key_arm", 0) == 1:
-                                            self._long_dialogue_publish_listening("server.started")
-                                    except Exception:
-                                        pass
-                                    try:
-                                        self._vc_on_server_record_uplink_ready("server.started")
-                                    except Exception:
-                                        pass
-                                    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))
-                            if self.AI_new_voice_url != "" and self.alarm_play_flag == 1 :#有闹钟,先响闹钟
-                                self.AI_VC_start()
-                                EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放铃声
-                                self.alarm_play_flag = 0
-                                self.voice_choose_flag = 1
-                            elif self.AI_TTS_str == "" and  self.AI_new_voice_url != "" and self.music_play_flag == 0:
-                                # if EventMesh.publish("get_history_voice_flag") == 1:
-                                EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
-                                # self.recv_voice = 0#当留言列表为空,留言播放标志清空
-                                # else:
-                                #     self.voice_cnt = 0
-                                #     print("定时器开启222222222")
-                                #     self.voice_timer.start(1000,1,self.voice_judge)
-                            elif  self.AI_TTS_str != "" and  self.AI_new_voice_url == "":
-                                EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
-                            
-                            elif self.AI_TTS_str != "" and  self.AI_new_voice_url != "" and self.music_play_flag == 0:#遇到tts和语音留言冲突时(并且没有音乐播放)
-                                if EventMesh.publish("get_history_voice_flag") == 1:#播放历史留言
-                                    if EventMesh.publish("get_wait_voice_flag") == 0:
-                                        EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
-                                        self.his_cnt = 0
-                                        self.his_timer.start(1000,1,self.history_play) 
-                                        self.voice_choose_flag = 1
-                                    else:#等待留言
-                                        if self.pwk_notice_flag == 0:
-                                            self.wait_cnt = 0
-                                            print("未听播放1")
-                                            EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
-                                            self.wait_timer.start(1000,1,self.wait_play) 
-                                            self.voice_choose_flag = 1
-                            
-                                        else:#已确定播放
-                                            print("未听播放2")
-                                            self.wait_cnt = 0
-                                            EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
-                                            self.wait_timer.start(1000,1,self.wait_single_play) 
-                                            self.voice_choose_flag = 1
-                                else:#播放正常留言
-                                    EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
-                                    self.voice_cnt = 0
-                                    print("定时器开启111111111")
-                                    self.voice_timer.start(1000,1,self.voice_judge) 
-                                    self.voice_choose_flag = 1
-                                # self.AI_TTS_str = ""    
-                            elif self.AI_TTS_str != "" and  self.AI_new_voice_url != "" and self.music_play_flag == 1 :#遇到tts和语音留言冲突时(并且有音乐播放)
-                                self.music_play_flag = 0
-                                EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
-                                self.music_timer.start(1000,1,self.music_play) 
-                                self.voice_choose_flag = 1
-                            else:
-                                #开始上传语音
-                                try:
-                                    if "vc_config" in globals() and vc_config and vc_config.get_bool("UPLOAD_START_TONE_ENABLED", False):
-                                        self.chenggong_tone()
-                                except Exception:
-                                    pass
-                                if self.VC_mode == 0:
-                                    self.request_uplink_start("server.started/idle")
-                                else:
-                                    # Push-To-Talk:started 后先滴(可上传)→ audio.create → 再开麦
-                                    _ptt_rec = False
-                                    try:
-                                        _ptt_rec = getattr(self, "_ptt_force_record_on_started", 0) == 1
-                                        if _ptt_rec:
-                                            self._ptt_force_record_on_started = 0
-                                    except Exception:
-                                        pass
-                                    try:
-                                        if _ptt_rec:
-                                            self._vc_on_server_record_uplink_ready(
-                                                "ptt_begin/started"
-                                            )
-                                    except Exception:
-                                        pass
-                                    try:
-                                        self.AI_client_VC_send_speech()
-                                    except Exception:
-                                        self.request_send_audio_create()
-                                    try:
-                                        if _ptt_rec:
-                                            self.VC_Speech_Start_now()
-                                    except Exception:
-                                        pass
-                                
-                            
-                            # if self.AI_new_voice_url != "":
-                            #     EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})
+                            print("TTS音频为:{},语音下发地址为:{}".format(
+                                self.AI_TTS_str, self.AI_new_voice_url))
+                            try:
+                                _plan = NotifyScheduler.plan_on_started(
+                                    self, skip_long_uplink=_skip_long_up)
+                                self._vc_apply_notify_started_plan(_plan)
+                            except Exception as e:
+                                usys.print_exception(e)
 
                         elif jsload_data["event"] == "config.update" :#配置更新中,需要停止录音
                             try:
@@ -720,6 +647,8 @@ class VCSessionIoMixin:
                             self._turn_has_vad_silence = 0
                             self._turn_has_tts_started = 0
                             self._llm_completed_ms = 0
+                            self._vc_defer_vad_uplink_stop = 0
+                            self._vc_vad_had_active_after_silence = 0
                             try:
                                 self._asr_completed_tone_done = 0
                             except Exception:
@@ -751,6 +680,16 @@ class VCSessionIoMixin:
                                 self._vc_led_alert_rb_flash()
                         elif jsload_data["event"] == "asr.completed" :#asr 终止的标识 
                             self._turn_has_asr_completed = 1
+                            try:
+                                if hasattr(self, "_vc_flush_deferred_vad_uplink_stop"):
+                                    self._vc_flush_deferred_vad_uplink_stop("asr.completed/vad")
+                            except Exception:
+                                pass
+                            try:
+                                if self._vc_effective_asr_text() and hasattr(self, "_vc_reset_long_idle_timer"):
+                                    self._vc_reset_long_idle_timer("asr.completed")
+                            except Exception:
+                                pass
                             try:
                                 if not (self.AI_ASR_str or "").strip():
                                     self.AI_ASR_str = getattr(self, "_last_asr_result_text", "") or ""
@@ -765,16 +704,7 @@ class VCSessionIoMixin:
                                         print("[VC][ASR] completed empty, skip think tone")
                                     except Exception:
                                         pass
-                                    try:
-                                        if (
-                                            getattr(self, "VC_mode", 0) == 0
-                                            and getattr(self, "Close_flag", 0) == 0
-                                            and getattr(self, "_vc_session_started", 0) == 1
-                                        ):
-                                            self._server_vad_silence = 0
-                                            self.request_uplink_start("asr.empty/reopen")
-                                    except Exception:
-                                        pass
+                                    # 空 ASR 不自动 reopen:下一轮由按键或 completed/打断 统一开麦,避免环境声 VAD 循环
                                 else:
                                     stream_on = False
                                     try:
@@ -787,9 +717,21 @@ class VCSessionIoMixin:
                                         thinking = getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking
                                     except Exception:
                                         thinking = False
-                                    if (not stream_on) and (not pcm_busy) and (not playing) and (not thinking):
+                                    vad_cont = getattr(self, "_vc_vad_had_active_after_silence", 0) == 1
+                                    if (
+                                        (not stream_on)
+                                        and (not pcm_busy)
+                                        and (not playing)
+                                        and (not thinking)
+                                        and (not vad_cont)
+                                    ):
                                         self._vc_local_play('U:/static/think.mp3', 1, 1, 650)
                                         self._asr_completed_tone_done = 1
+                                    elif vad_cont:
+                                        try:
+                                            print("[VC][ASR] skip think tone (vad pause merged)")
+                                        except Exception:
+                                            pass
                             except Exception:
                                 pass
                             print("self.commands_mode:{}".format(self.commands_mode))
@@ -802,7 +744,7 @@ class VCSessionIoMixin:
                                     if self.AI_ASR_send_data_queue.empty()!=True:
                                         self.AI_ASR_send_data_queue.clear()
                                     self.AI_ASR_send_buf_len = 0
-                                    EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+                                    EventMesh.publish("vc_led_sync")
                                     try:
                                         
                                         self.request_uplink_stop("asr.completed/config_cmd")
@@ -824,21 +766,42 @@ class VCSessionIoMixin:
                             # if dev_type != "7PIN" :
                             #EventMesh.publish("publish_AI_btnInfo",{"func":self.send_tone}) 
                         elif jsload_data["event"] == "vad.active" :#VAD 检测到说话→聆听
+                            try:
+                                self._vc_vad_had_active_after_silence = 1
+                                self._vc_defer_vad_uplink_stop = 0
+                                self._server_vad_silence = 0
+                            except Exception:
+                                pass
                             try:
                                 if "vc_config" in globals() and vc_config and vc_config.get_bool("VAD_ACTIVE_BLUE_LED", True):
                                     self._server_vad_silence = 0
                             except Exception:
                                 pass
                         elif jsload_data["event"] == "vad.silence" :#VAD 终止的标识 
-                            # 说明服务端已判定“本轮说话结束”;此时继续上行只会干扰后续(例如推迟 TTS)。
-                            # 仅在“正在录音上行”的语音阶段才停止,避免误触发。
-                            self._server_vad_silence = 1
+                            # 长对话:句中停顿仅标记,等 asr.completed 再停麦(配合服务端 vad_silence 加长)
                             self._turn_has_vad_silence = 1
-                            self._uplink_mark_end("vad.silence")
-                            if self.record_data_save_flag == 0:
-                                if self.VC_record_state_flag != 0 and self._vc_vad_uplink_stop_eligible():
-                                    self.request_uplink_stop("vad.silence")
+                            try:
+                                if hasattr(self, "_vc_long_dialogue_defer_vad_stop") and self._vc_long_dialogue_defer_vad_stop():
+                                    self._vc_defer_vad_uplink_stop = 1
+                                    try:
+                                        print("[VC][VAD] silence defer uplink_stop (long_dialogue)")
+                                    except Exception:
+                                        pass
+                                else:
+                                    self._server_vad_silence = 1
+                                    self._uplink_mark_end("vad.silence")
+                                    if self.record_data_save_flag == 0:
+                                        if self.VC_record_state_flag != 0 and self._vc_vad_uplink_stop_eligible():
+                                            self.request_uplink_stop("vad.silence")
+                            except Exception:
+                                self._server_vad_silence = 1
+                                self._uplink_mark_end("vad.silence")
                         elif jsload_data["event"] == "llm.created" :#思考(LLM)
+                            try:
+                                if hasattr(self, "_vc_reset_long_idle_timer"):
+                                    self._vc_reset_long_idle_timer("llm.created")
+                            except Exception:
+                                pass
                             try:
                                 self._ptt_await_server = 0
                             except Exception:
@@ -847,7 +810,7 @@ class VCSessionIoMixin:
                                 if not self._vc_tts_playback_active() and not getattr(
                                     self.fsm, "await_played", False
                                 ):
-                                    EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+                                    EventMesh.publish("vc_led_sync")
                             except Exception:
                                 pass
                             self._llm_completed_ms = 0
@@ -960,7 +923,8 @@ class VCSessionIoMixin:
                             pass
 
                         elif jsload_data["event"] == "canceled" :#打断
-                            # 停播/开麦在 btn 队列 _vc_fsm_dispatch_cancel_cb;is_ruppt 在长对话打断收尾时清
+                            # 长对话清 is_ruppt:仅在 btn 编排 _vc_orchestrator_server_event_cb 中处理,
+                            # 勿在 recv 线程提前 settle(会与 stop_downlink 竞态,偶发放行旧 TTS 尾包)
                             if getattr(self, "VC_mode", 0) != 0:
                                 self.is_ruppt_tts = 0
                             self._need_audio_played_ack = 0
@@ -1143,7 +1107,18 @@ class VCSessionIoMixin:
                         # print("下发音频数据2:{}".format(len(recv_data)))
                         #if self.VC_state >= VC_State_TTSStart:
                             #self.put_record_data([recv_data,""])
-                        if self.is_ruppt_tts == 0:#被设备主动打断打断则不播放,知道收到服务器返回的cancel才修改标志位
+                        _drop_pcm = False
+                        try:
+                            if (
+                                hasattr(self, "_long_dialogue_should_drop_inbound_tts")
+                                and self._vc_is_long_dialogue_mode()
+                            ):
+                                _drop_pcm = self._long_dialogue_should_drop_inbound_tts()
+                            elif getattr(self, "is_ruppt_tts", 0) == 1:
+                                _drop_pcm = True
+                        except Exception:
+                            _drop_pcm = getattr(self, "is_ruppt_tts", 0) == 1
+                        if not _drop_pcm:
                             if self.VC_play_TYPE == VC_play_TYPE_PCM:
                                 if self.play_pcm != None:
                                     try:
@@ -1161,7 +1136,7 @@ class VCSessionIoMixin:
                                         usys.print_exception(e)
                             EventMesh.publish("publish_AI_btnInfo",{"func":self.put_record_data,"data":[recv_data,""]}) 
                         else:
-                            print("被打断不播放") 
+                            print("被打断不播放")
                         utime.sleep_ms(1)
                 except Exception as e:
                     usys.print_exception(e)
@@ -1803,10 +1778,11 @@ class VCSessionIoMixin:
         self.AI_client_VC_send(ujson.dumps(json_data))
         try:
             _tid = json_data.get("header", {}).get("trace_id", "")
-            print("[VC][proto] Start enqueued trace_id={} VC_mode={} disable_vad={} ws_conn={}".format(
+            print("[VC][proto] Start enqueued trace_id={} VC_mode={} disable_vad={} vad_silence={}ms ws_conn={}".format(
                 _tid or "-",
                 self.VC_mode,
                 json_data.get("payload", {}).get("inbound_attributes", {}).get("disable_vad", "?"),
+                getattr(self, "vad_silence", "?"),
                 self.AI_client_VC_isconnect))
         except Exception:
             pass
index 440f3f3..b60d3b8 100644 (file)
@@ -125,15 +125,17 @@ class VCSessionMediaMixin:
             return False
 
     def _vc_play_record_start_tone(self, via=""):
-        """开录提示音 start.mp3;配置为 None 时回退 U:/static/start.mp3。"""
+        """开录提示音;RECORD_START_TONE_FILE=None 时不播(REQ-AI-07)。"""
         try:
-            path = "U:/static/start.mp3"
+            path = None
             hold_ms = 180
             if "vc_config" in globals() and vc_config:
                 cfg = vc_config.get_attr("RECORD_START_TONE_FILE", None)
                 if cfg is not None and str(cfg).strip():
                     path = str(cfg).strip()
                 hold_ms = int(vc_config.get_int("RECORD_START_TONE_GAP_MS", hold_ms))
+            if not path:
+                return False
             print("[VC] record_start_tone ready via={} path={}".format(via or "-", path))
             self._vc_local_play(path, hold_ms=hold_ms)
             return True
@@ -141,6 +143,44 @@ class VCSessionMediaMixin:
             usys.print_exception(e)
             return False
 
+    def _vc_play_key_press_tone(self, via=""):
+        """长对话短按操作确认音(REQ-AI-01);KEY_PRESS_TONE_FILE=None 时不播。"""
+        try:
+            path = None
+            hold_ms = 300
+            if "vc_config" in globals() and vc_config:
+                cfg = vc_config.get_attr("KEY_PRESS_TONE_FILE", None)
+                if cfg is not None and str(cfg).strip():
+                    path = str(cfg).strip()
+                hold_ms = int(vc_config.get_int("KEY_PRESS_TONE_DELAY_MS", hold_ms))
+            if not path:
+                return False
+            print("[VC] key_press_tone via={} path={}".format(via or "-", path))
+            self._vc_local_play(path, hold_ms=hold_ms)
+            return True
+        except Exception as e:
+            usys.print_exception(e)
+            return False
+
+    def _vc_arm_key_press_tone(self):
+        try:
+            self._vc_pending_key_press_tone = 1
+        except Exception:
+            pass
+
+    def _vc_try_play_key_press_tone_when_ready(self, via=""):
+        try:
+            if not getattr(self, "_vc_pending_key_press_tone", 0):
+                return False
+            if getattr(self, "Close_flag", 0) == 1:
+                return False
+            if not self._vc_play_key_press_tone(via=via):
+                return False
+            self._vc_pending_key_press_tone = 0
+            return True
+        except Exception:
+            return False
+
     _vc_play_ai_key_record_start_tone = _vc_play_record_start_tone
 
     def _vc_uplink_reason_is_user_key(self, reason):
@@ -155,76 +195,253 @@ class VCSessionMediaMixin:
         return False
 
     def _vc_on_server_record_uplink_ready(self, via):
-        """WS started / asr.started:服务端允许上行后再滴;PTT 可延后开麦。"""
-        try:
-            self._vc_try_play_record_start_tone_when_ready(via)
-        except Exception:
-            pass
+        """WS started / asr.started:用户开录走 _vc_user_record_entry,此处仅保留兼容。"""
         if str(via or "") != "asr.started":
             return
         try:
             if getattr(self, "_ptt_defer_speech_until_asr", 0) == 1:
                 self._ptt_defer_speech_until_asr = 0
-                self.VC_Speech_Start_now()
-                print("[PTT] defer_speech -> VC_Speech_Start (asr.started)")
+                self._vc_user_record_entry("asr.started/defer")
         except Exception:
             pass
 
-    def _vc_uplink_start_recording(self, reason=""):
-        """开麦统一出口(按键/打断续聊):会话已 started 时先滴再 VC_Speech_Start。"""
+    def _vc_user_record_tone_timer_cb(self, *args, **kwargs):
+        EventMesh.publish(
+            "publish_AI_btnInfo",
+            {"func": self._vc_user_record_uplink_open_cb},
+        )
+
+    def _vc_user_record_uplink_open_cb(self, event=None, msg=None):
+        """滴声结束后:允许上行 + 点亮聆听灯。"""
         try:
-            if (
-                getattr(self, "_vc_session_started", 0) == 1
-                and self._vc_uplink_reason_is_user_key(reason)
-            ):
-                self._vc_try_play_record_start_tone_when_ready("uplink_ready")
+            reason = getattr(self, "_vc_user_record_open_reason", "") or "tone_done"
+            self._vc_user_record_uplink_open(reason)
+        except Exception as e:
+            usys.print_exception(e)
+
+    def _vc_user_record_uplink_open(self, reason=""):
+        try:
+            self._vc_defer_listening_led = 0
+            self._vc_skip_local_play_stop_record = 0
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_record_state_flag", 0) == 1:
+                self.VC_Speech_Start_now()
+            elif getattr(self, "VC_record_state_flag", 0) == 0:
+                self.VC_Speech_Start()
+                if getattr(self, "VC_record_state_flag", 0) == 1:
+                    self.VC_Speech_Start_now()
+        except Exception as e:
+            usys.print_exception(e)
+        try:
+            self._vc_publish_listening_for_mode(
+                "user_record/uplink_open:" + str(reason or ""))
         except Exception:
             pass
         try:
             if getattr(self, "VC_mode", 0) == 0:
                 self._long_dialogue_key_arm = 0
+                if hasattr(self, "_long_dialogue_end_interrupt"):
+                    self._long_dialogue_end_interrupt()
+            elif getattr(self, "VC_mode", -1) == 1:
+                if hasattr(self, "_ptt_on_record_hardware_started"):
+                    self._ptt_on_record_hardware_started()
+                if hasattr(self, "_ptt_end_led_suppress"):
+                    self._ptt_end_led_suppress()
         except Exception:
             pass
-        self.VC_Speech_Start_now()
         try:
-            if getattr(self, "VC_mode", 0) == 0 and hasattr(self, "_long_dialogue_end_interrupt"):
-                self._long_dialogue_end_interrupt()
+            print("[VC][user_record] uplink_open reason={} rec={}".format(
+                reason, getattr(self, "VC_record_state_flag", -1)))
         except Exception:
             pass
 
-    def _vc_fire_local_play_end(self, tag=""):
-        """TTS 本地播完:VC_Play_End + FSM play_end + 模式切换 Close。"""
+    def _vc_user_record_entry(self, reason=""):
+        """用户开录统一入口:长对话与 PTT 分流(提示音均为 start.mp3)。"""
+        if getattr(self, "VC_mode", -1) == 1:
+            self._vc_user_record_entry_ptt(reason)
+        else:
+            self._vc_user_record_entry_long(reason)
+
+    def _vc_user_record_entry_long(self, reason=""):
+        """
+        长对话打断/续聊:
+        1) VC_Speech_Start 仅缓冲(record_state=1 不上传)
+        2) 播 start.mp3
+        3) 提示音结束后再 VC_Speech_Start_now + 蓝慢闪
+        """
+        try:
+            self._vc_user_record_open_reason = str(reason or "")
+            self._vc_defer_listening_led = 1
+            self._vc_skip_local_play_stop_record = 1
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_record_state_flag", 0) == 0:
+                self.VC_Speech_Start()
+            elif getattr(self, "VC_record_state_flag", 0) > 1:
+                pass
+        except Exception as e:
+            usys.print_exception(e)
+        hold_ms = 180
+        try:
+            if "vc_config" in globals() and vc_config:
+                hold_ms = int(vc_config.get_int("RECORD_START_TONE_GAP_MS", hold_ms))
+        except Exception:
+            pass
+        played = False
+        try:
+            played = self._vc_play_record_start_tone(via=reason)
+        except Exception:
+            played = False
+        if not played:
+            try:
+                self._vc_user_record_uplink_open(reason)
+            except Exception:
+                pass
+            return
+        try:
+            total_ms = int(hold_ms) + 40
+            if total_ms < 200:
+                total_ms = 200
+            self._vc_user_record_tone_timer.stop()
+            self._vc_user_record_tone_timer.start(total_ms, 0, self._vc_user_record_tone_timer_cb)
+            print("[VC][user_record] entry_long reason={} tone_ms={}".format(reason, total_ms))
+        except Exception as e:
+            usys.print_exception(e)
+            try:
+                self._vc_user_record_uplink_open(reason)
+            except Exception:
+                pass
+
+    def _vc_user_record_entry_ptt(self, reason=""):
+        """
+        单对话 PTT 长按:立即录音+上行;并行播 start.mp3;松手 end_speech 停传。
+        """
+        try:
+            self._vc_user_record_open_reason = str(reason or "")
+            self._vc_defer_listening_led = 0
+            self._vc_skip_local_play_stop_record = 1
+        except Exception:
+            pass
+        try:
+            self._vc_user_record_tone_timer.stop()
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_record_state_flag", 0) == 0:
+                self.VC_Speech_Start()
+            if getattr(self, "VC_record_state_flag", 0) == 1:
+                self.VC_Speech_Start_now()
+            elif getattr(self, "VC_record_state_flag", 0) == 0:
+                self.VC_Speech_Start()
+                if getattr(self, "VC_record_state_flag", 0) == 1:
+                    self.VC_Speech_Start_now()
+        except Exception as e:
+            usys.print_exception(e)
+        try:
+            if hasattr(self, "_ptt_on_record_hardware_started"):
+                self._ptt_on_record_hardware_started()
+            if hasattr(self, "_ptt_end_led_suppress"):
+                self._ptt_end_led_suppress()
+            self._vc_publish_listening_for_mode(
+                "user_record/ptt:" + str(reason or ""))
+        except Exception:
+            pass
+        try:
+            self._vc_play_record_start_tone(via=reason)
+        except Exception:
+            pass
+        try:
+            self._vc_skip_local_play_stop_record = 0
+            print("[VC][user_record] entry_ptt reason={} rec={}".format(
+                reason, getattr(self, "VC_record_state_flag", -1)))
+        except Exception:
+            pass
+
+    def _vc_uplink_start_recording(self, reason=""):
+        """开麦统一出口:长对话打断/续聊、按键开麦均走 user_record_entry。"""
+        try:
+            if hasattr(self, "_vc_reset_long_idle_timer"):
+                self._vc_reset_long_idle_timer(reason)
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_mode", 0) == 0:
+                self._long_dialogue_key_arm = 0
+        except Exception:
+            pass
+        self._vc_user_record_entry(reason)
+
+    def _vc_orchestrator_play_end_cb(self, tag=""):
+        """btn 编排线程:TTS 播完统一收尾。"""
         try:
             if tag:
                 print("[TTS] play_end_{}".format(tag))
         except Exception:
             pass
-        EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_End})
-        self._fsm_apply_actions(self.fsm.on_local_play_end())
+        actions = []
+        try:
+            actions = self.fsm.on_local_play_end() or []
+        except Exception:
+            actions = []
+        pre = []
+        post = []
+        try:
+            for a in actions:
+                if not a:
+                    continue
+                if a[0] == "uplink_start":
+                    post.append(a)
+                else:
+                    pre.append(a)
+        except Exception:
+            pre = actions
+            post = []
+        try:
+            self._fsm_apply_actions(pre)
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_mode", -1) == 1:
+                self._ptt_await_server = 0
+        except Exception:
+            pass
+        try:
+            self.VC_Play_End()
+        except Exception:
+            pass
         try:
             self.fsm.reset_rx_phases()
         except Exception:
             pass
         self._tts_first_write_ms = 0
-        self._vc_finish_play_end_close_if_needed()
-
-    def _vc_local_play(self, path, play_mode=1, loop=1, hold_ms=650):
-        """本地 MP3 短音:播前停录/停流式 TTS,避免与麦克风采集叠用。"""
         try:
-            if getattr(self, "record_data_save_flag", 0) == 0 and getattr(self, "VC_record_state_flag", 0) != 0:
-                self._uplink_request_stop("local_play")
-                utime.sleep_ms(30)
+            self._fsm_apply_actions(post)
         except Exception:
             pass
+        self._vc_finish_play_end_close_if_needed()
+
+    def _vc_fire_local_play_end(self, tag=""):
+        """TTS 本地播完:入编排队列,避免在播放线程直接改 FSM。"""
+        self._vc_enqueue_btn(self._vc_orchestrator_play_end_cb, tag)
+
+    def _vc_local_play(self, path, play_mode=1, loop=1, hold_ms=650):
+        """本地 MP3 短音(think/start 等):与上行录音并行,不停录;仅打断服务端 TTS 流。"""
         try:
-            if self._vc_tts_playback_active():
-                self._ptt_hard_mute_local_playback()
-                try:
-                    self.VC_Play_End()
-                except Exception:
-                    pass
+            from vc_media_arb import MediaArbiter
+            MediaArbiter.before_local(self)
         except Exception:
-            pass
+            try:
+                if self._vc_tts_playback_active():
+                    self._ptt_hard_mute_local_playback()
+                    try:
+                        self.VC_Play_End()
+                    except Exception:
+                        pass
+            except Exception:
+                pass
         try:
             hold = int(hold_ms)
             if hold < 200:
@@ -289,10 +506,14 @@ class VCSessionMediaMixin:
                 return True
             if getattr(self, "Close_flag", 0) == 1:
                 return False
-            user_intr = reason == "canceled" and (
-                getattr(self, "_vc_pending_interrupt_uplink", 0) == 1
-                or getattr(self, "is_ruppt_tts", 0) == 1
+            # canceled 开麦:仅用户打断(InterruptContext.pending_uplink),FSM 在 settle 前执行
+            user_intr = (
+                reason == "canceled"
+                and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1
             )
+            if reason == "canceled" and not user_intr:
+                print("[VC][uplink_ctrl] Deny START by canceled cause=not_user_interrupt")
+                return False
             if (
                 reason == "canceled"
                 and getattr(self, "VC_record_state_flag", 0) != 0
@@ -306,13 +527,14 @@ class VCSessionMediaMixin:
                     pass
                 elif (
                     self._vc_is_long_dialogue_mode()
-                    and reason in ("completed", "canceled")
+                    and reason == "completed"
+                    and self._vc_effective_asr_text()
                     and (
                         getattr(self, "_turn_has_vad_silence", 0) == 1
                         or getattr(self, "_turn_has_asr_completed", 0) == 1
                     )
                 ):
-                    # 长对话:vad 已结束一轮但 ASR 空,仍回到蓝慢闪聆听(勿停住)
+                    # 长对话:有有效 ASR 的 completed 才自动下一轮(空 ASR 不抢开麦)
                     pass
                 else:
                     print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason))
@@ -322,6 +544,18 @@ class VCSessionMediaMixin:
                     self.fsm, "turn", None
                 ) == VCTurnState.Thinking:
                     print("[VC][uplink_ctrl] Deny START by completed cause=llm_without_tts")
+                    try:
+                        if self._vc_is_long_dialogue_mode() and getattr(self, "_vc_session_started", 0) == 1:
+                            self._long_dialogue_publish_listening("completed/llm_without_tts")
+                        else:
+                            self._vc_publish_led("idle", "completed/llm_without_tts", prefer_off=1)
+                    except Exception:
+                        pass
+                    try:
+                        if hasattr(self, "_vc_reset_long_idle_timer"):
+                            self._vc_reset_long_idle_timer("llm_without_tts")
+                    except Exception:
+                        pass
                     return False
             debounce_ms = 450 if user_intr else 1200
             last_attr = "_vc_last_interrupt_uplink_ms" if user_intr else "_vc_last_auto_uplink_ms"
@@ -405,23 +639,14 @@ class VCSessionMediaMixin:
                     pass
                 return
             turn = getattr(self.fsm, "turn", VCTurnState.Idle)
-            # 仅 Listening/SpeechEnded 允许开麦进入下一轮;其它阶段一律拒绝(避免抢跑打乱链路)
+            # 仅 Listening/SpeechEnded 允许开麦;其它阶段拒绝(I0:禁止在门控里篡改 turn)
             if turn not in (VCTurnState.Listening, VCTurnState.SpeechEnded):
                 try:
-                    coerce = reason in _key_reasons or (
-                        reason == "canceled"
-                        and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1
-                    )
-                    if getattr(self, "VC_mode", 0) == 0 and coerce:
-                        self.fsm.turn = VCTurnState.Listening
-                        turn = VCTurnState.Listening
-                        print("[VC][uplink_ctrl] coerce turn->Listening for {}".format(reason))
-                    else:
-                        if reason in _key_reasons or reason in ("canceled", "local.play_end", "audio.played"):
-                            print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn))
-                        return
+                    if reason in _key_reasons or reason in ("canceled", "local.play_end", "audio.played"):
+                        print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn))
                 except Exception:
-                    return
+                    pass
+                return
         except Exception:
             return
 
@@ -619,6 +844,11 @@ class VCSessionMediaMixin:
         self.VC_Speech_Start()
         if self.VC_record_state_flag  == 1:
             self.VC_record_state_flag =2
+        try:
+            if int(getattr(self, "VC_mode", 0)) == 1:
+                self._ptt_record_open_ms = utime.ticks_ms()
+        except Exception:
+            pass
         print("[VC] Speech_Start_now record_state={}".format(self.VC_record_state_flag))
     def VC_Speech_Start(self):
         try:
@@ -700,13 +930,17 @@ class VCSessionMediaMixin:
                               self.VC_record_thread_id))
                 except Exception:
                     pass
-                # 录音开始:聆听灯(PTT 蓝快闪 / 长对话蓝慢闪)
+                # 聆听灯与上行一并开启(见 _vc_user_record_uplink_open)
                 try:
-                    self._vc_publish_listening_for_mode("VC_Speech_Start")
+                    if getattr(self, "_vc_defer_listening_led", 0) != 1:
+                        self._vc_publish_listening_for_mode("VC_Speech_Start")
                 except Exception:
                     pass
                 try:
-                    if getattr(self, "VC_mode", -1) == 1:
+                    if (
+                        getattr(self, "VC_mode", -1) == 1
+                        and getattr(self, "_vc_defer_listening_led", 0) != 1
+                    ):
                         self._ptt_on_record_hardware_started()
                 except Exception:
                     pass
@@ -1116,22 +1350,48 @@ class VCSessionMediaMixin:
     def play_tts(self,event = None,msg= None):
         EventMesh.publish("publish_AI_btnInfo",{"func":self.play_tts_cb,"data":msg}) 
     def play_tts_cb(self,data):
+        try:
+            from vc_media_arb import MediaArbiter
+            MediaArbiter.before_notify(self)
+        except Exception:
+            pass
         try:
             tts_str = data[0]
             is_close_VC = data[1]
+            try:
+                if len(data) > 2 and data[2]:
+                    self._pending_tts_kind = str(data[2])
+                    self._last_tts_kind = self._pending_tts_kind
+            except Exception:
+                pass
+            try:
+                from vc_notify import NotifyScheduler, CHANNEL_DEFER
+                _ch = NotifyScheduler.classify_play_tts(self, is_close_VC)
+                if _ch == CHANNEL_DEFER:
+                    self._vc_notify_queue_push(tts_str, is_close_VC)
+                    return
+            except Exception:
+                pass
             if tts_str != "":
                 self.AI_TTS_str = tts_str
             if is_close_VC != None:
                 self.Close_flag = is_close_VC
+            try:
+                k = getattr(self, "_pending_tts_kind", "") or ""
+                if k in ("mode_switch_long", "mode_switch_ptt"):
+                    self._vc_ms_phase = 2
+                    self._vc_ms_play_gen = int(getattr(self, "_vc_ms_active_gen", 0))
+            except Exception:
+                pass
             if self.AI_VC_send_data_queue.empty()!=True:
                 self.AI_VC_send_data_queue.clear()
             if self.AI_ASR_send_data_queue.empty()!=True:
                 self.AI_ASR_send_data_queue.clear()
             try:
                 if is_close_VC:
-                    EventMesh.publish("set_led_state", {"ai_state": "idle"})
+                    self._vc_publish_led("idle", "play_tts_cb")
                 else:
-                    EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+                    self._vc_publish_led("thinking", "play_tts_cb")
             except Exception:
                 pass
             self.AI_ASR_send_buf_len = 0
@@ -1146,6 +1406,22 @@ class VCSessionMediaMixin:
             usys.print_exception(e)
         pass
     def VC_Play_Start(self):
+        try:
+            from vc_media_arb import MediaArbiter
+            MediaArbiter.before_llm_pcm(self)
+        except Exception:
+            pass
+        try:
+            if int(getattr(self, "_vc_ms_phase", 0)) == 2:
+                play_gen = int(getattr(self, "_vc_ms_play_gen", -1))
+                active_gen = int(getattr(self, "_vc_ms_active_gen", 0))
+                if play_gen != active_gen:
+                    if self._vc_led_state_debug_enabled():
+                        print("[VC][ms] VC_Play_Start skip stale gen={} active={}".format(
+                            play_gen, active_gen))
+                    return
+        except Exception:
+            pass
         try:
             if self._ptt_should_skip_remote_playback():
                 if self._vc_led_state_debug_enabled():
@@ -1280,10 +1556,21 @@ class VCSessionMediaMixin:
                     self._vc_publish_listening_for_mode("VC_Play_End/recording")
                 else:
                     self._ptt_publish_listening_fast("VC_Play_End/recording")
+            elif (
+                getattr(self, "VC_mode", -1) == 1
+                and getattr(self, "VC_record_state_flag", 0) == 0
+                and not getattr(self, "_ptt_await_server", 0)
+                and not self._vc_tts_playback_active()
+            ):
+                self._vc_publish_led("idle", "VC_Play_End/ptt_round", prefer_off=1)
             else:
                 self._vc_led_sync_from_fsm()
         except Exception:
             pass
+        try:
+            self._vc_notify_queue_flush()
+        except Exception:
+            pass
         # 关键日志:定位“tts 结束绿灯仍不灭”究竟是 base 态(如 charging/full)还是 playing 未退出
         try:
             if "vc_config" in globals() and vc_config and (
@@ -1317,8 +1604,32 @@ class VCSessionMediaMixin:
         try:
             if getattr(self, "Close_flag", 0) != 1:
                 return
+            ms_phase = int(getattr(self, "_vc_ms_phase", 0))
+            mode_switch_tail = False
+            if ms_phase == 2:
+                play_gen = int(getattr(self, "_vc_ms_play_gen", -1))
+                active_gen = int(getattr(self, "_vc_ms_active_gen", 0))
+                if play_gen != active_gen:
+                    print("[VC][ms] play_end stale play_gen={} active={}".format(
+                        play_gen, active_gen))
+                    return
+                mode_switch_tail = True
             self.Close_flag = 0
             print("[VC] play_end -> Close (Close_flag)")
+            try:
+                if mode_switch_tail:
+                    self._vc_close_reason = "mode_switch_play_end"
+                    self._vc_ms_phase = 3
+                    self._vc_ms_close_pending = 1
+                    block_ms = 500
+                    try:
+                        block_ms = self._vc_ms_key_block_ms()
+                    except Exception:
+                        pass
+                    self._vc_ms_key_block_until_ms = utime.ticks_add(utime.ticks_ms(), block_ms)
+                    print("[VC][ms] key cooldown {}ms after play_end".format(block_ms))
+            except Exception:
+                pass
             EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
         except Exception as e:
             usys.print_exception(e)
diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_ux.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_ux.py
new file mode 100644 (file)
index 0000000..8ef0223
--- /dev/null
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-\r
+"""设计稿 UX 四态:idle / recording / thinking / tts(与 FSM 八态解耦)。"""\r
+try:\r
+    from vc_fsm import VCTurnState\r
+except Exception:\r
+    VCTurnState = None\r
+\r
+UX_IDLE = 0\r
+UX_RECORDING = 1\r
+UX_THINKING = 2\r
+UX_TTS = 3\r
+\r
+_UX_NAMES = ("idle", "recording", "thinking", "tts")\r
+\r
+\r
+def ux_name(ux):\r
+    try:\r
+        return _UX_NAMES[int(ux)]\r
+    except Exception:\r
+        return "idle"\r
+\r
+\r
+def derive_ux_state(vc):\r
+    """对齐 录音上传与播放-演示文稿:按键表与 LED 均基于此四态。"""\r
+    try:\r
+        if hasattr(vc, "_vc_tts_playback_active") and vc._vc_tts_playback_active():\r
+            return UX_TTS\r
+    except Exception:\r
+        pass\r
+    try:\r
+        if getattr(vc, "VC_record_state_flag", 0) != 0:\r
+            return UX_RECORDING\r
+    except Exception:\r
+        pass\r
+    try:\r
+        if int(getattr(vc, "_ptt_await_server", 0)) == 1:\r
+            return UX_THINKING\r
+    except Exception:\r
+        pass\r
+    if VCTurnState is not None:\r
+        try:\r
+            t = getattr(getattr(vc, "fsm", None), "turn", VCTurnState.Idle)\r
+            # PTT 轮间:FSM 仍 Playing 但本地已停播 → 设计 idle\r
+            if int(getattr(vc, "VC_mode", 0)) == 1 and t in (\r
+                VCTurnState.Playing,\r
+                VCTurnState.AwaitPlayedAck,\r
+            ):\r
+                return UX_IDLE\r
+            if t == VCTurnState.Thinking:\r
+                return UX_THINKING\r
+            if t in (\r
+                VCTurnState.TtsReceiving,\r
+                VCTurnState.Playing,\r
+                VCTurnState.AwaitPlayedAck,\r
+            ):\r
+                return UX_TTS\r
+        except Exception:\r
+            pass\r
+    return UX_IDLE\r