From: snickersun Date: Mon, 25 May 2026 03:44:43 +0000 (+0800) Subject: 整体架构优化,拆分仲裁等单独逻辑。 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=49431f6b995bed415bfeef6979a7a3112017d1a3;p=mirrors%2FASR1603_G5.git 整体架构优化,拆分仲裁等单独逻辑。 更新操作逻辑详细设计文档 添加项目默认agent规则 --- diff --git a/.cursor/rules/001-base.mdc b/.cursor/rules/001-base.mdc new file mode 100644 index 00000000..bde50c0b --- /dev/null +++ b/.cursor/rules/001-base.mdc @@ -0,0 +1,9 @@ +--- +alwaysApply: true +--- +# AI 行为基础规则 +1. 所有代码必须符合项目架构,不随意新增文件 +2. 命名、注释、格式统一 +3. 必须做边界判断、错误处理 +4. 不破坏原有功能 +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 index 00000000..0d59d0d1 --- /dev/null +++ b/.cursor/rules/002-design.mdc @@ -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 index 00000000..01fb226a --- /dev/null +++ b/.cursor/rules/003-bugfix.mdc @@ -0,0 +1,9 @@ +--- +alwaysApply: true +--- +# 修复 Bug 强制规则 +1. 先定位根因,再给修复方案 +2. 最小改动,不扩散影响范围 +3. 必须说明:原因 → 影响 → 修复点 +4. 修复后要验证边界 case +5. 不改无关代码、不引入新问题 \ No newline at end of file diff --git a/.cursor/rules/project.mdc b/.cursor/rules/project.mdc new file mode 100644 index 00000000..2499777d --- /dev/null +++ b/.cursor/rules/project.mdc @@ -0,0 +1,11 @@ +--- +description: EG810 MicroPython 项目约定 +alwaysApply: true +--- +所有生成代码必须: +- 你是高级嵌入式工程师,代码必须遵循最佳实践和最佳设计原则 +- 本工程是micropython工程, +- 2.sdk/Helios_SDK是编译路径,2.sdk/project/7PIN_1_5是项目路径 ,代码优先在项目路径里找,没有的通用代码才在编译路径里。 +- 编译指令是 new_clean.bat k8 1 +- 分析日志解决问题时,要考虑根因,不要只是靠简单判断条件进行兜底修复。 +- 每次修改都要考虑副作用,不能拆东墙补西墙 \ No newline at end of file diff --git "a/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240VC\346\236\266\346\236\204\351\207\215\346\236\204\344\270\216\350\277\255\344\273\243\350\256\241\345\210\222.md" "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240VC\346\236\266\346\236\204\351\207\215\346\236\204\344\270\216\350\277\255\344\273\243\350\256\241\345\210\222.md" new file mode 100644 index 00000000..a14ecabd --- /dev/null +++ "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240VC\346\236\266\346\236\204\351\207\215\346\236\204\344\270\216\350\277\255\344\273\243\350\256\241\345\210\222.md" @@ -0,0 +1,268 @@ +# AI吊坠 VC 架构重构与迭代计划 + +> **关联文档**:[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)、[详设-对照与决策](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md)、[VC_SESSION_ARCH](./VC_SESSION_ARCH.md) +> **实现真源**:`2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/` +> **状态**:I0~I3 已落地;**用户开录统一入口**(start.mp3 + 长/PTT 分流)已落地(2026-05-22) + +--- + +## 0. 问题陈述 + +固件处于 **FSM 半迁移** 阶段:协议轮次(`fsm.turn`)、媒体硬件标志(`VC_Play_flag`)、打断补丁标志(`is_ruppt_tts`、`_ptt_idle_latch`…)、模式切换子状态(`_vc_ms_phase`)**四套状态并行可写**,且 LED 存在「业务直推 + FSM 纠偏 + AI_led 锁」三条路径,导致越改越乱。 + +--- + +## 1. 设计原则(准绳) + +| # | 原则 | +|---|------| +| P1 | **唯一编排线程**:除 EXTI 仅投递事件外,副作用只在 `publish_AI_btnInfo` 队列执行 | +| P2 | **FSM 只管协议轮次**:`session` / `turn` / `await_played` / `tts_rx_phase` | +| P3 | **硬件态归 Media**:`VC_record_state_flag` / `VC_Play_flag` 由采播层维护,编排层只读或通过 action 改 | +| P4 | **LED 只读派生**:业务设 `_led_override` 或调 `vc_led_sync`;`set_led_state` 由 Presenter 聚合后单次下发 | +| P5 | **打断用 InterruptContext + turn_gen**:避免 `is_ruppt_tts` 等布尔堆叠 | +| P6 | **模式切换用 OverlayState**:按键屏蔽单入口,不分散 `_vc_ms_keys_blocked` | +| P7 | **用户开录单入口、模式分流**:长对话与 PTT 提示音/上行时序不同,禁止一套 defer 逻辑硬套两种模式 | + +--- + +## 2. 目标架构 + +``` +GPIO/WS/Timer ──► publish_AI_btnInfo ──► AI_VC.handle / _fsm_apply_actions + │ + ┌─────────────────────────┼─────────────────────────┐ + ▼ ▼ ▼ + VCStateMachine InterruptContext VCOverlayState + (vc_fsm) (vc_interrupt) (vc_overlay) + │ │ + └────────────┬────────────┘ + ▼ + VCModeStrategy (vc_mode_strategy) ──► 按键 press/release/long + │ + ▼ + LedPresenter (vc_present) + │ + ▼ + set_led_state → AI_led +``` + +### 2.1 新增 / 重构模块 + +| 文件 | 职责 | +|------|------| +| `vc_interrupt.py` | `InterruptContext`:打断轮次、同步 legacy 标志 | +| `vc_present.py` | `build_present_state` / `present_to_led_msg`:LED 唯一推导 | +| `vc_overlay.py` | `OVERLAY_*` 常量、`overlay_keys_blocked`:模式切换按键屏蔽 | +| `vc_notify.py` | `NotifyScheduler`:`play_tts` takeover/defer、查找设备等 | +| `vc_mode_strategy.py` | `LongDialogueStrategy` / `PTTStrategy`:AI 键分发 | +| `vc_session_media.py` | **用户开录**:`_vc_user_record_entry*`、上行门控、`VC_Speech_*` | + +### 2.2 副作用单出口(I1) + +`_fsm_apply_actions` 支持的动作: + +| action | 说明 | +|--------|------| +| `uplink_start` / `uplink_stop` | 经 `_vc_uplink_start_recording` → `_vc_user_record_entry`(长对话)或 PTT 直开 | +| `play_start` | 已有 | +| `send_audio_played` | 已有 | +| `led_sync` | 调用 `_vc_led_present_sync` | +| `stop_downlink` | `_vc_stop_downlink_for_interrupt` | +| `send_cancel` | WebSocket `cancel` | + +`vc_session_io` 禁止直接 `set_led_state`(thinking 等),改为 `vc_led_sync`。 + +--- + +## 3. 迭代计划与落地状态 + +### I0 止血 ✅ + +- [x] `_uplink_request_start` 删除 **coerce turn→Listening**(改由 `on_server_canceled` / 打断路径设 turn) +- [x] 长对话 EXTI:`vc_long_dialogue_key_down` **再入队**到 btn 线程,EXTI 不直接改标志 +- [x] `overlay_keys_blocked` 统一模式切换按键屏蔽(兼容 `_vc_ms_phase`) +- [x] 打断路径去掉提前 `_long_dialogue_publish_listening` / 重复 `_vc_arm_key_press_tone` +- [x] `_uplink_request_start` 末尾不再重复播按键音(避免与 `user_record` 双播) + +> **演进说明(I0 之后)**:7PIN_1_5 开录提示音由 I0 阶段的 `RECORD_START_TONE=None` 调整为 **`start.mp3`**,见下文「用户开录」。 + +### I1 副作用收口 ✅ + +- [x] 扩展 `_fsm_apply_actions`:`led_sync` / `stop_downlink` / `send_cancel` +- [x] `vc_session_io` 思考灯改为 `vc_led_sync` +- [x] 订阅 `vc_led_sync` → `_vc_led_present_sync` + +### I2 Presenter + InterruptContext ✅ + +- [x] `InterruptContext` 接管长对话打断 begin/end,同步 `is_ruppt_tts` 等 legacy 字段 +- [x] `_vc_publish_led` / `_vc_led_sync_from_fsm` 收敛为 Presenter 路径 +- [x] `get_vc_present_state` 供调试与 `AI_led` 查询 + +### I3 NotifyChannel + ModeStrategy ✅ + +- [x] `play_tts`:`NotifyScheduler.classify_play_tts` → `takeover`(断连重连)/ `defer`(对话中排队) +- [x] `server.started`:`NotifyScheduler.plan_on_started` + `_vc_apply_notify_started_plan` +- [x] `vc_mode_strategy.py`:`LongDialogueStrategy` / `PTTStrategy`,`AI_key_press/release/long_press` 经 `_vc_get_strategy()` +- [x] `_notify_queue`:`VC_Play_End` 后 `_vc_notify_queue_flush` + +### 用户开录统一入口 ✅(2026-05-22) + +长对话 **AI 键打断/续聊** 与单对话 **长按开麦** 共用 `_vc_user_record_entry(reason)`,按 `VC_mode` 分流;提示音统一为 **`RECORD_START_TONE_FILE`(start.mp3)**,与短按「滴」分离。 + +#### 7PIN_1_5 配置(`products/configs/7PIN_1_5/default_config.py`) + +| 配置项 | 值 | 用途 | +|--------|-----|------| +| `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_record_state_flag` | 含义 | +|------------------------|------| +| `0` | 未录音 | +| `1` | 硬件已启,PCM 进 `AI_record_tmp_queue`,**不上传** | +| `2` | `VC_Speech_Start_now()` 后,允许 `AI_client_VC_send` 上行 | + +#### API 与内部标志(`vc_session_media.py` / `vc_session.py`) + +| 符号 | 说明 | +|------|------| +| `_vc_user_record_entry(reason)` | 统一入口;`VC_mode==1` → PTT,否则 → 长对话 | +| `_vc_user_record_entry_long` | 长对话:defer 上行 + defer 蓝灯 | +| `_vc_user_record_entry_ptt` | PTT:立即 `Start` + `Start_now` + 蓝快闪 | +| `_vc_user_record_uplink_open` | 长对话:滴声结束后 `Start_now` + `listening` | +| `_vc_defer_listening_led` | `1` 时 `VC_Speech_Start` 不抢先发 listening | +| `_vc_skip_local_play_stop_record` | 播 start.mp3 期间 `_vc_local_play` 不 `VC_Speech_End` | +| `_vc_uplink_start_recording` | `request_uplink_start` 最终调用 `user_record_entry` | + +#### 时序对照 + +| 模式 | 步骤 | LED | +|------|------|-----| +| **长对话** 打断/续聊 | ① `VC_Speech_Start`(flag=1 仅缓冲)→ ② start.mp3 → ③ 定时器 `_vc_user_record_uplink_open` → `VC_Speech_Start_now` | ③ 与上行同时蓝**慢闪** | +| **单对话** PTT 长按 | ① 立即 `VC_Speech_Start` + `VC_Speech_Start_now`(flag=2 上行)→ ② 并行 start.mp3 | ① 即蓝**快闪**;**松手** `end_speech` 停传 → 思考灯等 ASR | + +```mermaid +sequenceDiagram + participant U as 用户按键 + participant E as _vc_user_record_entry + participant L as entry_long + participant P as entry_ptt + participant M as 媒体/WS + + U->>E: uplink / ptt_arm + alt VC_mode 长对话 + E->>L: defer LED + 仅缓冲 + L->>M: VC_Speech_Start flag=1 + L->>L: start.mp3 + L->>M: uplink_open Start_now flag=2 + L->>L: listening 慢闪 + else VC_mode 单对话 PTT + E->>P: 立即上行 + P->>M: Start + Start_now flag=2 + P->>L: listening 快闪 + P->>P: start.mp3 并行 + U->>M: 松手 end_speech + M->>M: thinking 等 ASR + end +``` + +#### 调用链入口 + +| 场景 | 入口 | +|------|------| +| 长对话 `request_uplink_start`(含 `canceled` 后续聊) | `_vc_uplink_start_recording` → `_vc_user_record_entry_long` | +| PTT `ptt_begin` å·² started | `_ptt_arm_uplink` → `_vc_user_record_entry_ptt` | +| PTT 等 `server.started` 后首轮 | `OP_PTT_ROUND` → `_vc_user_record_entry("ptt_begin/started")` | +| PTT `asr.started` 且 `_ptt_defer_speech_until_asr` | `_vc_on_server_record_uplink_ready` → `_vc_user_record_entry_ptt` | + +#### 联调日志关键字 + +| 模式 | 期望日志 | +|------|----------| +| 长对话 | `[VC][user_record] entry_long` → `[VC] record_start_tone` → `[VC][user_record] uplink_open` | +| PTT | `[VC][user_record] entry_ptt rec=2` → `[PTT] release end_speech` | + +#### PTT 轮间 LED(TTS 播完应 idle) + +| 阶段 | LED | +|------|-----| +| 松手等 ASR/LLM | `_ptt_await_server=1` → thinking | +| TTS 播放 | `stream_active` → playing | +| TTS 本地播完 | `play_end` 先 `on_local_play_end`,再 `VC_Play_End` → **idle**(`VC_Play_End/ptt_round`),等下次长按 | + +根因修复:勿在 `VC_Play_End` 时仅凭 `FSM.turn=Playing` 再亮思考灯。 + +> 全量场景表(按键/打断/一轮对话/通知/Close 等)见 **[模式执行手册](./AI吊坠VC模式执行手册-长对话与单对话.md)**。 + +--- + +### 7PIN_1_5 机型说明(无闹钟 / IoT 查找设备) + +| 能力 | 实现 | +|------|------| +| 闹钟/日程 | **不启用**(`AI_ALARM_ENABLED=False`,`main_init` 不加载 `ai_alarm`) | +| IoT 查找设备 | `protocol.IOTWeb_find_deviceProtoCol.recv` → `play_tts([文案, 1, "find_device"])` | +| 通道 | `is_close_VC=1` + `find_device` → **takeover**(立即断对话会话、重连 WS 播 TTS,播完 Close) | +| 对话中收到查找 | 不打 defer 队列(与闹钟不同,查找须马上响) | + +联调日志:`[VC][notify]` 不应出现查找设备 defer;应走 `play_tts_cb` → `websocket_deconnect` → `AI_VC_start`。 + +--- + +## 4. 状态不变式(联调日志) + +开发模式可在 `_1S_timer_cb_` 打印违反项: + +| ID | 不变式 | +|----|--------| +| INV-1 | `turn==Playing` → `VC_Play_flag==1` 或打断丢弃中 | +| INV-2 | `VC_record_state_flag!=0` → turn 为 Listening/Speech* 或 InterruptContext.active | +| INV-3 | `_vc_pending_interrupt_uplink==1` → `is_ruppt_tts==1` | +| INV-4 | `Close` 后 InterruptContext/overlay/PTT latch 已清零 | +| INV-5 | 同一编排 tick 内 `set_led_state` 至多 1 次(Presenter) | +| INV-6 | 长对话 `entry_long` 期间 `VC_record_state_flag==2` 前应已播 start.mp3(或配置 None 时立即 uplink_open) | +| INV-7 | PTT 长按按住期间 `VC_record_state_flag==2`;松手后应 `end_speech` 且非误触发 `AI_VC_start` | + +--- + +## 5. 联调检查清单(每迭代必跑) + +| # | 长对话 | 单对话(PTT) | +|---|--------|----------------| +| 1 | TTS/思考中短按打断:500ms 内无残余 TTS | 长按 `ptt_begin` 可打断下行 TTS | +| 2 | `VC_init` 后队列深度 0 | 同左 | +| 3 | 开录前会话已 `started` | 未 started 时走 `audio.create`,勿 SKIP | +| 4 | 打断续聊:先听到 **start.mp3**,再蓝慢闪且对方收到 PCM | **按住即上行**;松手 thinking → TTS playing → **播完 idle** | +| 5 | `canceled` 后 800ms 内开麦(`entry_long`→`uplink_open`) | 松手→思考灯→再长按新一轮 | +| 6 | 日志含 `[KEY][FSM][LED][VC][user_record]` | 含 `[PTT] release end_speech` | +| 7 | 短按仅 **key_prompt æ»´**,非 start.mp3 | 短按停播 TTS 不误 `end_speech` | + +编译:`new_clean.bat k8 1`(项目路径 `7PIN_1_5`)。 + +--- + +## 6. 与交互规范 REQ 的映射 + +| REQ | 架构落点 | +|-----|----------| +| REQ-AI-01 | 短按操作确认:`KEY_PRESS_TONE_FILE`(key_prompt.mp3),与开录 start.mp3 分离 | +| REQ-AI-02~04 | `InterruptContext` + cancel 串行 + 长对话 `entry_long` 开麦 | +| REQ-AI-05~06 | PTT `_ptt_idle_latch` → Presenter `force_idle`;松手 `end_speech` | +| REQ-AI-07 | 开录提示音 `RECORD_START_TONE_FILE=start.mp3`;长对话 **defer 上行**,PTT **立即上行** | +| REQ-NOTE-02 | Presenter 表驱动 listening/thinking/playing(慢闪/快闪) | +| REQ-SUP-01 | AI_led 充电灯被 Presenter 阶段压制 | +| REQ-PWR-05 | OverlayState 模式切换屏蔽按键 | +| IoT 查找 | `vc_notify` takeover,无闹钟 defer | + +--- + +## 7. 引用 + +- **模式执行手册(操作×协议×音×录×灯×TTS 顺序 + 实现标注)**:[AI吊坠VC模式执行手册-长对话与单对话.md](./AI吊坠VC模式执行手册-长对话与单对话.md) +- 评审入口:[详设-对照与决策 §3~§4](./AI吊坠(双按键+三色指示灯)详设-对照与决策.md) +- 场景步骤:[详设-场景与协议流程](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md) diff --git "a/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240VC\346\250\241\345\274\217\346\211\247\350\241\214\346\211\213\345\206\214-\351\225\277\345\257\271\350\257\235\344\270\216\345\215\225\345\257\271\350\257\235.md" "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240VC\346\250\241\345\274\217\346\211\247\350\241\214\346\211\213\345\206\214-\351\225\277\345\257\271\350\257\235\344\270\216\345\215\225\345\257\271\350\257\235.md" new file mode 100644 index 00000000..4e8296f2 --- /dev/null +++ "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240VC\346\250\241\345\274\217\346\211\247\350\241\214\346\211\213\345\206\214-\351\225\277\345\257\271\350\257\235\344\270\216\345\215\225\345\257\271\350\257\235.md" @@ -0,0 +1,293 @@ +# AI吊坠 VC 模式执行手册(长对话 / 单对话) + +> **真源代码**:`2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/` +> **关联**:[迭代计划](./AI吊坠VC架构重构与迭代计划.md)、[交互规范_new](./AI吊坠(双按键+三色指示灯)交互规范_new.md)、[详设-场景与协议](./AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md) +> **机型示例**:7PIN_1_5(`RECORD_START_TONE=start.mp3`,无闹钟) + +--- + +## 0. 图例与全局约束 + +### 0.1 实现状态标注 + +| 标记 | 含义 | +|------|------| +| ✅ | 已在 7PIN_1_5 项目路径落地,I0~I3 + 用户开录/PTT 轮间灯已联调修过 | +| ⚠️ | 有实现但属兼容/遗留分支,或依赖配置关闭 | +| 🔲 | 规范有述,本机型/本迭代未启用 | + +### 0.2 模式与协议差异(`vc_fsm.VCMode`) + +| 项 | 长对话 `VC_mode=0` | 单对话 PTT `VC_mode=1` | +|----|-------------------|------------------------| +| VAD | 服务端 VAD 开(`disable_vad=false`) | VAD 关(`disable_vad=true`) | +| 上行包裹 | 直接 PCM 帧(`started` 后) | 必须 **`audio.create` → 帧 → `audio.completed`** | +| 开麦触发 | 短按续聊 / 打断后开麦 / TTS 播完自动开麦(dialog) | **仅长按** `ptt_begin` | +| 停麦 | `vad.silence` → `uplink_stop`;或本地 `VC_Speech_End` | **松手** → `AI_client_VC_end_speech()` | +| 策略类 | `LongDialogueStrategy` | `PTTStrategy` | + +### 0.3 编排线程(所有副作用顺序的前提) + +| 步骤 | 说明 | 代码 | +|------|------|------| +| 1 | GPIO/EXTI 只投递 `EventMesh` / `publish_AI_btnInfo` | `vc_buttons.py`、`vc_long_dialogue_key_down` 再入队 ✅ | +| 2 | **btn 队列**内执行:`AI_key_*`、`ptt_begin`、`request_uplink_*`、`VC_Play_End`、FSM actions | `vc_session.py` `_vc_enqueue_btn` ✅ | +| 3 | WS 收包可在 IO 线程解析,**改 FSM/灯/上行** 须经 `on_server_event` → actions 或再入队 btn | `vc_session_io.py` ✅ | + +### 0.4 录音上行三态(共用) + +| `VC_record_state_flag` | 硬件 | WS 上行 | 典型设置点 | +|------------------------|------|---------|------------| +| `0` | 停 | 无 | `VC_Speech_End` | +| `1` | 开,PCM 进 `AI_record_tmp_queue` | **禁止** | `VC_Speech_Start` | +| `2` | 开 | **允许** `AI_client_VC_send` | `VC_Speech_Start_now` | + +### 0.5 提示音配置(7PIN_1_5) + +| 配置 | 文件 | 触发场景 | 状态 | +|------|------|----------|------| +| `KEY_PRESS_TONE_FILE` = key_prompt.mp3 | 短按操作确认「滴」 | 长对话短按(非开录主路径) | ✅ | +| `RECORD_START_TONE_FILE` = start.mp3 | 用户开录 | 长对话 `entry_long`;PTT `entry_ptt` 并行播 | ✅ | +| `PTT_RECORD_TOO_SHORT_TTS` | notify 合成 | PTT 长按录音 < `PTT_MIN_RECORD_MS` | ✅ | + +### 0.5.1 按键与 sid(见 `录音上传与播放-演示文稿.md`) + +| 键 | 要点 | 代码 | +|----|------|------| +| **AI 单击 idle** | 开录;**仅 Power 短按 Close 后**下次为新 sid,否则续旧 sid | `KeyRouter._long_ai_press` | +| **AI 单击 recording** | 停录立即重开,旧 sid | `_vc_restart_record_same_dialog` | +| **PTT 单击 idle** | `key_prompt` + idle | `KeyRouter.handle_ptt_release` | +| **PTT 录音过短** | notify TTS + idle | `KeyRouter.ptt_release_after_long_press` | +| **Power 单击** | cancel → Close → 清 `_cur_dialog_id` | `KeyRouter.handle_power_short` | +| **Power 长按 3s** | 关机音 + 断电 | `vc_buttons.pwk_key_Long_Press` | + +实现收敛:`vc_ux` + `vc_key_router` + `vc_media_arb`(见 `VC实现审查与简化架构.md`)。 + +### 0.6 LED 推导优先级(`vc_present.present_to_led_msg`) + +从高到低(先命中先生效): + +1. `_led_override`(`_vc_publish_led` imperative)✅ +2. `force_idle`(`_ptt_should_force_idle_led` / `_vc_led_should_standby_idle`)✅ +3. `hold_listening`(录音 / PTT 打断锁 / `_vc_defer_listening_led` 等)✅ +4. PTT 且 `_ptt_await_server` → **thinking** ✅ +5. `stream_active`(`VC_Play_flag` / TTS 播中)→ **playing** ✅ +6. FSM `turn` → thinking / listening / idle;PTT 且已停播的 Playing → **idle** ✅ + +灯物理节奏:`VC_mode=0` 蓝**慢闪**;`VC_mode=1` 蓝**快闪**(`mode=blue_blink` / period 250ms)✅ + +--- + +## 1. 服务端事件 → FSM → 编排动作(共用) + +WS 事件统一入口:`vc_session_io` → `fsm.on_server_event` → `_fsm_apply_actions` ✅ + +| 服务端 event | FSM.turn 变化 | 典型 actions / 副作用 | 长对话 | PTT | +|--------------|---------------|----------------------|--------|-----| +| `started` | → `Listening` | `NotifyScheduler.plan_on_started`(媒体/开麦计划) | ✅ | ✅ | +| `asr.started` | `speech_uplink_open=1` | PTT:可触发 `_vc_user_record_entry_ptt`(defer 场景) | ✅ | ✅ | +| `asr.*` / `asr.completed` | 结果写 `AI_ASR_str` | 配置/cmd 可能 `Close`+重连 | ✅ | ✅ | +| `vad.silence` | → `SpeechEnded` | **`uplink_stop`**(录音中) | ✅ 主路径 | 🔲 VAD 关 | +| `llm.created` | → `Thinking` | `_ptt_await_server=0`;`vc_led_sync` | ✅ | ✅ | +| `llm.completed` | 可能补 Thinking | 计时兜底 | ✅ | ✅ | +| `tts.started` | → `TtsReceiving` | **`uplink_stop`** + **`play_start`**;`_ptt_await_server=0` | ✅ | ✅ | +| `tts.completed` | 保持 `Playing`,`await_played=1` | 本地播完由 `play_end` 收尾 | ✅ | ✅ | +| `completed` | 无 TTS 时 → `Listening` | 长对话可 `uplink_start`;有 TTS 时不抢跑 | ✅ | 仅收尾 | +| `canceled` | → `Listening` | 长对话:`uplink_start`(打断后续聊);PTT:`_vc_on_ptt_cancel_settled` | ✅ | ✅ | + +**TTS 本地播完顺序(2026-05-22 修)** ✅ + +``` +tts.completed → (缓冲播完) → _vc_fire_local_play_end + → ① fsm.on_local_play_end()(含 send_audio_played、长对话 dialog 可 uplink_start) + → ② _ptt_await_server=0 + → ③ VC_Play_End()(PTT → idle / VC_Play_End/ptt_round) + → ④ fsm.reset_rx_phases() +``` + +--- + +## 2. 长对话模式(`VC_mode=0`) + +### 2.1 按键与入口 + +| # | 用户操作 | 前置条件 | 执行顺序(协议·音·录·灯·TTS) | 实现 | +|---|----------|----------|------------------------------|------| +| L1 | **短按** 唤醒 | 未连接或未 `started` | ① `AI_VC_start` ② WS `Start` ③ 等 `started` ④ `NotifyScheduler` 可能 `OP_LONG_UPLINK` / boot 提示 ⑤ 灯 **listening 慢闪** | ✅ `_ai_key_press_long_dialogue_wake` | +| 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` | +| 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` | +| L4 | **长按** | 同短按分发 | 与 L1/L2/L3 相同(`KeyRouter.dispatch_press` 入队) | ✅ | +| L5 | **松手** | 长对话 | 无 PTT 式 end_speech;停麦靠 **vad.silence** 或本地逻辑 | ✅ | +| L6 | EXTI 按下 | 长对话 | 仅 `publish_AI_btnInfo` 再入队,不直接改标志 | ✅ `vc_long_dialogue_key_down` | + +**短按打断时序(精简)** + +```mermaid +sequenceDiagram + participant U as 用户短按 + participant B as btn队列 + participant W as WS + participant M as 媒体 + + U->>B: interrupt_on_short_press + B->>W: cancel + W-->>B: canceled + B->>M: entry_long Start flag=1 + M->>M: start.mp3 + M->>M: uplink_open Start_now flag=2 + M->>W: PCM 上行 +``` + +### 2.2 服务端驱动一轮(VAD 开) + +| 阶段 | 协议/本地顺序 | LED | 实现 | +|------|---------------|-----|------| +| 聆听 | 用户说话 → PCM 上行 | listening 慢闪 | ✅ | +| 停说 | `vad.silence` → `uplink_stop` → `VC_Speech_End`(若本地在录) | 仍 listening 或 **listening**(`VC_Speech_End/long_wait`) | ✅ | +| 思考 | `llm.created` | **thinking**(`vc_led_sync`) | ✅ | +| TTS 下发 | `tts.started` → `uplink_stop` + `VC_Play_Start` | **playing** | ✅ | +| TTS 播完 | `play_end` → 长对话 **dialog** 可自动 `uplink_start`(续聊) | 回 **listening** 或保持策略 | ✅ `on_local_play_end` | +| 无 TTS | `completed` 且非 await_played | 可 `uplink_start` | ✅ | + +### 2.3 长对话 · 提示音 / 通知 / 关闭 + +| # | 场景 | 顺序 | 实现 | +|---|------|------|------| +| L7 | 模式切换 / 开机提示 TTS | `play_tts` **takeover** → 断 WS → 播完 `Close` | ✅ `vc_notify` | +| L8 | 对话中 IoT 查询类 TTS | **defer** 队列,`VC_Play_End` 后 flush | ✅ | +| L9 | **查找设备** | **takeover**(立即),非 defer | ✅ | +| L10 | 空闲超时 `VC_IDLE_AUTO_CLOSE_SEC` | `Close` → 灯 idle | ✅ | +| L11 | `Close()` | 停录停播 → `websocket_deconnect` → `VC_init` → 灯 idle | ✅ | + +--- + +## 3. 单对话 PTT 模式(`VC_mode=1`) + +### 3.1 按键与入口 + +| # | 用户操作 | 前置条件 | 执行顺序(协议·音·录·灯·TTS) | 实现 | +|---|----------|----------|------------------------------|------| +| P1 | **长按** 首轮 | 未连接 | ① `_ptt_force_record_on_started=1` ② `AI_VC_start` ③ `started` ④ `OP_PTT_ROUND`:`audio.create`+`entry_ptt` | ✅ `ptt_begin` | +| P2 | **长按** å·² started | 无 TTS/思考占用 | ① `ptt_begin`→`_ptt_arm_uplink` ② **`audio.create`** ③ `entry_ptt`:`Start`+**`Start_now`**(flag=2) ④ 并行 **start.mp3** ⑤ 灯 **listening 快闪** | ✅ | +| P3 | **长按** 打断 TTS/思考 | `_vc_ptt_need_interrupt_wait` | ① `_vc_begin_ptt_cancel_wait` **`cancel`** ② 灯 listening(`ptt_cancel_wait`)③ 收 **`canceled`**→`_ptt_arm_uplink`(同 P2) | ✅ | +| P4 | **按住** 说话 | 录音中 flag=2 | 持续 PCM 上行(须已 `audio.create`) | ✅ | +| 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` | +| P6 | **短按**(未长按) | TTS 播中 | ① `cancel` ② 停播 ③ `_ptt_idle_latch=1` ④ 灯 **idle**(不再被 Play_End 拉回 thinking) | ✅ `_ptt_stop_tts_on_short_press` | +| P7 | **短按** | 空闲 | 按下由策略吞掉;松手无 end_speech | ✅ `PTTStrategy.on_key_press` | +| P8 | **松手** 无录音 | 误长按未开录 | `cancel` 清理 pending `audio.create` | ✅ | +| P9 | **松手** 录音过短 | `< PTT_MIN_RECORD_MS` | notify TTS + idle | ✅ `ptt_release_after_long_press` | +| P10 | **Power 短按** | 任意 UX | cancel → Close → 清 sid | ✅ `KeyRouter.handle_power_short` | + +**PTT 一轮完整时序** + +```mermaid +sequenceDiagram + participant U as 用户 + participant B as btn + participant W as WS + participant M as 媒体/灯 + + U->>B: 长按 ptt_begin + B->>W: audio.create + B->>M: entry_ptt Start+Start_now + M->>M: start.mp3 并行 + M->>M: listening 快闪 + U->>B: 松手 + B->>W: audio.completed + B->>M: VC_Speech_End + M->>M: thinking (_ptt_await_server) + W-->>M: llm.created / tts.started + M->>M: playing + W-->>M: tts.completed + M->>M: play_end → on_local_play_end → VC_Play_End + M->>M: idle 等下次长按 +``` + +### 3.2 服务端驱动一轮(VAD 关) + +| 阶段 | 协议顺序 | LED | 实现 | +|------|----------|-----|------| +| 松手后 | `asr.*` → `llm.created` | thinking(await 或 FSM) | ✅ | +| TTS | `tts.started` → 下行 PCM → `VC_Play_Start` | **playing** | ✅ | +| 播完 | `tts.completed` → 本地播完 → **idle** | **idle**(非 thinking) | ✅ 2026-05-22 ä¿® | +| 轮间 | 会话保持 `started`,**不**自动开麦 | idle,等 **P2** | ✅ | + +### 3.3 PTT · 提示音 / 关闭 + +| # | 场景 | 顺序 | 实现 | +|---|------|------|------| +| P9 | 模式切换提示 | takeover TTS,播完 Close | ✅ | +| P10 | 空闲深睡 `VC_IDLE_DEEP_SLEEP_SEC` | `Close`(`VC_IDLE_AUTO_CLOSE_SEC_PTT=0` 不累加旧逻辑) | ✅ | +| P11 | `Close` | 同共用 L11 | ✅ | + +--- + +## 4. 用户开录统一 API(跨模式) + +| API | 长对话 | PTT | 文件 | +|-----|--------|-----|------| +| `_vc_user_record_entry` | → `_vc_user_record_entry_long` | → `_vc_user_record_entry_ptt` | `vc_session_media.py` ✅ | +| 上行时机 | start.mp3 后 `uplink_open` | 立即 `Start_now` | ✅ | +| 灯 | defer → 与 uplink 同刻 listening | 立即 listening 快闪 | ✅ | + +**调用链汇总** + +| 场景 | reason / 入口 | +|------|----------------| +| 长对话续聊 | `request_uplink_start` → `key.press/continue` | +| 长对话 canceled 后 | `uplink_start` / `canceled` | +| PTT arm | `_ptt_arm_uplink` → `ptt_begin/arm` | +| PTT 等 started | `OP_PTT_ROUND` → `ptt_begin/started` | +| PTT asr.started defer | `asr.started/defer` | + +--- + +## 5. 共用能力(与模式正交) + +| # | 能力 | 行为摘要 | 实现 | +|---|------|----------|------| +| S1 | **NotifyScheduler** | `play_tts`:takeover vs defer;`server.started` plan | `vc_notify.py` ✅ | +| S2 | **InterruptContext** | 长对话短按打断;同步 `is_ruppt_tts` | `vc_interrupt.py` ✅ | +| S3 | **Overlay 模式切换** | `_vc_ms_phase` / `overlay_keys_blocked` 屏蔽 AI 键 | `vc_overlay.py` ✅ | +| S4 | **FSM actions** | uplink_start/stop、play_start、send_cancel、led_sync、stop_downlink | `vc_session.py` `_fsm_apply_actions` ✅ | +| S5 | **Presenter 灯** | 唯一 `set_led_state` 聚合 | `vc_present.py` ✅ | +| S6 | **闹钟** | 7PIN_1_5 **不加载** `ai_alarm` | 🔲 `AI_ALARM_ENABLED=False` | +| S7 | **蓝牙键 BT_AI_key_press** | 部分走旧 compat `AI_VC_start`+arm tone | ⚠️ 兼容路径 | + +--- + +## 6. 代码索引(按职责) + +| 模块 | 路径 | 职责 | +|------|------|------| +| 编排主类 | `vc_session.py` (`AI_VC`) | 按键、PTT、长对话、Close、灯 latch | +| 媒体/上行 | `vc_session_media.py` | `VC_Speech_*`、`VC_Play_*`、`user_record_*` | +| WS/协议 | `vc_session_io.py` | 事件解析、`llm/tts` 副作用 | +| FSM | `vc_fsm.py` | turn/session、event→action | +| 按键策略 | `vc_mode_strategy.py` | Long / PTT 分发 | +| LED | `vc_present.py` | 灯态推导 | +| 通知 | `vc_notify.py` | started plan / play_tts 分流 | +| 打断 | `vc_interrupt.py` | InterruptContext | +| 配置 | `products/configs/7PIN_1_5/default_config.py` | _tone / idle 秒数 | + +--- + +## 7. 联调日志速查 + +| 模式 | 关键日志 | +|------|----------| +| 长对话开录 | `[VC][user_record] entry_long` → `record_start_tone` → `uplink_open` | +| 长对话打断 | `[KEY] long_dialogue interrupt -> cancel` → `canceled` → `entry_long` | +| PTT 开录 | `[VC][user_record] entry_ptt rec=2` | +| PTT 松手 | `[PTT] release end_speech` | +| PTT 播完 | `[TTS] play_end_fire` → `VC_Play_End/ptt_round` 或 `present_sync idle` | +| 异常 | `VC_Play_End led=... thinking`(播完仍思考 → 查 PTT idle 修复是否合入) | + +--- + +## 8. 修订记录 + +| 日期 | 内容 | +|------|------| +| 2026-05-22 | 首版:长/PTT 分表、协议顺序、提示音/灯/TTS 顺序、实现标注 | +| 2026-05-22 | 补充 PTT TTS 播完 idle、`play_end` 与 FSM 顺序 | diff --git "a/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\344\272\244\344\272\222\350\247\204\350\214\203_new.md" "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\344\272\244\344\272\222\350\247\204\350\214\203_new.md" index e5473187..983b5137 100644 --- "a/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\344\272\244\344\272\222\350\247\204\350\214\203_new.md" +++ "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\344\272\244\344\272\222\350\247\204\350\214\203_new.md" @@ -149,6 +149,7 @@ | **[交互规范-新旧变更说明.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 章一一对应。 diff --git "a/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\350\257\246\350\256\276-\345\257\271\347\205\247\344\270\216\345\206\263\347\255\226.md" "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\350\257\246\350\256\276-\345\257\271\347\205\247\344\270\216\345\206\263\347\255\226.md" index b7e854e7..840c65df 100644 --- "a/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\350\257\246\350\256\276-\345\257\271\347\205\247\344\270\216\345\206\263\347\255\226.md" +++ "b/1.doc/K8_diaozhui/design/AI\345\220\212\345\235\240\357\274\210\345\217\214\346\214\211\351\224\256+\344\270\211\350\211\262\346\214\207\347\244\272\347\201\257\357\274\211\350\257\246\350\256\276-\345\257\271\347\205\247\344\270\216\345\206\263\347\255\226.md" @@ -1,348 +1,349 @@ -# 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<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/Helios_SDK/services/microPython/ports/quectel/core/ai_VC.py` | -| 旧附录(已迁移) | [交互规范_附录-工程与代码审查.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 休眠;清理遗留版本脚本 | - +# 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<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 休眠;清理遗留版本脚本 | + diff --git a/1.doc/K8_diaozhui/design/VC_SESSION_ARCH.md b/1.doc/K8_diaozhui/design/VC_SESSION_ARCH.md index 7f1f9002..fc28bc21 100644 --- a/1.doc/K8_diaozhui/design/VC_SESSION_ARCH.md +++ b/1.doc/K8_diaozhui/design/VC_SESSION_ARCH.md @@ -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\345\256\236\347\216\260\345\256\241\346\237\245\344\270\216\347\256\200\345\214\226\346\236\266\346\236\204.md" "b/1.doc/K8_diaozhui/design/VC\345\256\236\347\216\260\345\256\241\346\237\245\344\270\216\347\256\200\345\214\226\346\236\266\346\236\204.md" new file mode 100644 index 00000000..512f43b1 --- /dev/null +++ "b/1.doc/K8_diaozhui/design/VC\345\256\236\347\216\260\345\256\241\346\237\245\344\270\216\347\256\200\345\214\226\346\236\266\346\236\204.md" @@ -0,0 +1,54 @@ +# VC 实现审查与简化架构(7PIN_1_5) + +> 权威:`录音上传与播放-演示文稿.md`(含 **Power 键**、**sid 规则**、**PTT 录音过短 notify**) + +## 1. 设计稿要点(2025 更新) + +### 媒体三车道 + +| 车道 | 抢占 | +|------|------| +| LLM PCM | notify/local defer | +| Notify TTS | 关 llm + local | +| Local MP3 | 关 llm + notify;录音中可并行 | + +### sid 规则 + +- **AI idle 开录**:默认 **旧 sid**(同 WS 会话续 `dialog_id`)。 +- **仅 Power 短按 Close 后**:清 `_cur_dialog_id`,下次 `AI_VC_start` 为 **新 sid**。 + +### Power 短按 + +各 UX 态均:**协议 cancel(按需)→ Close WS → idle/休眠 → 清 sid**。 + +### PTT 异常 + +长按松手且录音 < `PTT_MIN_RECORD_MS`:cancel、**play_tts notify**、idle。 + +## 2. 代码结构(三件套) + +| 模块 | 文件 | +|------|------| +| UX 四态 | `vc_ux.py` → `derive_ux_state` | +| 按键表 | `vc_key_router.py` → AI / Power / PTT release | +| 媒体仲裁 | `vc_media_arb.py` → `before_local` / `before_notify` / `before_llm_pcm` | + +`vc_mode_strategy` 仅转发 `KeyRouter`;`vc_present` 用 UX 映 LED。 + +## 3. 符合度(当前) + +| 项 | 状态 | +|----|------| +| UX + KeyRouter + MediaArbiter 骨架 | ✅ | +| 长对话 recording 重开 | ✅ | +| PTT idle/thinking/tts 短按 | ✅ | +| Power 短按按 UX cancel+Close | ✅ | +| PTT 录音过短 notify | ✅ | +| notify `play_tts` 仲裁入口 | ✅ | +| `vc_session_io` 仅 FSM actions | 🔲 后续 | + +## 4. 迁移剩余 + +- 收敛 `play_tts_cb` 断 WS:区分 takeover notify 与会话内 synthesis +- 录音灯挂 `VC_Speech_Start_now` 首帧 +- 删与 `InterruptContext` 重复的 `_ptt_*` latch diff --git "a/1.doc/K8_diaozhui/design/\345\275\225\351\237\263\344\270\212\344\274\240\344\270\216\346\222\255\346\224\276-\346\274\224\347\244\272\346\226\207\347\250\277.md" "b/1.doc/K8_diaozhui/design/\345\275\225\351\237\263\344\270\212\344\274\240\344\270\216\346\222\255\346\224\276-\346\274\224\347\244\272\346\226\207\347\250\277.md" new file mode 100644 index 00000000..7b92ccb9 --- /dev/null +++ "b/1.doc/K8_diaozhui/design/\345\275\225\351\237\263\344\270\212\344\274\240\344\270\216\346\222\255\346\224\276-\346\274\224\347\244\272\346\226\207\347\250\277.md" @@ -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秒 | 任意状态 | 播放关机提示音,关闭设备电源 | 操作同长对话 | | + + diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/Makefile b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/Makefile index 152fe305..1c8fa202 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/Makefile +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/Makefile @@ -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 \ diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/main_init.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/main_init.py index e1439b1e..e43dcaa6 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/main_init.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/main_init.py @@ -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) diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py index a06287db..e7c2e84e 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py @@ -1,65 +1,72 @@ -# 设备分支配置:7PIN_1_5 -# 仅用于编译/打包阶段选择,不作为运行时加载入口。 - -DEV_TYPE = "7PIN_1_5" - -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_SILENCE_DEFAULT = 1000 -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 = None -KEY_PRESS_TONE_DELAY_MS = 300 -RECORD_START_TONE_FILE = "U:/static/start.mp3" -# 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 = "请您按住圆形的按钮开始讲话,讲完松开即可 " - +# 设备分支配置: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 = "录音时间太短,请按住圆形按钮说话" + diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/protocol.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/protocol.py index 8e8a3e88..98b11d48 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/protocol.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/protocol.py @@ -1,4197 +1,4197 @@ -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]) #播放tts闹钟文本内容 - 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]) - -#接收小程序控制上传设备状态 -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))) +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 index 00000000..44ade205 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/test/tts_local_mp3_concurrent_test.py @@ -0,0 +1,372 @@ +# -*- coding: UTF-8 -*- +""" +TTS(PCM 流) + 本地 MP3 并行播放自测 + +模拟 VC 长对话场景: + - 服务端 TTS:audio.Audio.PCM + stream_write(16kHz PCM) + - 本地短音:audio.Audio(0).play()(如 think.mp3 / start.mp3) + +用法(设备 REPL 或脚本入口): + import tts_local_mp3_concurrent_test + tts_local_mp3_concurrent_test.run_all() + +或: + exec(open('.../tts_local_mp3_concurrent_test.py').read()) + run_all() + +烧录前请确认本地 MP3 路径存在(任选其一): + U:/static/think.mp3、U:/static/start.mp3、/usr/tesr.mp3 +可选 PCM 文件:/usr/tts_tesr_01.wav(标准 44 字节头 16kHz mono 16bit) +无文件时使用内置生成的测试音调 PCM。 +""" + +import audio +import utime +import usys +import _thread + +try: + import uos +except ImportError: + uos = None + +try: + from machine import Pin +except ImportError: + Pin = None + +# ---- 与 vc_session 一致 ---- +SAMPLE_RATE = 16000 +CHANNELS = 1 +PCM_CHUNK_MS = 40 +TEST_DURATION_MS = 4000 +LOCAL_MP3_CANDIDATES = ( + "U:/static/think.mp3", + "U:/static/start.mp3", + "/usr/tesr.mp3", + "/usr/tts_tesr.mp3", +) +PCM_WAV_CANDIDATES = ( + "/usr/tts_tesr_01.wav", + "/usr/tts_tesr.wav", +) + +_play_pcm = None +_aud = None +_local_cb_events = [] +_pcm_errors = [] +_feed_done = 0 +_test_running = 0 + + +def _log(msg): + try: + print("[tts+mp3-test] {}".format(msg)) + except Exception: + pass + + +def _file_exists(path): + if not path or uos is None: + return False + try: + uos.stat(path) + return True + except Exception: + return False + + +def _pick_first_existing(paths): + for p in paths: + if _file_exists(p): + return p + return None + + +def _local_audio_cb(event): + try: + _local_cb_events.append(event) + if event == 0: + _log("local mp3: audio-play start") + elif event == 7: + _log("local mp3: audio-play finish") + else: + _log("local mp3: cb event={}".format(event)) + except Exception: + pass + + +def _init_audio(): + global _aud + _aud = audio.Audio(0) + try: + if Pin is not None: + _aud.set_pa(Pin.GPIO29) + except Exception: + pass + try: + _aud.setCallback(_local_audio_cb) + except Exception: + pass + try: + _aud.setVolume(10) + except Exception: + pass + + +def _as_pcm_write_buf(data): + """与 vc_session_media 一致:stream_write 需要 bytearray(bytes 切片在部分 MPY 上报错)。""" + if not data: + return bytearray() + if isinstance(data, bytearray): + return data + try: + return bytearray(data) + except Exception: + try: + return bytearray(memoryview(data)) + except Exception: + return bytearray() + + +def _pcm_stream_write(pcm, piece): + buf = _as_pcm_write_buf(piece) + if not buf: + return 0 + return pcm.stream_write(buf, len(buf)) + + +def _pcm_gen_chunk(ms=PCM_CHUNK_MS, freq=523): + """生成 16bit mono PCM 块(无文件时的 TTS 模拟数据)。""" + import math + n = SAMPLE_RATE * ms // 1000 + if n < 1: + n = 1 + buf = bytearray(n * 2) + phase = getattr(_pcm_gen_chunk, "_phase", 0) + for i in range(n): + t = (phase + i) / float(SAMPLE_RATE) + v = int(2800 * math.sin(2.0 * 3.1415926 * freq * t)) + if v > 32767: + v = 32767 + if v < -32768: + v = -32768 + buf[2 * i] = v & 0xFF + buf[2 * i + 1] = (v >> 8) & 0xFF + _pcm_gen_chunk._phase = phase + n + return buf + + +def _read_wav_pcm(path, max_bytes=0): + """读取 WAV 裸 PCM(仅支持标准 44 字节头、16kHz/16bit/mono)。""" + with open(path, "rb") as f: + hdr = f.read(44) + if len(hdr) < 44 or hdr[0:4] != b"RIFF" or hdr[8:12] != b"WAVE": + raise ValueError("not wav") + data = f.read() if not max_bytes else f.read(max_bytes) + return bytearray(data) if data else bytearray() + + +def _pcm_open(): + """对齐 VC_Play_Start:打开 PCM 下行流。""" + global _play_pcm + _play_pcm = None + utime.sleep_ms(50) + last_err = None + for i in range(3): + try: + if i: + utime.sleep_ms(60 + i * 40) + _play_pcm = audio.Audio.PCM(0, 1, SAMPLE_RATE, 1, 1, 2) + ret = _play_pcm.stream_write_start() + _log("PCM stream_write_start ret={} try={}".format(ret, i)) + return ret + except Exception as e: + last_err = e + try: + if _play_pcm is not None: + _play_pcm.close() + except Exception: + pass + _play_pcm = None + if last_err: + raise last_err + return -1 + + +def _pcm_close(): + global _play_pcm + try: + if _play_pcm is not None: + try: + _play_pcm.stream_stop() + except Exception: + pass + try: + _play_pcm.close() + except Exception: + pass + except Exception as e: + _log("PCM close err: {}".format(e)) + _play_pcm = None + + +def _tts_feed_thread(duration_ms, wav_path=None): + """后台线程:向 PCM 流持续写入,模拟 TTS 下行。""" + global _feed_done, _pcm_errors + _feed_done = 0 + end_ms = utime.ticks_add(utime.ticks_ms(), duration_ms) + pcm_data = None + pcm_off = 0 + if wav_path: + try: + pcm_data = _read_wav_pcm(wav_path) + _log("TTS feed use wav pcm bytes={}".format(len(pcm_data))) + except Exception as e: + _log("read wav fail {} -> synth tone".format(e)) + pcm_data = None + chunk_samples = SAMPLE_RATE * PCM_CHUNK_MS // 1000 + chunk_bytes = chunk_samples * 2 + wrote = 0 + while _test_running and utime.ticks_diff(end_ms, utime.ticks_ms()) > 0: + try: + if _play_pcm is None: + break + if pcm_data: + if pcm_off >= len(pcm_data): + pcm_off = 0 + piece = pcm_data[pcm_off : pcm_off + chunk_bytes] + pcm_off += len(piece) + if not piece: + piece = _pcm_gen_chunk() + else: + piece = _pcm_gen_chunk() + ret = _pcm_stream_write(_play_pcm, piece) + if ret is None or ret < 0: + _pcm_errors.append("stream_write ret={}".format(ret)) + else: + wrote += ret + except Exception as e: + _pcm_errors.append(str(e)) + usys.print_exception(e) + break + utime.sleep_ms(PCM_CHUNK_MS // 2) + _feed_done = 1 + _log("TTS feed thread exit wrote={} errors={}".format(wrote, len(_pcm_errors))) + + +def _local_mp3_play(path): + """对齐 _vc_local_play:aud.play 播本地 MP3,不停 PCM。""" + _log("local aud.play path={}".format(path)) + try: + _aud.play(1, 1, path) + return True + except Exception as e: + _log("local aud.play FAIL: {}".format(e)) + usys.print_exception(e) + return False + + +def _stop_all(): + global _test_running + _test_running = 0 + utime.sleep_ms(80) + try: + _aud.stopPlayStream() + except Exception: + pass + try: + _aud.stopAll() + except Exception: + pass + _pcm_close() + utime.sleep_ms(100) + + +def scenario_sequential(local_path, wav_path=None): + """先 TTS 再本地 MP3,作基线。""" + global _test_running, _pcm_errors, _local_cb_events + _pcm_errors = [] + _local_cb_events = [] + _log("=== sequential: TTS then local mp3 ===") + _test_running = 1 + try: + _pcm_open() + _tts_feed_thread(TEST_DURATION_MS, wav_path) + utime.sleep_ms(TEST_DURATION_MS + 200) + _pcm_close() + utime.sleep_ms(300) + _local_mp3_play(local_path) + utime.sleep_ms(2500) + finally: + _stop_all() + _log("sequential done local_cb={} pcm_err={}".format( + _local_cb_events, len(_pcm_errors))) + + +def scenario_concurrent(local_path, wav_path=None, overlap_delay_ms=200): + """ + TTS PCM 与本地 MP3 并行(模拟长对话边播 TTS è¾¹ think/start 短音)。 + overlap_delay_ms:开 PCM 后延迟多久播本地 MP3。 + """ + global _test_running, _pcm_errors, _local_cb_events + _pcm_errors = [] + _local_cb_events = [] + _log("=== concurrent: TTS + local mp3 overlap_delay={}ms ===".format( + overlap_delay_ms)) + _test_running = 1 + tid = None + try: + _pcm_open() + try: + _thread.stack_size(12 * 1024) + except Exception: + pass + tid = _thread.start_new_thread( + _tts_feed_thread, (TEST_DURATION_MS + 1500, wav_path)) + utime.sleep_ms(overlap_delay_ms) + ok_local = _local_mp3_play(local_path) + wait_ms = TEST_DURATION_MS + 2000 + t0 = utime.ticks_ms() + while utime.ticks_diff(utime.ticks_ms(), t0) < wait_ms: + if _feed_done: + break + utime.sleep_ms(50) + _log("concurrent local_ok={} pcm_errors={} local_cb={}".format( + ok_local, _pcm_errors, _local_cb_events)) + except Exception as e: + _log("concurrent FAIL: {}".format(e)) + usys.print_exception(e) + finally: + _test_running = 0 + _stop_all() + return len(_pcm_errors) == 0 + + +def run_all(): + _log("start") + _init_audio() + local_path = _pick_first_existing(LOCAL_MP3_CANDIDATES) + wav_path = _pick_first_existing(PCM_WAV_CANDIDATES) + if not local_path: + _log("ERROR: no local mp3 found, candidates={}".format(LOCAL_MP3_CANDIDATES)) + return + _log("local_mp3={} wav_pcm={}".format(local_path, wav_path or "(synth)")) + try: + scenario_sequential(local_path, wav_path) + utime.sleep_ms(800) + ok = scenario_concurrent(local_path, wav_path, overlap_delay_ms=150) + if ok: + _log("RESULT: concurrent OK (no pcm stream_write errors)") + else: + _log("RESULT: concurrent HAS errors, see pcm_errors above") + except Exception as e: + _log("run_all exception") + usys.print_exception(e) + finally: + _stop_all() + _log("end") + + +if __name__ == "__main__": + run_all() diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_buttons.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_buttons.py index 4d9d34a7..1f4ab88a 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_buttons.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_buttons.py @@ -1,1098 +1,1167 @@ -# -*- 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.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("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 btnInfo_queue_thread(self): - while True: - data = self.btnInfo_queue.get() - self.btnInfo_queue_count-=1 - #EventMesh.debug_log("Info_queue:data = {}".format(data)) - if data != None: - try: - if "data" in data : - data["func"](data["data"]) - else : - data["func"]() - except Exception as e: - #EventMesh.debug_log("btnInfo_queue:ERROR:{}".format(e)) - if "data" in data : - print(data["data"]) - usys.print_exception(e) - def publish_AI_btnInfo(self,event,msg): - #print("publish_AI_btnInfo 1") - 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) - +# -*- 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) + diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_common.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_common.py index ef5443b4..728d240d 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_common.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_common.py @@ -1,112 +1,112 @@ -# -*- 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 "", "" - +# -*- 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 index 00000000..88db1527 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_interrupt.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""长对话/PTT 打断上下文:绑定 turn_gen,收敛 is_ruppt_tts 等 legacy 标志。""" +try: + import utime +except Exception: + utime = None + + +class InterruptContext(object): + KIND_NONE = "" + KIND_LONG_SHORT = "long_short" + KIND_PTT_SHORT = "ptt_short" + KIND_PTT_LONG = "ptt_long" + + def __init__(self): + self.active = 0 + self.kind = self.KIND_NONE + self.gen = 0 + self.pending_uplink = 0 + self.drop_tts = 0 + self.ruppt_tts = 0 + self.settled_ms = 0 + self.cancel_deadline_ms = 0 + + def clear(self, vc=None): + self.active = 0 + self.kind = self.KIND_NONE + self.gen = 0 + self.pending_uplink = 0 + self.drop_tts = 0 + self.ruppt_tts = 0 + self.settled_ms = 0 + self.cancel_deadline_ms = 0 + if vc is not None: + self._sync_legacy(vc) + + def begin(self, vc, kind, pending_uplink=0, cancel_wait_ms=800): + try: + gen = int(getattr(vc, "_vc_turn_gen", 0)) + except Exception: + gen = 0 + self.active = 1 + self.kind = kind or self.KIND_NONE + self.gen = gen + self.pending_uplink = 1 if pending_uplink else 0 + self.drop_tts = 1 + self.ruppt_tts = 1 + self.settled_ms = 0 + if utime and cancel_wait_ms: + try: + self.cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), int(cancel_wait_ms)) + except Exception: + self.cancel_deadline_ms = 0 + else: + self.cancel_deadline_ms = 0 + self._sync_legacy(vc) + return gen + + def settle(self, vc): + """打断开麦完成:清 is_ruppt/drop 以允许新 TTS;保留 settled_ms 供迟到 canceled 判 stale。""" + if utime: + try: + self.settled_ms = utime.ticks_ms() + except Exception: + self.settled_ms = 0 + self.pending_uplink = 0 + self.active = 0 + self.drop_tts = 0 + self.ruppt_tts = 0 + self._sync_legacy(vc) + + def end_drop_window(self, vc): + """新一轮 TTS 开始:结束丢弃窗口。""" + self.drop_tts = 0 + self.ruppt_tts = 0 + self.settled_ms = 0 + self.pending_uplink = 0 + self.active = 0 + self._sync_legacy(vc) + + def is_stale_gen(self, vc): + try: + return int(getattr(vc, "_vc_turn_gen", 0)) != int(self.gen) + except Exception: + return False + + def _sync_legacy(self, vc): + """与 vc_session_io / media 中既有 getattr 兼容。""" + try: + vc._vc_pending_interrupt_uplink = 1 if self.pending_uplink else 0 + vc.is_ruppt_tts = 1 if self.ruppt_tts else 0 + vc._drop_tts_audio = 1 if self.drop_tts else 0 + vc._long_dialogue_interrupt_settled_ms = int(self.settled_ms or 0) + vc._vc_cancel_deadline_ms = int(self.cancel_deadline_ms or 0) + except Exception: + pass + + def load_legacy(self, vc): + try: + self.pending_uplink = 1 if getattr(vc, "_vc_pending_interrupt_uplink", 0) else 0 + self.ruppt_tts = 1 if getattr(vc, "is_ruppt_tts", 0) else 0 + self.drop_tts = 1 if getattr(vc, "_drop_tts_audio", 0) else 0 + self.settled_ms = int(getattr(vc, "_long_dialogue_interrupt_settled_ms", 0) or 0) + self.cancel_deadline_ms = int(getattr(vc, "_vc_cancel_deadline_ms", 0) or 0) + self.active = 1 if (self.pending_uplink or self.ruppt_tts or self.drop_tts) else 0 + except Exception: + pass 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 index 00000000..d9436cb1 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_key_router.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +"""按键表驱动:mode × UX × 事件 → 既有 vc_session 方法。""" +try: + import EventMesh + import constants + import utime +except Exception: + EventMesh = None + constants = None + utime = None + +from vc_ux import derive_ux_state, UX_IDLE, UX_RECORDING, UX_THINKING, UX_TTS, ux_name + +try: + import vc_config +except Exception: + vc_config = None + + +class KeyRouter(object): + @staticmethod + def dispatch_press(vc): + try: + if int(getattr(vc, "VC_mode", 0)) == 1: + return KeyRouter._ptt_press(vc) + return KeyRouter._long_ai_press(vc) + except Exception as e: + try: + import usys + usys.print_exception(e) + except Exception: + pass + return False + + @staticmethod + def handle_ptt_release(vc): + if int(getattr(vc, "VC_mode", 0)) != 1: + return False + if getattr(vc, "AI_key_long_press_flag", 0) == 1: + return False + ux = derive_ux_state(vc) + try: + print("[KEY][router] ptt_release ux={}".format(ux_name(ux))) + except Exception: + pass + if ux == UX_TTS: + try: + if vc._ptt_is_active_tts_playback(): + vc._ptt_stop_tts_on_short_press() + return True + except Exception: + pass + if ux == UX_THINKING: + try: + vc._ptt_stop_dialog_to_idle() + return True + except Exception: + pass + if ux == UX_IDLE: + try: + vc.key_prompt() + return True + except Exception: + pass + return False + + @staticmethod + def handle_power_short(vc): + """Power 短按:cancel → Close → 清 sid(设计稿幻灯片 6)。""" + ux = derive_ux_state(vc) + try: + print("[KEY][router] power_short ux={}".format(ux_name(ux))) + except Exception: + pass + try: + vc._vc_send_protocol_cancel("power_short/ux={}".format(ux_name(ux))) + except Exception: + pass + try: + vc._vc_close_reason = "power_short" + except Exception: + pass + try: + if EventMesh: + EventMesh.publish("publish_AI_btnInfo", {"func": vc.Close}) + except Exception: + pass + try: + if EventMesh and constants: + EventMesh.publish(constants.EVENT.SYSTEM_UP) + except Exception: + pass + return True + + @staticmethod + def _long_ai_press(vc): + ux = derive_ux_state(vc) + try: + print("[KEY][router] ai_press ux={} {}".format( + ux_name(ux), getattr(vc, "_vc_fsm_log_snip", lambda: "")())) + except Exception: + pass + if ux == UX_RECORDING: + try: + return vc._vc_restart_record_same_dialog() + except Exception: + pass + if ux in (UX_THINKING, UX_TTS): + try: + vc._long_dialogue_interrupt_on_short_press() + return True + except Exception: + pass + if vc._ai_key_press_long_dialogue_session(): + return True + if vc._ai_key_press_long_dialogue_wake(): + if EventMesh and constants: + EventMesh.publish(constants.EVENT.SYSTEM_UP) + return True + return KeyRouter._long_fallback_start(vc) + + @staticmethod + def _long_fallback_start(vc): + try: + vc.Close_flag = 0 + vc._skip_started_uplink_once = 0 + vc._vc_long_dialogue_key_arm_session() + vc._long_dialogue_publish_listening("key.press/fallback") + vc.get_config_timer_count = 0 + vc.key_state = 1 + vc.AI_TTS_str = "" + vc.AI_ASR_str = "" + vc.AI_ASR_str_tmp = "" + EventMesh.publish("publish_AI_btnInfo", {"func": vc.AI_VC_start}) + vc.AI_key_long_press_flag = 0 + return True + except Exception: + return False + + @staticmethod + def _ptt_press(vc): + return True + + @staticmethod + def ptt_release_after_long_press(vc): + """PTT 长按松手:录音过短 → notify TTS + idle。""" + if getattr(vc, "VC_record_state_flag", 0) == 0: + return False + min_ms = 400 + tts = "录音时间太短,请按住圆形按钮说话" + try: + if vc_config: + min_ms = int(vc_config.get_int("PTT_MIN_RECORD_MS", min_ms)) + tts = vc_config.get_attr("PTT_RECORD_TOO_SHORT_TTS", tts) or tts + except Exception: + pass + t_open = int(getattr(vc, "_ptt_record_open_ms", 0) or 0) + if not t_open or utime is None: + return False + try: + if utime.ticks_diff(utime.ticks_ms(), t_open) >= min_ms: + return False + except Exception: + return False + try: + vc._ptt_abort_short_record(tts) + except Exception: + pass + return True 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 index 00000000..c834eac8 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_media_arb.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""媒体车道仲裁:llm_pcm / notify_tts / local(对齐设计稿幻灯片 4)。""" +try: + import EventMesh +except Exception: + EventMesh = None + +LANE_LLM_PCM = 1 +LANE_NOTIFY = 2 +LANE_LOCAL = 3 + + +class MediaArbiter(object): + @staticmethod + def llm_active(vc): + try: + return vc._vc_tts_playback_active() + except Exception: + return False + + @staticmethod + def before_local(vc): + """local 播:仅打断 llm_pcm;不停录、不断 notify WS。""" + try: + if MediaArbiter.llm_active(vc): + try: + vc._ptt_hard_mute_local_playback() + except Exception: + pass + try: + vc.VC_Play_End() + except Exception: + pass + except Exception: + pass + + @staticmethod + def before_notify(vc): + """notify TTS:关 llm_pcm + local(设计稿;会话接管仍由 play_tts_cb 建链)。""" + try: + MediaArbiter.before_local(vc) + if getattr(vc, "_vc_local_play_busy", None) and vc._vc_local_play_busy(): + try: + vc.aud.stopAll() + except Exception: + pass + if hasattr(vc, "_vc_stop_downlink_for_interrupt"): + vc._vc_stop_downlink_for_interrupt("notify") + except Exception: + pass + + @staticmethod + def before_llm_pcm(vc): + """llm 下行:停 local 短音;notify èµ° defer 队列。""" + try: + if getattr(vc, "_vc_local_play_busy", None) and vc._vc_local_play_busy(): + try: + vc.aud.stopAll() + except Exception: + pass + except Exception: + pass + + @staticmethod + def montage_allowed(vc): + """llm 播时缓存/跳过 montage(vc_buttons 已有类似逻辑,集中判断)。""" + if MediaArbiter.llm_active(vc): + return False + try: + if EventMesh and EventMesh.publish("get_vc_tts_playback_active") == 1: + return False + except Exception: + pass + return True 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 index 00000000..79f60e14 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_mode_strategy.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""单对话 / 长对话按键策略:入口仅转发 KeyRouter(设计稿表驱动)。""" +try: + import EventMesh +except Exception: + EventMesh = None +try: + from vc_key_router import KeyRouter +except Exception: + KeyRouter = None + + +class VCModeStrategy(object): + def on_key_press(self, vc): + return False + + def on_key_release(self, vc): + return False + + def on_key_long_press(self, vc): + return False + + +class LongDialogueStrategy(VCModeStrategy): + def on_key_press(self, vc): + if KeyRouter is not None: + return KeyRouter.dispatch_press(vc) + return False + + def on_key_release(self, vc): + return False + + def on_key_long_press(self, vc): + if vc._vc_ms_keys_blocked("AI_long_press"): + return True + if getattr(vc, "trans_flag", 0) == 1: + vc.Close() + EventMesh.publish("start_voice_record", 4) + vc.AI_key_long_press_flag = 1 + if hasattr(vc, "_vc_key_router_press_cb"): + vc._vc_enqueue_btn(vc._vc_key_router_press_cb, priority=1) + else: + vc._vc_enqueue_btn(vc._ai_key_press_long_dialogue_dispatch, priority=1) + return True + + +class PTTStrategy(VCModeStrategy): + def on_key_press(self, vc): + if KeyRouter is not None: + return KeyRouter.dispatch_press(vc) + return True + + def on_key_release(self, vc): + vc._ai_key_release_handle_ptt() + return True + + def on_key_long_press(self, vc): + if vc._vc_ms_keys_blocked("AI_long_press"): + return True + if getattr(vc, "trans_flag", 0) == 1: + vc.Close() + EventMesh.publish("start_voice_record", 4) + vc.AI_key_long_press_flag = 1 + try: + vc._ptt_idle_latch = 0 + vc._ptt_short_press_stop = 0 + except Exception: + pass + vc._vc_enqueue_btn(vc.ptt_begin, priority=1) + return True + + +def strategy_for_mode(vc_mode): + try: + if int(vc_mode) == 1: + return PTTStrategy() + except Exception: + pass + return LongDialogueStrategy() 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 index 00000000..3516c108 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_notify.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +"""通知通道:server.started 媒体排队、play_tts 与对话会话分流。""" +try: + from vc_fsm import VCTurnState +except Exception: + VCTurnState = None +try: + import vc_config +except Exception: + vc_config = None + +# play_tts 通道 +CHANNEL_TAKEOVER = "takeover" # 断连重连 WS synthesis(IoT 查找设备/模式切换/无对话) +CHANNEL_DEFER = "defer" # 对话进行中排队,空闲后播放(is_close_VC=0 的 IoT 查询等) + +# TTS 业务种类(_pending_tts_kind / play_tts 第三参数) +TTS_KIND_FIND_DEVICE = "find_device" +TTS_KIND_MODE_SWITCH_LONG = "mode_switch_long" +TTS_KIND_MODE_SWITCH_PTT = "mode_switch_ptt" +TTS_KIND_BOOT_PROMPT = "boot_prompt" +TTS_KIND_PROMPT = "prompt" + +# 须立即占用会话、不可 defer +_TAKEOVER_KINDS = ( + TTS_KIND_FIND_DEVICE, + TTS_KIND_MODE_SWITCH_LONG, + TTS_KIND_MODE_SWITCH_PTT, + TTS_KIND_BOOT_PROMPT, + TTS_KIND_PROMPT, +) + +# server.started plan op +OP_LONG_UPLINK = "long_uplink" +OP_ALARM_VOICE = "alarm_voice" +OP_VOICE_URL = "voice_url" +OP_SYNTHESIS = "synthesis" +OP_SYNTHESIS_HIS_TIMER = "synthesis_his_timer" +OP_SYNTHESIS_WAIT_TIMER = "synthesis_wait_timer" +OP_SYNTHESIS_WAIT_SINGLE = "synthesis_wait_single" +OP_SYNTHESIS_VOICE_TIMER = "synthesis_voice_timer" +OP_SYNTHESIS_MUSIC_TIMER = "synthesis_music_timer" +OP_IDLE_UPLINK = "idle_uplink" +OP_PTT_ROUND = "ptt_round" +OP_UPLOAD_TONE = "upload_tone" + + +def dialog_session_active(vc): + """对话占用中:通知类 play_tts 应 defer。""" + try: + if getattr(vc, "Close_flag", 0) == 1: + return False + if getattr(vc, "VC_satrt_flag", 0) != 1: + return False + if getattr(vc, "AI_client_VC_isconnect", 0) != 1: + return False + if getattr(vc, "VC_Play_flag", 0) == 1: + return True + if getattr(vc, "VC_record_state_flag", 0) != 0: + return True + # 7PIN_1_5 无闹钟业务;alarm_play_flag 仅 legacy(如 play_single_voice) + if getattr(vc, "music_play_flag", 0) == 1: + return True + if VCTurnState is not None and getattr(vc, "fsm", None) is not None: + t = getattr(vc.fsm, "turn", VCTurnState.Idle) + if t not in (VCTurnState.Idle, VCTurnState.Listening): + return True + except Exception: + pass + return False + + +class NotifyScheduler(object): + @staticmethod + def classify_play_tts(vc, is_close_vc): + try: + if is_close_vc: + return CHANNEL_TAKEOVER + except Exception: + pass + try: + k = getattr(vc, "_pending_tts_kind", "") or getattr(vc, "_last_tts_kind", "") or "" + if k in _TAKEOVER_KINDS: + return CHANNEL_TAKEOVER + except Exception: + pass + if dialog_session_active(vc): + return CHANNEL_DEFER + return CHANNEL_TAKEOVER + + @staticmethod + def plan_on_started(vc, skip_long_uplink=0): + """替代 vc_session_io started 内 if-elif 媒体分支。""" + plan = [] + try: + import EventMesh + except Exception: + EventMesh = None + + # 长对话:started 补开麦(原 597-623,与媒体队列正交) + try: + if ( + not skip_long_uplink + and getattr(vc, "VC_mode", 0) == 0 + and getattr(vc, "Close_flag", 0) == 0 + ): + plan.append({"op": OP_LONG_UPLINK}) + except Exception: + pass + + tts = getattr(vc, "AI_TTS_str", "") or "" + url = getattr(vc, "AI_new_voice_url", "") or "" + alarm = getattr(vc, "alarm_play_flag", 0) == 1 + music = getattr(vc, "music_play_flag", 0) == 1 + + # 闹钟铃声(其它机型);7PIN_1_5 无闹钟,alarm_play_flag 通常为 0 + if url and alarm: + plan.append({"op": OP_ALARM_VOICE}) + elif not tts and url and not music: + plan.append({"op": OP_VOICE_URL}) + elif tts and not url: + plan.append({"op": OP_SYNTHESIS}) + elif tts and url and not music: + try: + his = EventMesh.publish("get_history_voice_flag") == 1 if EventMesh else False + except Exception: + his = False + if his: + try: + wait = EventMesh.publish("get_wait_voice_flag") == 0 if EventMesh else True + except Exception: + wait = True + if wait: + plan.append({"op": OP_SYNTHESIS_HIS_TIMER}) + else: + try: + notice = getattr(vc, "pwk_notice_flag", 0) == 0 + except Exception: + notice = True + if notice: + plan.append({"op": OP_SYNTHESIS_WAIT_TIMER}) + else: + plan.append({"op": OP_SYNTHESIS_WAIT_SINGLE}) + else: + plan.append({"op": OP_SYNTHESIS_VOICE_TIMER}) + elif tts and url and music: + plan.append({"op": OP_SYNTHESIS_MUSIC_TIMER}) + else: + try: + if "vc_config" in globals() and vc_config and vc_config.get_bool( + "UPLOAD_START_TONE_ENABLED", False + ): + plan.append({"op": OP_UPLOAD_TONE}) + except Exception: + pass + try: + if getattr(vc, "VC_mode", 0) == 0: + plan.append({"op": OP_IDLE_UPLINK}) + else: + plan.append({"op": OP_PTT_ROUND}) + except Exception: + plan.append({"op": OP_IDLE_UPLINK}) + return plan 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 index 00000000..639d6f68 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_overlay.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""VC 叠加态:模式切换等短时覆盖 FSM 的按键屏蔽(兼容 _vc_ms_phase)。""" + +OVERLAY_IDLE = 0 +OVERLAY_MODE_SWITCH_PREP = 1 +OVERLAY_MODE_SWITCH_PLAYING = 2 +OVERLAY_MODE_SWITCH_CLOSING = 3 +OVERLAY_MODE_SWITCH_DONE = 3 # 与 CLOSING 同值,兼容旧 phase=3 + + +def overlay_from_vc(vc): + try: + return int(getattr(vc, "_vc_overlay", getattr(vc, "_vc_ms_phase", 0)) or 0) + except Exception: + return OVERLAY_IDLE + + +def overlay_set(vc, state): + try: + vc._vc_overlay = int(state or 0) + vc._vc_ms_phase = vc._vc_overlay + except Exception: + pass + + +def overlay_keys_blocked(vc, tag=""): + """模式切换 PREP/PLAYING/CLOSE 期间屏蔽 AI/电源短按(单入口)。""" + try: + phase = overlay_from_vc(vc) + if phase in ( + OVERLAY_MODE_SWITCH_PREP, + OVERLAY_MODE_SWITCH_PLAYING, + OVERLAY_MODE_SWITCH_CLOSING, + ): + try: + print("[VC][overlay] key blocked phase={} tag={}".format(phase, tag)) + except Exception: + pass + return True + if getattr(vc, "_vc_ms_close_pending", 0) == 1: + try: + print("[VC][overlay] key blocked close_pending tag={}".format(tag)) + except Exception: + pass + return True + until = int(getattr(vc, "_vc_ms_key_block_until_ms", 0) or 0) + if until: + try: + import utime + if utime.ticks_diff(until, utime.ticks_ms()) > 0: + try: + print("[VC][overlay] key blocked cooldown tag={}".format(tag)) + except Exception: + pass + return True + except Exception: + pass + except Exception: + pass + return False 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 index 00000000..7e268320 --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_present.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +"""LED 呈现:由 UX 四态 + 覆盖态推导 ai_state(唯一 set_led_state 入口)。""" +try: + from vc_fsm import VCTurnState +except Exception: + VCTurnState = None +try: + from vc_ux import derive_ux_state, UX_RECORDING, UX_THINKING, UX_TTS +except Exception: + derive_ux_state = None + + +def build_present_state(vc): + """只读聚合,不写 vc。""" + st = { + "mode": 0, + "turn": -1, + "force_idle": 0, + "hold_listening": 0, + "prefer_off": 0, + "stream_active": 0, + "ptt_await_server": 0, + "override_ai": None, + "override_prefer_off": 0, + } + try: + st["mode"] = int(getattr(vc, "VC_mode", 0)) + except Exception: + pass + try: + st["turn"] = int(getattr(getattr(vc, "fsm", None), "turn", -1)) + except Exception: + pass + try: + ov = getattr(vc, "_led_override", None) + if ov and isinstance(ov, (tuple, list)) and len(ov) >= 1: + st["override_ai"] = ov[0] + st["override_prefer_off"] = 1 if (len(ov) > 1 and ov[1]) else 0 + except Exception: + pass + try: + if hasattr(vc, "_ptt_should_force_idle_led") and vc._ptt_should_force_idle_led(): + st["force_idle"] = 1 + except Exception: + pass + try: + if hasattr(vc, "_vc_should_hold_listening_led") and vc._vc_should_hold_listening_led(): + st["hold_listening"] = 1 + except Exception: + pass + try: + if hasattr(vc, "_vc_led_should_standby_idle") and vc._vc_led_should_standby_idle(): + st["force_idle"] = 1 + if hasattr(vc, "_vc_led_standby_prefer_off") and vc._vc_led_standby_prefer_off(): + st["prefer_off"] = 1 + except Exception: + pass + try: + if hasattr(vc, "_vc_tts_playback_active") and vc._vc_tts_playback_active(): + st["stream_active"] = 1 + except Exception: + pass + try: + st["ptt_await_server"] = 1 if getattr(vc, "_ptt_await_server", 0) else 0 + except Exception: + pass + return st + + +def present_to_led_msg(vc, present=None): + """返回传给 set_led_state 的 dict;不发布。""" + if present is None: + present = build_present_state(vc) + if present.get("override_ai"): + msg = {"ai_state": present["override_ai"]} + if present.get("override_prefer_off") or present.get("prefer_off"): + msg["prefer_off"] = 1 + return msg + if present.get("force_idle"): + msg = {"ai_state": "idle"} + if present.get("prefer_off"): + msg["prefer_off"] = 1 + return msg + if present.get("hold_listening"): + return {"ai_state": "listening"} + if present.get("ptt_await_server") and int(present.get("mode", 0)) == 1: + return {"ai_state": "thinking"} + if present.get("stream_active"): + return {"ai_state": "playing"} + if derive_ux_state is not None: + try: + ux = derive_ux_state(vc) + if ux == UX_RECORDING: + return {"ai_state": "listening"} + if ux == UX_TTS: + return {"ai_state": "playing"} + if ux == UX_THINKING: + return {"ai_state": "thinking"} + except Exception: + pass + msg = {"ai_state": "idle"} + if present.get("prefer_off"): + msg["prefer_off"] = 1 + return msg diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py index 27f42874..f93fd6de 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py @@ -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): - """新一轮服务端 TTS(非打断尾包):允许下行播放。""" + """新一轮服务端 TTS:结束打断丢弃窗口,允许 VC_Play_Start 与下行写入。""" 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): - """长对话按键开录意图:等 server.started 后再滴/开麦。""" + """长对话按键唤醒/续聊:滴与蓝灯在 _vc_user_record_entry 统一处理。""" 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 长按:等 started / asr.started 后再滴。""" + """PTT 长按:等 started / asr.started 后开录,无开录提示音(REQ-AI-07)。""" 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): - """单对话:服务端仍在思考/收 TTS/播报时,开录前须先 cancel 并等 canceled。""" + """单对话:思考/TTS/播中/松手等服务器时,开录前须先 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() diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py index 47cc7fcd..c53e180b 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py @@ -1,80 +1,107 @@ # -*- 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 diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py index 440f3f39..b60d3b8e 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py @@ -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 index 00000000..8ef0223a --- /dev/null +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_ux.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""设计稿 UX 四态:idle / recording / thinking / tts(与 FSM 八态解耦)。""" +try: + from vc_fsm import VCTurnState +except Exception: + VCTurnState = None + +UX_IDLE = 0 +UX_RECORDING = 1 +UX_THINKING = 2 +UX_TTS = 3 + +_UX_NAMES = ("idle", "recording", "thinking", "tts") + + +def ux_name(ux): + try: + return _UX_NAMES[int(ux)] + except Exception: + return "idle" + + +def derive_ux_state(vc): + """对齐 录音上传与播放-演示文稿:按键表与 LED 均基于此四态。""" + try: + if hasattr(vc, "_vc_tts_playback_active") and vc._vc_tts_playback_active(): + return UX_TTS + except Exception: + pass + try: + if getattr(vc, "VC_record_state_flag", 0) != 0: + return UX_RECORDING + except Exception: + pass + try: + if int(getattr(vc, "_ptt_await_server", 0)) == 1: + return UX_THINKING + except Exception: + pass + if VCTurnState is not None: + try: + t = getattr(getattr(vc, "fsm", None), "turn", VCTurnState.Idle) + # PTT 轮间:FSM 仍 Playing 但本地已停播 → 设计 idle + if int(getattr(vc, "VC_mode", 0)) == 1 and t in ( + VCTurnState.Playing, + VCTurnState.AwaitPlayedAck, + ): + return UX_IDLE + if t == VCTurnState.Thinking: + return UX_THINKING + if t in ( + VCTurnState.TtsReceiving, + VCTurnState.Playing, + VCTurnState.AwaitPlayedAck, + ): + return UX_TTS + except Exception: + pass + return UX_IDLE